Dynamic Services
- Concept
- Declaring a dynamic service
- Component Configuration
- Instanciating a dynamic service
- Dynamic Services on Compositions
In the Components reference section, we briefly breached the subject of how one could create ports dynamically on a component. This is rarely used, but is actually, nonetheless, very useful.
This page will expand on the subject from the perspective of Syskit: how, in Syskit, dynamic ports are modelled and handled. The core concept behind this integration is the dynamic service.
Best practice prefer instanciating more than one component to having dynamic ports and dynamic services when possible
Concept
Conceptually, dynamic ports are part of both a component's data flow interface and its configuration interface. Syskit being rather obviously very dataflow oriented, this "dual identity" is solved by providing a way to declare that ports will exist that are not part of the "main" component interface, along with some configuration data. It is up to the component's extension block to configure the component as required by the instanciated dynamic services.
For instance, the rock-gazebo integration has rock_gazebo::ModelTask
.
The component's exported_joints
property is a list of set of joints that should be
exported on an output port (and for which commands can be received on an input port).
To integrate this in Syskit, we define a dynamic service in the
rock_gazebo.rb extension file.
The block passed to dynamic_service
does some bit of argument normalization and
then defines the required service (a device driver is a type of data service, and
therefore valid)
Later, the component updates the exported_links
property accordingly within its own
#update_properties
method.
Declaring a dynamic service
Dynamic services are declared on the component model using the dynamic_service
method.
dynamic_service Services::ServiceModel, as: "dynamic_service_name" do
# Put here what to do to actually instanciate the service ... It usually is
# simply a call to provides
#
# The name of the instanciated service is accessible as `name`. Instanciation
# options (if any) are available as 'options' (a Hash)
#
# The name is omitted in the provides call. It is handled by the dynamic
# service instanciation logic.
provides Services::ServiceModel
end
The service model is the model of the data services that will be instanciated.
The dynamic_service_name
is arbitrary, used when the dynamic services are to
be instanciated.
Within the block, the only thing that is required is to actually call provides
with the required model (that is, either the model itself, or a service that is
declared as providing that model).
However, this provides
is different than the non-dynamic one. In the 'static' case,
provides
will map the ports of the service to the ports of the task, and fail if ports
do not exist.
In the 'dynamic' case, however, Syskit will interpret missing ports as ports
that will be created by the component at runtime (that's the "dynamic" part).
One will often make these ports unique, which is done by using the
instanciated port name - available as the name
method within the block. In the
example below, instanciating the object_position
service with a name of wall
will be interpreted by Syskit as creating a port called wall_position_samples
Dynamic services can also refer to static ports. For instance, the canbus::Task
component will create one output per client, but has a single input for all the
CAN-connected devices.
dynamic_service CommonModels::Services::Position, as: "object_position" do
provides CommonModels::Services::Position,
"position_samples" => "#{name}_position_samples"
end
Component Configuration
The configuration that matches the instanciated dynamic services must be generated
within the #update_properties
method within the component extension
file. One calls #each_required_dynamic_service
and update the configuration accordingly.
Syskit.extend_model Orogen.project.Task do
def update_properties
super
properties.exported_objects = each_required_dynamic_service.map do |srv|
# Options pass during instanciation are available as
# `srv.model.dynamic_service_options`
{ name: srv.name }
end
end
end
Instanciating a dynamic service
Let's reinforce something: instanciating a dynamic service actually updates the component model. It does not actually create ports. Port creation will happen at runtime, when the component is configured.
That "addition of new ports", however, allows to use the new ports in the component network(s) as if they existed in the original model.
Instanciating a dynamic service is done with with_dynamic_service
.
task_m_with_instanciated_service = task_m.with_dynamic_service(
"dynamic_service_name", as: "service_name", **options
)
Where "dynamic_service_name"
is the name of the dynamic service as provided to
dynamic_service
. service_name
is the name of the service once instanciated and
options
is passed as-is to the dynamic service block, and is available as
dynamic_service_options
during configuration.
Very important the method returns the component model with the newly added dynamic service, which might be different from the original. For instance,
task_m = OroGen.rock_gazebo.ModelTask.with_dynamic_service("link_export", as: "new_s")
# Here, task_m != OroGen.rock_gazebo.ModelTask
task_m = task_m.with_dynamic_service("link_export", as: "another_s")
# Here, task_m stayed the same
Dynamic Services on Compositions
Dynamic services can also be defined on composition, in which case
- there is (obviously) no
update_properties
step - the
dynamic_service
block is still expected to provide the service, which can e.g. be done by either overloading an existing child, or even adding a new child