Component Interfaces

We'll cover in this page how to define your task's interface. All statements presented in this page are to be included in a component definition, i.e. between 'do' and 'end' in

# Documentation of the task context
task_context "ClassName" do
   needs_configuration

   ...
end

The only constraint on ClassName is that it must be different from the project name. How one is meant to interact with these elements in the task's own code is dealt with later

The needs_configuration statement is historical and should always be present.

Interface Elements

RTT Component Interface

  • Ports are used to transfer data between the components
  • Properties are used to store and set configuration parameters
  • Finally, Operations (not represented here) are used to do remote method calls on the components

As a general rule of thumb, the components should communicate with each other only through ports. The properties and operations (as well as the state machine covered in the next page) are meant to be used by a coordination layer, namely Syskit in our case.

Documentation for an interface element should be written as a comment directly on top of the declaration.

Ports

Ports are defined with

# A documentation string
input_port 'in', 'my_type'
# Another documentation string
output_port 'out', 'another_type'

Properties

Properties are defined with

# What this property is about
property "name", "configuration_type"

Plain properties must be read by the component only before it is started. If one needs to be able to change the value at runtime, the property must be declared dynamic (see next section)

Properties using simple types (booleans, ints, enums, …) can have a default value specified directly in the orogen file:

# If true, the image is halved
property "downscale_2x", "/bool", false

For more complex types, the initialization should be done in the task's constructors, e.g.

Task::Task(...) {
    _initial_position.set(Eigen::Vector3.Zero);
}

Dynamic Properties

Plain properties can't be changed at runtime. They must be set before the component is configured. To be allowed to change a property at runtime, declare it as dynamic in the orogen file:

# What this property is about
property("name", "configuration_type")
    .dynamic

Don't make everything dynamic. Use dynamic properties only for things that (1) won't cause unacceptable latency in the component's processing and (2) for which the "dynamicity" is easy to implement. A counter example is for instance a device whose change in parameter would take a few seconds. This should definitely not be dynamic. A good example would be a simple scaling parameter, which is only injected in a numerical equation - that is something that won't require any internal reinitialization.

By default, a dynamic property will simply be updated in a thread-safe way, so that the component's runtime hooks (e.g. updateHook) can read it safely. This is fine for simple properties, whose value can be read and applied at each execution cycle.

For more complex changes, that e.g. require the reconfiguration of the underlying library (or of the underlying device), it is possible to redefine an automatically generated virtual method which will be called on update. The method is always named set${property_name_with_first_letter_uppercase}. As always with orogen, in doubt, just have a look into the templates/tasks/ folder (once code generation ran after the addition of the dynamic attribute). The task template will contain a default implementation of this method, for instance:

bool PIDTask::setSettings(::std::vector<ActuatorSettings> const & value)
{
    return (motor_controller::PIDTaskBase::setSettings(value));
}

This default method just calls the default implementation, which will actually update the value of the property (here _settings) and return true to indicate the update was accepted.

When reimplementing this method, make sure that:

  • it returns true if the change was accepted, and that the property object was updated accordingly (just call the base method for that)
  • it returns false if the new value is not acceptable
  • it is error-safe, that is the underlying code should continue to behave as-if the value was not changed if an error occurs and the method returns false

This last point (error-safety) is in itself a good reason to avoid dynamic properties. Error safety can be rather hard to get right. You've been warned.

The set callbacks are not automatically called in configureHook, only when the component is running. If you want to call them, for instance because they do some validatio on the values, you may call the updateDynamicProperties() method which will, and return true if all setters have returned true, or false otherwise.

Operations

The operations offer a mechanism from which a task context can expose functionality through remote method calls. They are defined with:

# Documentation of the operation
operation('commandName')
    .argument('arg0', '/arg/type')
    .argument('arg1', '/example/other_arg')

Additionally, a return type can be added with

# Documentation of the operation
operation('operationName')
    .returns('int')
    .argument('arg0', '/arg/type')
    .argument('arg1', '/example/other_arg')

Note the dot at the beginning of all the additional operation definition statements. This dot is important and, if omitted, will lead to syntax errors. If no return type is provided, the operation returns nothing.

When to use an operation ? Well, don't. Mostly. Operations should very rarely be used, as they create hard synchronization between components. The one common case where an operation is actually useful is if something really expensive needs to rarely be done in the middle of the component processing, such as dumping an internal state that is really expensive to dump.

Dynamic Ports

Some components (e.g. the logger or the canbus components) may create new ports at runtime, based on their configuration. To integrate within Syskit, it is necessary to declare that such creation is possible. This is done with the dynamic_input_port and dynamic_output_port statements, possibly using a regular expression as name pattern and either a message type or nil for "type unknown".

The following for instance declares, in the Rock canbus::Task, that ports with arbitrary names might be added to the task interface, and that these ports will have the /canbus/Message type.

dynamic_output_port /.*/, "/canbus/Message"

oroGen currently provides no support for dynamic ports at the C++ level. dynamic_output_port and dynamic_input_port are purely declarative, it is the job of the component implementer to handle their creation and destruction. This is details later

Syskit expects dynamic ports to be created at configuration time and removed at cleanup time. Dynamic ports are modelled in Syskit using dynamic services

Inheritance

It is possible to make the components inherit from each other, and have the other oroGen features play well.

Given a Task base class, the subclass is defined with

task_context "SubTask", subclasses: "Task" do
end

When one does so, the component's subclass inherits from the parent's class, in the C++ way. This of course means that it has access to the methods defined on the parent class. From a component point of view, it also means that it inherits the parent's class interface.

When inheriting between task contexts, the following constraints will apply:

  • it is not possible to add a task interface object (port, property, …) that has the same name than one defined by the parent model.
  • the child shares the parent's state definitions

Finally, "abstract task models", i.e. task models that are used as a base for others, but which it would be meaningless to deploy since they don't have any functionality can be marked as abstract with

task_context "SubTask" do
    abstract
end

One can also inherit from a task defined by another oroGen package. Import the package first at the top of the .orogen file with

using_task_library "base_package"

and subclass the task from base_package using its full name:

task_context 'Task', subclasses: "base_package::Task" do
end

Next let's have a look at the component lifecycle state machine