In the system modelling, compositions are what bind components together: they define, for a specific task, behaviour or subsystem (depending from which side of robotics you come from), what components are needed and how these components should be connected together to perform the required function.
In the above description – as within the rest of this documentation – “component” refers to a data service, task context or composition.
Compositions are defined with
class ModelName < Syskit::Composition
...
end
Generation A template for a new composition as well as its related unit tests, following Syskit’s naming conventions and file system structure, can be generated with
syskit gen cmp namespace/name_of_composition
for instance
syskit gen cmp localization/global_pose_estimator
in the rock bundle would create the Rock::Compositions::Localization::GlobalPoseEstimator composition in models/compositions/localization/global_pose_estimator.rb and the associated test/compositions/localization/test_global_pose_estimator.rb file in the test folder. Files in the hierarchy (e.g. models/compositions/localization.rb and test/compositions/suite_localization.rb) are updated to require the new file(s).
Naming compositions are by convention defined in the BundleName::Compositions module, and are saved in models/compositions/name_of_composition.rb. For instance, a Localization::GlobalPoseEstimator composition in the Rock bundle would be saved in models/compositions/localization/global_pose_estimator.rb and the full class name would be Rock::Compositions::Localization::GlobalPoseEstimator. Any task context, composition or data service used in the composition file should be required first with either the using_task_library statement (for task contexts) or a simple require of the relevant file. Use syskit browse to find out what is defined where.
The main definition statements in a composition are ‘add’ and ‘connect’
class ModelName < Syskit::Composition
add Base::PoseSrv, :as => 'pose'
end
The “add” line adds a child to the composition, i.e. declares that a component of that type needs to run to perform the function of this composition. Children need to be given a name (which should reflect the child’s role). For instance, the rock_ugv_nav bundle defines the SLAM::Odometry composition with:
module SLAM
class Odometry < Syskit::Composition
add Services::ActuatorControlledSystem, :as => 'motors'
add Services::Orientation, :as => 'imu'
add OroGen::Skid4Odometry::Task, :as => 'odometry'
end
end
In this example, the composition would require three components:
- a component providing the Services::ActuatorControlledSystem service, which gives motor data
- a component providing the Services::Orientation service, which gives the orientation in space of the system whose odometry we’re computing
- finally, the task context that is going to do the computation (from the {rock_pkg: slam/orogen/odometry} package).
This is all well, but (obviously), these children need to be connected together. This is done with:
module SLAM
class Odometry < Syskit::Composition
add Services::ActuatorControlledSystemSrv, :as => 'motors'
add Services::OrientationSrv, :as => 'imu'
add OroGen::Skid4Odometry::Task, :as => 'odometry'
imu_child.connect_to odometry_child
motors_child.connect_to odometry_child
end
end
The “_child” statements refer to the defined children (i.e. imu_child for the imu, odometry_child for the odometry, …). In the form above, the connect_to statement will try to create all connections that are possible and unambiguous. Given an output port, a possible connection is ambiguous if (1) more than one input port has a matching data type and (2) none of these input ports have the same name than the output. If no connection is found, an error is generated.
When ambiguous connections exist, one should give the required connection explicitly by selecting the input port, the output port or both with e.g.
imu_child.orientation_samples_port.connect_to odometry_child
imu_child.connect_to odometry_child.orientation_samples_port
imu_child.orientation_samples_port.connect_to
odometry_child.orientation_samples_port
Just as for the child, the ports are referred to as (port_name)_port, i.e. orientation_samples_port is a port named “orientation_samples”.
These models can be checked graphically using the Syskit tool. If you have not yet installed the rock_ugv_nav bundle, do it by calling
aup rock_ugv_nav
then go in bundles/rock_ugv_nav and run
syskit browse
On the left side, select the Rock::Compositions::SLAM::Odometry model. The right pane will show you the following graphical representation of the composition:
TODO: add figure.
Providing Services
In order to be able to use compositions in lieu of services, for instance so that we can use them in other compositions, we have to give them ports.
Since compositions are only aggregations of other components, they don’t have ports on themselves. They “get” ports by exporting ports from their children (one can export both input and output ports this way):
module SLAM
class Odometry < Syskit::Composition
...
export odometry_child.odometry_delta_samples_port
provides Services::RelativePose
end
end
As you can see in the example above, the export of a port allows you to then use the provides statement to declare that the composition provides a service and therefore be able to use it whenever said service is needed.
Configuration
In the same way than compositions do not really have ports, but only “export” the ports of its children, compositions do not have configurations by themselves. Instead, configurations are defined as a list of configurations for the children.
For instance:
module SLAM
class Odometry < Syskit::Composition
...
conf 'high_speed',
'pose_provider' => ['default', 'high_sampling_rate']
end
end
Unlike what is possible with task contexts, one cannot “merge” configurations on top of each other (i.e. only one configuration can be selected at a time). Moreover, the configuration names are not verified at declaration time (in the example above, ‘pose_provider’ might be a service and therefore has no notion of configuration files).
Dependency specification: tasks and behaviours (advanced)
When one adds a child to a composition, he also declares that, at runtime, the composition will need an instance of this child to run. Having a working odometry in a system requires, for instance, a properly configured instance of OroGen::Skid4Odometry::Task to run. The default dependency setup therefore defines that there is a dependency constraint failure if the child task stops while the composition is running. In other words, Roby’s dependency relation is used with:
composition.depends_on(child, :failed => [:stop])
The :as option on the #add line is used as the dependency role. It means that, at runtime, one can access a particular child of the composition by its name:
composition.odometry_child
This can be changed at definition time by giving additional parameters to the add statement. Such a special case is used to change the composition from a behaviour (runs until not needed) to a goal-oriented task (runs until it achieved a specified goal). A (hypothetical) Goto, which would use a TrajectoryFollower::Task to reach a certain goal using a predefined trajectory could be defined with:
class Goto < Syskit::Composition
add OroGen::TrajectoryFollower::Task,
:success => [:reached_the_end],
:as => 'follower'
end
where reached_the_end is one of the runtime states reported by the OroGen::TrajectoryFollower::Task component
Extending the Composition Models
Composition models are subclasses of Syskit::Composition which is itself a grandchild of Roby::Task. As such, much more can be done using the runtime code execution features of Roby.
For instance, due to the limitations of the composition definition implementation, one cannot currently use the complete range of event constraints in the composition definition itself. This has to be done at instanciation time by overriding Composition.instanciate:
class Goto < Syskit::Composition
def self.instanciate(plan, *args)
composition = super
composition.follower_child.reached_the_end_event.
forward_to composition.success_event
composition
end
end
See Roby’s own documentation for more information. In general, one would extend compositions with new events, scripts or poll blocks directly in the block given to composition. For instance:
class Localization < Syskit::Composition
event :lost
forward :lost => :failed
end