Writing the Hooks
This page describes the part of the C++ API, and their usage pattern, that are relevant to the implementation of the hooks in a Rock component. You should already have familiarized yourself with the component interface and its lifecycle state machine.
Interface Objects in C++
There is one C++ object for each declared interface element. The name of the object is the name of the element with a leading underscore. For instance,
# Another documentation string
output_port 'out', 'another_type'
is mapped to a C++ attribute of type RTT::InputPort<another_type>
called
_out
.
Code Generation and Code Updates
oroGen will not update a file that is already present on disk. Whenever an
interface object requires the addition or removal of a method (operations and
dynamic properties), one must manually modify the corresponding files in
tasks/
. To ease the process, oroGen does update template files in
templates/
In order to achieve this, each component is implemented in two classes: the one
you modify - which has the name declared in the orogen file - and a Base
class that is directly modified by orogen. The latter is where the interface
objects are defined. Have a look if you're interested in understanding more
about the component's implementation. It's in the .orogen/tasks/
directory
Properties
Plain properties are read only. They must be read either in the configureHook()
or
in the startHook()
. Syskit will write them before calling configure
.
A property is read with .get()
:
configuration_type config = _name.get();
Dynamic properties can be read at runtime. However, the property update method
is called in-between two hooks, and therefore any delay due to the update will
impact the component's update rate, plus one must take into account that the
state of the system does change in-between two updateHook
calls. In other
words, dynamic properties have a cost both on the component's implementation
complexity and on its predictability. Use them wisely.
There are two ways to handle dynamic properties. Either by reading the property
object repeatedly, or by implementing a hook method that is called when the
property is written at runtime. This hook method is called setPropertyName
for a property_name
property. In doubt, check the template files in
templates/
.
If you do reimplement this method, always call the method from the base class (as the generated template instructs you to do).
Ports
The ports map to C++ attributes on the component class, with the name prefixed
by an underscore (i.e. _in
and _out
here. The most common operation is to
read the input port and write an output port;
my_type in_sample;
RTT::DataFlow status = _in.read(sample);
another_type out_sample;
_out.write(out_sample);
The status
return value indicates whether there was nothing to read
(RTT::NoData
), a new, never-read sample was read (RTT::NewData
) or an
already-read sample was read (RTT::OldData
). Let's now look at the common
port-reading patterns.
All input ports are cleared on startHook
, i.e. just after startHook
, the
status will very likely be NoData
. This is done so that the component does not read
stale data from its last execution.
Input ports can be used in the C++ code in two ways, which one you want to use depends on what you actually want to do.
-
if you want to read all new samples that are on the input (since an input port can be connected to multiple output ports) {: #port-read-while}
// my_type is the declared type of the port my_type sample; while (_in.read(sample, false) == RTT::NewData) { // got a new sample, do something with it // The 'false' here is a small optimization }
-
if you are just interested by having some data
// my_type is the declared type of the port my_type sample; if (_in.read(sample) != RTT::NoData) { // got a sample, do something with it }
Finally, to write on an output, you use 'write':
// another_type is the declared type of the port
another_type data = calculateData();
_out.write(data);
Another operation of interest is the connected() predicate. It tests if there is a data provider that will send data to input ports (in.connected()) or if there is a listener component that will get the samples written on output ports.
For instance,
if (_out.connected())
{
// generate the data for _out only if somebody may be interested by it. This
// is useful if generating // the data is costly
another_type data = calculateData();
_out.write(data);
}
Dynamic Ports
Components that have a dynamic port mechanism must create these ports in
configureHook
. They will usually do so based on their configuration (i.e. values
set on their properties). In addition, for the purpose of Syskit integration, these
components need to declare that they have dynamic ports
For the purpose of example, let's assume that we're implementing a time source, and need different ports to be at different periods. A valid configuration type would be
struct PortConfiguration
{
std::string port_name;
base::Time period;
};
To hold the list of created ports, the task would need an attribute
typedef RTT::InputPort<type::of::the::Port> TimeOutputPort;
std::vector<TimeOutputPort*> mCreatedPorts;
The task's configureHook
would create the ports (after checking for e.g. name
collisions)
for (auto const& conf : _port_configurations.get())
{
TimeOutputPort* port = new TimeOutputPort("name_of_the_port");
ports()->addPort(port);
mCreatedPorts.push_back(port);
}
and cleanupHook
would remove and delete them
while (!mCreatedPorts.empty())
{
TimeOutputPort* port = mCreatedPorts.back();
mCreatedPorts.pop_back();
ports->removePort(created_port->getName());
delete created_port;
}
Operations
Operations map to a C++ method. E.g. for the declaration
operation('operationName').
returns('int').
argument('arg0', '/arg/type').
argument('arg1', '/example/other_arg')
oroGen will generate a method with the signature
return_type operationName(arg::type const& arg0, example::other_arg const& arg1);
By default, the operations are run into the thread of the callee, i.e. the thread of the component on which the operation is defined. This is easier from a thread-safety point of view, as one thus guarantees that there won't be concurrent access to the task's internal state. However, it also means that the operation will be executed only when all the task's hooks have returned (waiting potentially long).
If it is desirable, one can design the operation's C++ method to be thread-safe and declare it as being executed in the caller thread instead of the callee thread. This is done with
operation('operationName').
returns('int').
argument('arg0', '/arg/type').
argument('arg1', '/example/other_arg').
runs_in_caller_thread