YAML application syntax
The following sections define the YAML syntax used to describe an AICA application.
Overview
An application description may contain some or all of the following top-level fields.
on_start:
...
components:
...
hardware:
...
conditions:
...
sequences:
...
buttons:
...
The fields components
and hardware
define the main building blocks of the application.
The fields conditions
and sequences
define fine-grained application logic to
trigger application state events. The on_start
field defines a list of events to be triggered
when the application is started, while the buttons
field defines interactive buttons to manually trigger
events through AICA Studio.
Events
Events drive the emergent behaviour of an application. Events can be triggered from internal application logic through component predicates, conditions, sequences, UI buttons or automatically at the start of the application. In each of these cases, events are defined in the YAML under specific event keywords.
Read more about events in the Concepts guide.
Load or unload a component
Components can be loaded or unloaded by component name.
load:
component: <component_name>
unload:
component: <component_name>
It is possible to load or unload multiple components simultaneously by specifying a list of components.
load:
- component: component_a
- component: component_b
Transition from one component to another
Component A can invoke a transition to component B as a shorthand for "unload component A, load component B".
transition: <component_name>
Trigger a lifecycle transition
Request a lifecycle transition on the component that is triggering the event, using one of the available transitions
(configure
, activate
, deactivate
, cleanup
, or shutdown
).
lifecycle: activate
Request a lifecycle transition on a different component.
lifecycle:
transition: activate
component: <component_name>
Use a list to trigger multiple transitions from a single predicate.
lifecycle:
- transition: activate
component: <component_name>
- transition: deactivate
component: <component_name>
Set a parameter
Set a parameter on the component that is triggering the event.
set:
parameter: <parameter_name>
value: <parameter_value>
Set a parameter on a different component.
set:
parameter: <parameter_name>
value: <parameter_value>
component: <component_name>
Set a parameter on the controller of a particular hardware interface.
set:
parameter: <parameter_name>
value: <parameter_value>
controller: <controller_name>
hardware: <hardware_name>
Call a service
Call a service with no payload on the component that is triggering the event.
call_service: <service_name>
Call a service on a different component.
call_service:
service: <service_name>
component: <component_name>
Call a service on a controller.
call_service:
service: <service_name>
controller: <controller_name>
hardware: <hardware>
Call a service with a string payload.
call_service:
service: <service_name>
component: <component_name>
payload: "..."
The service payload can also be written as any standard YAML object. The application parser will automatically encode the object into a string format when making the service call. In this case, the component service is responsible for parsing the string back into a YAML object, dict or structure as necessary.
call_service:
service: <service_name>
component: <component_name>
payload:
foo: some content
bar: [ x, y, z ]
baz:
a: 1
b: 2
Load or unload a hardware interface
Load and initialize a hardware interface.
load:
hardware: <hardware_name>
Unload and destroy a hardware interface.
unload:
hardware: <hardware_name>
Load or unload a controller
load:
hardware: <hardware_name>
controller: <controller_name>
unload:
hardware: <hardware_name>
controller: <controller_name>
Use a list to load or unload multiple controllers from a single predicate.
load:
- hardware: <hardware_name>
controller: controller_a
- hardware: <hardware_name>
controller: controller_b
Activate or deactivate a controller
Use the switch_controllers
event to list the controllers to be activated or deactivated for a specific hardware
interface.
switch_controllers:
hardware: <hardware_name>
activate: [ <controller_one>, <controller_two> ]
deactivate: [ <controller_three>, <controller_four> ]
To activate or deactivate a single controller, the controller name can be given directly instead of using a list.
switch_controllers:
hardware: <hardware_name>
activate: <controller_name>
A controller must be loaded before it can be activated, and must be deactivated before it can be unloaded.
Manage sequences
Use the sequence
event to either start, restart or abort a named sequence in the application
description. Use the respective start
, restart
or abort
fields either individually or collectively.
sequence:
start: <sequence_name>
restart: <sequence_name>
abort: <sequence_name>
To manage multiple sequences with the same event trigger, use a list syntax.
sequence:
- start: sequence_a
- start: sequence_b
Components
Components are listed under a top-level field called components
. Component names must be unique, and should
generally follow the lower_camel_case
naming convention.
components:
component_a:
component: ... # required
display_name: ... # optional
position: ... # optional
log_level: ... # optional
mapping: ... # optional
parameters: ... # optional
inputs: ... # optional
outputs: ... # optional
events: ... # optional
component_b:
...
Each component is defined with a number of fields, as shown below. The fields are defined in the next section.
Component
The component
field defines the actual component implementation to use for the component.
It takes a fully qualified class name as registered by the RCLCPP_COMPONENTS_REGISTER_NODE
macro.
The registered class name of a component should include the package name within the namespace. For example, the
registration foo_components::Foo
refers to a component Foo
in package foo_components
.
my_component:
component: foo_components::Foo
Display name
The optional display_name
field can be used to give the component a more human-readable name (one that does not have
to conform to the lower_snake_case
naming convention of the YAML syntax). It is only used when rendering the component
as a node in the AICA interactive graph editor. If omitted, the name is taken directly from the YAML field (from the
previous example, it would default to my_component
).
Position
The position
field is used to define the desired location of the button in the application graph. It has two subfields
defining the X and Y location, respectively.
my_component:
position:
x: 100
y: 200
Log level
The log_level
optionally sets the log severity level for this component.
Supported levels are: [unset, debug, info, warn, error, fatal]
my_component:
log_level: debug
Mapping
The mapping
field optionally defines overrides for the component name and namespace. Normally, the component node
is instantiated with the same name as the top level component name and put on the base namespace.
By specifying a mapping name
or namespace
or both, the instantiated node name is updated accordingly.
# Without the mapping directive, the node name becomes /component_a
component_a:
...
# With the mapping directive, the node name becomes /my_component_namespace/my_new_component_name
component_b:
mapping:
name: my_new_component_name
namespace: my_component_namespace
Parameters
The parameters
field allows initial component parameters values to be set using a name: value
syntax. These values
are only applied when the component is loaded. To set parameter values after a component has been loaded, use the
set parameter event.
my_component:
parameters:
my_string_parameter: my string value
my_double_parameter: 2.0
Component rate
The rate
parameter is a special reserved parameter that defines the step rate of a component in Hertz, which is
the inverse of the execution period.
For example, if an image processing component should run some computation at 20 frames per second, then the rate parameter should be set to 20 Hertz.
my_component:
parameters:
rate: 20
Inputs and outputs
The inputs
and outputs
fields are used to connect component signals together to enable communication, signal
processing and control loops. Each signal is specified using a name: value
syntax, where the name is the name
of the signal according to the component description, and the value is and the name of the signal topic.
If a component output is assigned to the same topic name as another component input, they are connected, as
illustrated in the example below.
my_component:
inputs:
robot_state: /state
applied_force: /force
outputs:
robot_command: /command
my_other_component:
outputs:
force_torque_sensor: /force
Predicate events
Component predicates can be used to trigger events by adding the named predicate and corresponding events
under the events
field of a component definition. For example:
my_component:
events:
is_active:
load: ...
unload: ...
some_other_predicate_name:
set: ...
call_service: ...
Special event predicates
In addition to standard component predicates produced by the component at runtime, two other event triggers can be associated with a component. These triggers are provided by the Dynamic State Engine which manages the component rather than the component itself.
on_load
The on_load
predicate is provided by the state engine and set to true after the component has been loaded. Any events
associated with the on_load
predicate are handled after the node has been instantiated.
component:
events:
on_load:
<some triggered event>: ...
on_unload
The on_unload
predicate is similar to the on_load
predicate and is provided by the state engine. Any events
associated with the on_unload
predicate are handled once the component interface has been destroyed.
component:
events:
on_unload:
<some triggered event>: ...
Hardware
Hardware interfaces describe the connected robots and their corresponding controllers.
hardware:
robot_a:
urdf: ...
rate: ...
parameters: # optional
...
display_name: ... # optional
position: ... # optional
controllers:
...
robot_b:
...
URDF
The urdf
field refers to a specially formatted robot description file which defines the joint configurations and the
hardware interface driver needed to communicate with the robot.
A hardware interface can be linked to URDF file in one of the following ways:
- By name of the custom URDF uploaded to the AICA database
- By name of an example URDF included in the AICA image (available examples depend on license and distribution versions)
- By the path of a URDF file mounted in the container filesystem
- By URDF string content inserted directly in the YAML (not recommended for large files)
# referring to a custom robot description uploaded to the user database
robot_a:
urdf: My custom robot
# referring to a built-in robot description from the included examples
robot_b:
urdf: Universal Robots 5e (default configuration)
# using the path to a URDF file mounted in the container filesystem
robot_c:
urdf: /home/ros2/my_robot.urdf
# defining the URDF content in-line
robot_d:
urdf: |
<robot name="example">
<ros2_control name="ExampleRobotHardwareInterface" type="system">
<hardware>
<plugin>robot_interface/GenericInterface</plugin>
</hardware>
...
</ros2_control>
...
</robot>
Use the Hardware tab in AICA Studio to manage available URDFs.
Alternatively, use the API endpoints at /v1/data/hardware
and /v1/examples/hardware
to manage custom hardware and
view the available built-in example URDFs, respectively.
Rate
The rate
field defines the robot control frequency in Hz.
Parameters
The parameters
field is used to set hardware-specific parameter values which override the default values from the
associated URDF.
Specifically, the URDF is expected to include a <ros2_control>
tag under which hardware properties are defined,
including the hardware plugin and any number of parameters specific to that plugin.
For example, a robot_interface/GenericInterface
plugin may accept a robot_ip
parameter to specify the IP address:
<robot name="example">
<ros2_control name="ExampleRobotHardwareInterface" type="system">
<hardware>
<plugin>robot_interface/GenericInterface</plugin>
<param name="robot_ip">192.168.0.1</param>
</hardware>
...
</ros2_control>
...
</robot>
By adding robot_ip
under the parameters
field, the default IP address can be overridden when the hardware interface
is loaded:
my_robot:
urdf: Example Robot
parameters:
robot_ip: 172.16.0.1
In this example, the robot interface would be loaded with the IP address of 172.16.0.1
instead of the default
192.168.0.1
as specified in the URDF. This allows parameters to be selectively altered at deploy time directly in the
application description without needing to modify the URDF itself.
Hardware parameter values are only applied if the parameter name matches an existing hardware parameter in the URDF. If the parameter does not exist in the URDF, it will not be added.
Display name
This optional field is identical to the component display name and is used to assign a nicer, human-readable display name to the hardware interface when rendered as a node in the AICA interactive graph editor.
Position
This optional field is identical to the component position and is used to provide an X, Y position for the hardware interface when rendered as a node in the AICA interactive graph editor.
This field only affects visualization of the application graph and has no other run-time effect. If a position is not specified, the node will be rendered at a procedurally chosen location.
Controllers
Controllers are the interface between components in the application and hardware in the real world. They convert desired reference signals into real joint commands according to some internal control law, and convert joint states from the robot back to signals.
Controllers are listed under a top-level controllers
field. Controller names must be unique within the given hardware
interface, and should generally follow the lower_camel_case
naming convention.
Under each controller, the plugin
field refers to a registered controller plugin name.
The parameters
field then refers to configurable parameters for the given controller.
The inputs
and outputs
fields define the ROS2 topics to which each signal of the controller should be connected.
See also Component Inputs and Outputs.
Predicates can be used to trigger events by adding the named predicate and corresponding events
under the events
field of a controller definition. See also Predicate Events.
Optionally, the position
field can be used to specify an X, Y location for rendering the hardware interface
as a node in the AICA interactive graph editor. See also Component Position.
For example:
robot:
controllers:
broadcaster:
plugin: joint_state_broadcaster/JointStateBroadcaster
twist_controller:
plugin: compliant_twist_controller/CompliantTwistController
parameters:
linear_principle_damping: 10.0
linear_orthogonal_damping: 10.0
angular_stiffness: 1.0
angular_damping: { a: 1.0, b: true }
inputs:
command: /motion_generator/command_output
outputs:
state: /recorder/state_input
events:
my_predicate_name:
set: ...
call_service: ...
Conditions
Conditions are event triggers based on logical combinations of predicates.
Conditions are listed under a top-level field called conditions
. Condition names must be unique, and should
generally follow the lower_camel_case
naming convention.
Conditional events are triggered only on the rising edge of the condition, preventing the repeated execution of an event if the condition stays true.
Define events to be triggered by a condition by listing them under the condition name. See the events section for available event syntax.
conditions:
condition_1:
component: ...
predicate: ...
events:
...
condition_2:
controller: ...
hardware: ...
predicate: ...
events:
...
condition_3:
<conditional_operator>: ... # not, all, any, one_of
events:
...
Simple conditions
A simple condition evaluates just a single component or controller predicate and triggers the listed events when it is true.
condition_1:
component: my_component
predicate: some_component_predicate
events:
...
condition_2:
controller: my_controller
hardware: my_hardware
predicate: some_controller_predicate
events:
...
Conditional operators
To combine multiple predicates together into a single true / false condition, the following operators can be used.
The operators can refer to one or more component predicates with the syntax
{ component: component_a, predicate: some_predicate }
Not
The not
operator takes a single item and negates its value. It is true when the item is false, and false when the
item is true.
condition_1:
not: { component: component_a, predicate: some_predicate }
events:
...
All
The all
operator takes a list of items and is true only when every listed item is true.
condition_1:
all:
- { component: component_a, predicate: some_predicate }
- { component: component_b, predicate: some_predicate }
events:
...
Any
The any
operator takes a list of items and is true when at least one of the listed items is true.
condition_1:
any:
- { component: component_a, predicate: some_predicate }
- { component: component_b, predicate: some_predicate }
events:
...
One of
The one_of
operator takes a list of items and is true only when exactly one of the listed items is true.
condition_1:
one_of:
- { component: component_a, predicate: some_predicate }
- { component: component_b, predicate: some_predicate }
events:
...
Nested conditions
The conditional operators can be applied recursively for more complex conditions. The following example could be
collapsed into the equivalent logical pseudocode: NOT(a AND b AND (c OR d OR (e XOR f)))
conditions:
nested_condition:
not:
all:
- { component: component_1, predicate: a }
- { component: component_2, predicate: b }
- any:
- { component: component_3, predicate: c }
- { component: component_4, predicate: d }
- one_of:
- { component: component_5, predicate: e }
- { component: component_6, predicate: f }
Sequences
A sequence is a list of steps that are handled sequentially in order. Sequence steps are either standard state events or conditional blocks; the conditional steps are used either to wait for a condition, predicate or fixed time interval, or to assert the current value of a condition or predicate.
Similar to conditions, sequences are listed under a top-level field called sequences
. Sequence names
must be unique, and should generally follow the lower_camel_case
naming convention.
After sequences are defined in the yaml, they can be managed using sequence state events; component or controller predicates, conditions and even sequences can also start, restart or abort a sequence.
The example below uses a combination of standard event steps and conditional blocks; it asserts that a component is active, sets a parameter on that component, waits 10 seconds, and then activates a controller.
sequences:
sequence_1:
- assert:
component: my_component
predicate: is_active
- set:
component: my_component
parameter: speed
value: 2.0
- wait:
seconds: 10
- switch_controllers:
hardware: my_hardware
activate: my_controller
Sequence assert
The assert
keyword is an assertion step to check if a condition or predicate is true. If the assertion succeeds,
the sequence continues to the next step. If the assertion fails, the sequence is automatically aborted.
Optionally, assertion failure can be used to trigger breakout events as a form of error handling.
The following examples show the syntax to check either a condition or predicate respectively.
assert:
condition: my_condition
assert:
component: my_component
predicate: some_predicate
assert:
controller: my_controller
hardware: my_hardware
predicate: some_predicate
The else
keyword is optionally used to define breakout events if the assertion fails. The following example would
unload a component if condition my_condition
is not true and then abort the sequence.
assert:
condition: my_condition
else:
unload:
component: my_component
Sequence wait
The wait
keyword is used to wait for either a fixed time interval or for a condition or predicate to be
true.
Waiting for a specified time interval
The simplest case is waiting for fixed duration, which uses the seconds
field to define the time to wait in seconds.
wait:
seconds: 10
Waiting for a condition or predicate
The following examples show the syntax to wait for either a condition or predicate state respectively.
wait:
condition: my_condition
wait:
component: my_component
predicate: some_predicate
wait:
controller: my_controller
hardware: my_hardware
predicate: some_predicate
Compared to the simple fixed-time wait, a conditional wait step could block the sequence indefinitely. The timeout
field can be used when waiting for a condition or predicate to set a maximum time limit. The time limit is defined in
seconds with the seconds
field.
Similar to assertions, the sequence is aborted if the wait step times out. The optional events
keyword can be used
under the timeout
field to define breakout events if the assertion fails. The following example would unload a
component if condition my_condition
is not true within 10 seconds and then abort the sequence.
wait:
condition: my_condition
timeout:
seconds: 10
events:
unload:
component: my_component
Using sequences to manage program flow
Because sequences can also use sequence state events as steps, fine-grained looping and branching logic can be expressed.
For example, this sequence would activate and deactivate a lifecycle component every 5 seconds in an endless loop:
sequences:
my_sequence:
- lifecycle:
component: my_component
transition: activate
- wait:
seconds: 5
- lifecycle:
component: my_component
transition: deactivate
- wait:
seconds: 5
- sequence:
restart: my_sequence
The next example starts sequence_2
if a condition is true, and else starts sequence_3
.
sequences:
sequence_1:
- wait:
seconds: 5
- assert:
condition: my_condition
else:
sequence:
start: sequence_3
- sequence:
start: sequence_2
On Start
The on_start
keyword is reserved as a special event trigger when the application is launched.
List the events to trigger on startup (for example, to load components and hardware interfaces).
on_start:
load:
- component: component_a
- component: component_b
- hardware: robot_a
Buttons
Buttons are interactive elements in AICA Studio. They are used to manually trigger state events when an application is running by clicking the trigger button in the application graph. Buttons have no effect on the application if the UI is not used.
buttons:
my_button:
position: ...
on_click:
...
Position
This optional field is identical to the component position and is used to provide an X, Y position for the component when rendered as a node in the AICA interactive graph editor.
On Click
List the events to trigger when the button is clicked while the application is running (for example, to unload a component).
my_button:
on_click:
unload:
component: component_a
Validating a YAML application
The YAML application schema defines the structural rules of an AICA application and effectively distinguishes between valid and invalid syntax.
Many modern IDEs and code editors can be configured to support custom schemas and provide in-line validation and completion of the YAML content.
Validating a YAML application with Visual Studio Code
Developers working with Visual Studio Code can validate YAML application files easily in two steps:
- Install the YAML extenstion from RedHat
- Associate a schema with the YAML application by adding the following modeline to the file:
# yaml-language-server: $schema=https://docs.aica.tech/schemas/1-4-1/application.schema.json