wiki:Tutorials/Beginner/Data Port Basics

This article provides the minimum information that everybody working with data ports should know. All other relevant features for component and application development are covered in Advanced/Data Ports.

Data flow is the primary communication paradigm in Finroc. Especially on lower levels of robot control systems, components (modules, groups) exchange informationen via data flow edges (e.g. in control loops). The Finstruct tool visualizes a set of connected components as a data flow graph.

Component interfaces consist of a set of ports. Data ports are used to easily transfer data from one component to all connected components. There are input and output ports: Output ports are used to publish data. Input ports are used to receive data from another component. Finally, there are routing ports. They are used in group interfaces and forward data to all connected input ports. From outside the group, they look like input or output ports.

In our tools, the following graphical representation is used:

Data Types

Each port class has a template parameter T. This is the type of data transferred via this port. Only ports that have compatible data types can be connected.

T can be any C++ data type that can be serialized to a binary stream using our serialization library rrlib_serialization. See Advanced/Suitable Port Data Types on making C++ data types serializable.

Creating ports

Component and application developers typically use the convenient port classes defined in module and group classes - e.g. tInput and tOutput in case of a simple tModule.

To add a port to a component interface, add it as a public member variable to the component class in the component's header file. In the provided code templates, there is a section for ports:

//----------------------------------------------------------------------
// Ports (These are the only variables that may be declared public)
//----------------------------------------------------------------------
public:

To add, for instance, an output port of type double, it is sufficient to add it here:

  tOutput<double> output_port

The port names are automatically generated from variable names in source code. This port would be called "Output Port" - unless a different port name is provided in the constructor.

Constructor parameters

Data ports have many features which can be enabled via constructor parameters - see Advanced/Data Ports. A port constructor with all these possible parameters would have a lot of parameters. However, only few of them are typically used. Specifying bounds, for instance, is only allowed for suitable types. That's why we decided to give ports a constructor that takes a variadic list of parameters.

The doxygen comment says about all there is to say:

  /*!
   * Constructor takes a variadic argument list... just any properties you want to assign to port.
   *
   * Port name and parent are usually determined automatically (however, only possible when port is direct class member).
   * If this is not possible/desired, name needs to be provided as first constructor argument - parent as arbitrary one.
   *
   * A string as first parameter is interpreted as port name; Any other or further string as config entry (irrelevant for ports).
   * A framework element pointer is interpreted as parent.
   * tFrameworkElement::tFlags arguments are interpreted as flags.
   * A tQueueSettings argument creates an input queue with the specified settings.
   * tBounds<T> are port's bounds.
   * tUnit argument is port's unit.
   * const T& is interpreted as port's default value.
   * tPortCreationInfo<T> argument is copied. This is only allowed as first argument.
   *
   * This becomes a little tricky when T is a string type. There we have these rules:
   * A String not provided as first argument is interpreted as default value.
   * Any further string is interpreted as config entry.
   */

To specify any constructor arguments (which is not required), add the variable to the component constructor's initializer list. The list is marked with the following line, in the component code templates:

  If you have some member variables, please initialize them here. Especially built-in types (like pointers!). Delete this line otherwise!

For the example, this could look like this:

  output_port("Other name for output port", 10)

The port would now have an initial value of 10 and a different name.

Connecting ports

Ports can be connected if their types are compatible. Basically, connecting can either be done in source code - or graphically using finstruct. Both is covered in the Simple Robot Simulation Tutorial.

In source code, ports are connected using their ConnectTo methods (see tPortWrapperBase). This is usually done in some group constructor.

  module1->output_port.ConnectTo(module2->input_port);

To connect many ports at once, the ConnectByName methods in tPortGroup can be handy.

  module1->GetSensorOutputs().ConnectByName(module2->GetSensorInputs());

Publishing data via output ports

Data can be published via output ports using the following pattern (we have an output port of type double in this example):

  data_ports::tPortDataPointer<double> buffer_to_publish = output_port.GetUnusedBuffer();
  *buffer_to_publish = 42;
  buffer_to_publish.SetTimestamp(timestamp); // optional
  output_port.Publish(buffer_to_publish);

tPortDataPointer is a smart pointer class for Finroc data port buffers. Usage is similar to std::unique_ptr (as it is movable).

Receiving data via input ports

Data can be received using a port's GetPointer() method.

  data_ports::tPortDataPointer<const double> received_buffer = input_port.GetPointer();

tPortDataPointer ensures that the buffer can safely be accessed until it goes out of scope. Note that received buffers are read-only (enforced by the const keyword).

Changed flags

Possibly, modules only want to trigger execution of code, if a port value has changed since the last cycle (last call of Update(), Sense() or Control()).

They can check this via a port's HasChanged() method:

  if (input_port.HasChanged())
  {
    // do something
  }

It is also possible to check whether any of the component's input ports has changed - e.g.:

  if (this->SensorInputChanged())
  {
    // At least one of your sensor input ports has changed. Do something useful with its data.
  }

Changed flags are automatically reset after executing Update(), Sense() or Control().

Note: The changed flag is always set when a port's buffer changes - even if this new buffer has the same value and timestamp.

It is guaranteed that the changed flag is set at least once on each incoming buffer (=> no buffer change gets lost). In rare cases, however, an incoming buffer can cause the changed flag to be set in two consecutive cycles. This is due to the lock-free port implementation.

If this is not acceptable, a module should maintain a copy of the last port value and compare this with any new value in order to detect changes.

Simplifications for cheaply copied types

Finroc data ports provide an optional more convenient API for cheaply copied types.

Cheaply copied types provide computationally inexpensive copy constructors and copy assignment operators. In particular, they do not manage any memory internally (as this is problematic in real-time code).

For such types, publishing can be done with a simple call to a port's Publish method (again, a double port in this example):

  output_port.Publish(42, optional_timestamp);

Values can be received using Get():

  double current_port_value = input_port.Get(optional_timestamp_buffer);

Port API

Ports provide a lot more functionality.

For output ports, the API can found in the these three classes: tOutputPort tPort tPortWrapperBase

For input ports, the API can found in the these three classes: tInputPort tPort tPortWrapperBase

Last modified 4 years ago Last modified on 17.12.2014 14:15:41

Attachments (1)

Download all attachments as: .zip