Deployments
So far, we have declared and written a C++ component class. That is, we implemented the component's computation. We have however not yet breached the subject of describing when the component's methods (Hooks) are called. This is what the deployment is all about.
In Rock, each deployment is a separate binary (UNIX process) in which a certain
number of tasks have been instanciated. The role of the deployment is to:
- group threads in processes
- group tasks into threads (and specify the thread parameters)
- assign each task to a triggering mechanism, that defines in which conditions
the task's updateHook
will be called.
The combination of thread information and triggering mechanism is called an activity.
All the deployment definitions that are covered in this section are defined
within an oroGen project. They can either be done in the same project that
defines the tasks, or use tasks from other projects by loading them beforehand
with using_task_library 'project_name'
.
Default Deployments
orogen creates a default deployment for each declared non-abstract task. This default deployment puts each component in a single thread, in its own process. It uses a default triggering mechanism that is defined on the task context. This default activity should be considered a "sane default", but components should in general not rely on this activity being the only way it could be deployed.
In some cases, it is beneficial to deploy components differently than the default. This is done by defining explicit deployments.
Explicit Deployments
Deployment blocks declare one binary, that is a set of components along with their activities that are grouped into a single process.
deployment "test" do
<deployment statements>
add_default_logger
end
This statement generates a "test" binary which will be installed by CMake. If that is not desired, for instance if it is for testing purposes only, add the do_not_install statement in the block:
deployment "test" do
do_not_install
<other deployment statements>
end
The most basic thing that can be done in a deployment is listing the tasks that should be instantiated. It is done by using the 'task' statement:
task 'TaskName', 'orogen_project::TaskClass'
It will create a task with the given name and class. By default, that task will
have its own thread. Use using_task_library
to import tasks from another project.
Triggers
Trigger statements can be placed either in the component's task_context
block, or as a refinement of an explicit deployment's task
statement.
The first case looks like
task_context "Task" do
periodic 0.1
end
The second case looks like
deployment 'test' do
task('task', 'Task').
periodic(0.1)
end
When a task is added in the explicit deployment, the component's default
activity will be used (as defined in its task_context
block). However, the
periodic
and fd_driven
activity statements that are available within the
task_context
statement can also be used in a deployment's definition to
change. port_driven
cannot. This overrides the default.
Note the dot at the end of the task
statement. This is a fluid
interface, don't forget that each modifier for the task definition is actually
a chain of method calls, and require the dots.
Periodic Triggering (periodic
)
This is the most simple triggering method. When a task is declared periodic, its
updateHook
will be called with a fixed time period. The task is within its own
thread.
To use this triggering mechanism, simply add the periodic(period)
statement
to the task context:
task_context 'Task' do
...
periodic 0.01
end
The period is given in seconds. The periodic activity cannot be combined with other triggering mechanisms.
Port-Driven Triggering (port_driven
)
A port-driven task is a task that wants to perform computations whenever new data is available on its input ports. In general, data-processing tasks (as for instance image processing tasks) fall into that category: their goal is to take data from their input, process it, and push it to their outputs.
A port-driven task is declared by using the port_driven
statement.
task_context "Task" do
input_port 'image', '/Camera/Frame'
input_port 'parameters', '/SIFT/Parameters'
output_port 'features' '/SIFT/FeatureSet'
port_driven 'image'
end
During runtime, the updateHook
method will be called when new data arrives on
the listed ports (in this case image
). Other input ports are ignored by the
triggering mechanism. Obviously, the listed ports must be input ports. In
addition, they must be declared before the call to port_driven
.
Finally, if called without arguments, port_driven
will activate the port
triggering on all input ports declared before it is called. This means that, in
task_context "Task" do
input_port 'image', '/Camera/Frame'
input_port 'parameters', '/SIFT/Parameters'
output_port 'features' '/SIFT/FeatureSet'
port_driven
end
both parameters
and image
are triggering. Now, in the following declaration, only image
will be:
task_context "Task" do
input_port 'image', '/Camera/Frame'
port_driven
input_port 'parameters', '/SIFT/Parameters'
output_port 'features' '/SIFT/FeatureSet'
end
FD-Driven Triggering (fd_driven
)
In the IO triggering scheme, updateHook
is called whenever new data is made
available on a file descriptor. It allows to very easily implement drivers,
that are waiting for new data on the driver communication line(s). The task
has its own thread.
Note if you're writing a task that has to interact with I/O, consider using Rock's iodrivers_base library and the corresponding orogen integration
To use the IO-driven mechanism, use the fd_driven
statement. fd-driven and
port-driven triggering can be combined.
task_context 'Task' do
...
fd_driven
end
To access more detailed information on the trigger reason, and to set up the
trigger mechanism, one must access the underlying activity. Two parts are
needed, one in configureHook
to tell the activity which file descriptors to watch
for, and one in cleanupHook
to remove all the watches (that last part is
mandatory)
First of all, include the header in the task's cpp file:
#include <rtt/extras/FileDescriptorActivity.hpp>
Second, set up the watches in configureHook
bool MyTask::configureHook()
{
// Here, "fd" is the file descriptor of the underlying device
// it is usually created in configureHook()
RTT::extras::FileDescriptorActivity* activity =
getActivity<RTT::extras::FileDescriptorActivity>();
// This is mandatory so that the task can be deployed
// with e.g. a port-driven or periodic activity
if (activity)
activity->watch(fd);
return true;
}
It is possible to list multiple file descriptors by having multiple calls to
watch()
.
One can set a timeout in milliseconds, in which case updateHook
with be
called after that many milliseconds after the last successful trigger.
activity->setTimeout(100);
Finally, you must clear all watches in stopHook():
void MyTask::cleanupHook()
{
RTT::extras::FileDescriptorActivity* activity =
getActivity<RTT::extras::FileDescriptorActivity>();
if (activity)
activity->clearAllWatches();
}
The FileDescriptorActivity class offers a few ways to get more information related to the trigger reason (data availability, timeout, error on a file descriptor). These different conditions can be tested with:
RTT::extras::FileDescriptorActivity* fd_activity =
getActivity<RTT::extras::FileDescriptorActivity>();
if (fd_activity)
{
if (fd_activity->hasError())
{
}
else if (fd_activity->hasTimeout())
{
}
else
{
// If there is more than one FD, discriminate. Otherwise,
// we don't need to use isUpdated
if (fd_activity->isUpdated(device_fd))
{
}
else if (fd_activity->isUpdated(another_fd))
{
}
}
}
Threading
When in an explicit deployment, one has the option to fine-tune the assignment of tasks to threads.
The first option is to associate a task with a thread. When there is a trigger, the thread is woken up and the task will be asynchronously executed when the OS scheduler decides to do so. It is the safest option (and the default) as the different tasks are made independent from each other.
The second option is to not associate the task with its own thread. Instead,
the thread that triggers it will be used to run the task. This is really only
useful for port-driven tasks: the task that wrote on the triggering port
will also execute the triggered task's updateHook
. The main advantage is that
the OS scheduler is removed from the equation, which can reduce latency. The
periodic and IO triggering mechanisms require the task to be in its own
thread.
When using a separate thread, the underlying thread can be parametrized with a scheduling class (realtime/non-realtime) and a priority. By default, a task is non-realtime and with the lowest priority possible. Changing it is done with the following statements:
task('TaskName', 'orogen_project::TaskClass')
.realtime
.priority(<priority value>)
Where the priority value is a number between 1 (lowest) and 99 (highest).
Note the dot at the end of the task
statement. This is a fluid
interface, don't forget that each modifier for the task definition is actually
a chain of method calls, and require the dots.
The second case is called a sequential activity and is declared with:
task('TaskName', 'orogen_project::TaskClass')
.sequential