Write ROS 2 Launch files using Python, XML, and YAML

Directory of series articles

ROS2 important concepts
ament_cmake_python user documentation
ROS2 ament_cmake user documentation
Managing ROS 2 dependencies using rosdep

Article directory

  • Table of Contents of Series Articles
  • Preface
  • 1. Launch file example
    • 1.1 Python version
    • 1.2 XML version
    • 1.3 YAML version
  • 2. Use the Launch file from the command line
    • 1. Launching
    • 2. Set parameters
    • 3. Control turtles
  • 3. Python, XML or YAML: Which language should I use?
  • 4. Why use ROS 2 Launch

Foreword

ROS 2 launch files can be written in Python, XML, and YAML. This guide explains how to use these different formats to accomplish the same task and discusses when to use each format.

1. Launch file example

Below is a Launch file implemented in Python, XML and YAML. Each Launch file does the following:

  • Set command line parameters with default values

  • Contains another launch file

  • Include another startup file in another namespace

  • Start the node and set its namespace

  • Start a node, set its namespace, and set parameters in the node (using parameters)

  • Create a node to remap messages from one topic to another

1.1 Python version

First, we will introduce the several classes and methods involved.

  1. ament_index_python.get_package_share_path(package_name, print_warning=True)
Returns the shared directory of the given package as pathlib.Path.
For example, if you install package "foo" to "/home/user/ros2_ws/install" and call this function with "foo" as argument, then it will return a value representing "/home /user/ros2_ws/install/share/foo", then you can use it to build the path to the shared file, i.e. get_package_share_path('foo') /'urdf/robot.urdf'
  1. launch.LaunchDescription
Basics: LaunchDescriptionEntity
A description of the bootable system.
The description consists of a sequence of entities that represent the system designer's intent.
The description may also have parameters, declared by the launch.actions.DeclareLaunchArgument action in the launch description.
The description's arguments are accessible through the get_launch_arguments() method. Parameters are gathered by searching for entities in this launch description and the description of each entity (which may include entities resulting from these entities).
  1. launch.actions.declare_launch_argument.DeclareLaunchArgument(Action)
Basics: Action
Action declaring new startup parameters.
Startup parameters are stored in the "startup configuration" with the same name. See launch.actions.SetLaunchConfiguration and launch.substitutions.LaunchConfiguration.
Any launch parameters declared in launch.LaunchDescription will be displayed as parameters when including that launch description, for example, as additional parameters in the launch.actions.IncludeLaunchDescription action, or as a command line when launching with ros2 launch.... parameter.
In addition to a name (which is also where the parameter results are stored), startup parameters may have a default value, a selection list of valid values, and a description. If a default value is given, the parameter becomes optional and the default value will be placed in the launch configuration. If no default value is given and no value is given when the startup instructions are included, an error occurs. If a selection list is given and the given value is not in it, an error occurs.
Substitutions can be used for the default value, but the name and description can only be Text since they require a meaningful value before starting, such as when listing command line arguments.
It should be noted that the declaration of startup parameters must be in a certain part of the startup description, and this part can be described without startup. For example, if you declare a launch parameter in a conditional group or as a callback to an event handler, tools like ros2 launch may not know that parameter before launching the description. In this case, the parameter will not be visible on the command line, but an exception may be thrown if the parameter does not meet the requirements (and has no default value) after accessing it.
In other words, the postcondition accessing the operation either has a value set by the launch configuration with the same name, or an exception is thrown because no value is set and there is no default value. However, preconditions do not guarantee that parameters following the condition or case inclusion will be visible.
For example;
ld = LaunchDescription([
    DeclareLaunchArgument('simple_argument'),
    DeclareLaunchArgument('with_default_value', default_value='default'),
    DeclareLaunchArgument(
        'with_default_and_description',
        default_value='some_default',
        description='this argument is used to configure ...'),
    DeclareLaunchArgument(
        'mode',
        default_value='A',
        description='Choose between mode A and mode B',
        choices=['A', 'B']),
    # other actions here, ...
])
  1. launch.actions.GroupAction
Basics: Action
Actions that can produce other operations.
This action is used to nest other actions without including separate launch instructions, while also optionally having a condition (like all other actions), extending and forwarding launch configuration and environment variables, and/or just the group and its spawn The operation declares the startup configuration.
When Scoped=True, changes to the launch configuration and environment variables are limited to the scope of the operation within the group operation.
When scoped=True and forwarding=True , all existing launch configuration and environment variables are available in the scoped context.
When scope=True and forwarding=False, all existing launch configuration and environment variables are removed from the scope context.
Any launch configurations defined in the launch_configurations dictionary will be set in the current context. When scoped=False, these configurations will persist even after the GroupAction has completed. When scoped=True, these configurations will only be available to actions within the GroupAction. When scope=True and forwarding=False, the launch_configurations dictionary will be evaluated before clearing and then reset in the cleared scope context.
  1. launch.actions.IncludeLaunchDescription
Basics: Action
Contains actions that start a description source and generate its entities when accessed.
You can pass parameters to the launch description, which are declared through the launch.actions.DeclareLaunchArgument action.
If the given parameter does not match a declared launch parameter name, it will still be set as the launch configuration using the launch.actions.SetLaunchConfiguration action. The reason for this is that it is not always possible to detect all instances of a declared launch parameter class in a given launch description.
On the other hand, if a given launch description declares a launch parameter, but does not provide its value to this operation, an error will sometimes be thrown. However, this error will only occur if the declared startup parameter is unconditional (sometimes the action that declares the startup parameter will only be accessed under specific circumstances) and there is no default value to choose from.
Conditionally included startup parameters without default values will eventually cause an error if unsatisfied parameters cannot be discovered in advance despite best-effort parameter checking.
  1. launch.launch_description_sources.PythonLaunchDescriptionSource(LaunchDescriptionSource)
Basics: LaunchDescriptionSource
A wrapper for Python startup files that can be loaded during the startup process.
  1. launch.substitutions.LaunchConfiguration(Substitution)
Substitute variables for accessing startup configuration variables.
  1. launch.substitutions.TextSubstitution(Substitution)
Can replace a single string text.
  1. PushROSNamespace(Action)
Action to push the ros namespace.
It will automatically pop up when used within a scoped `GroupAction`. There is no other way to pop it.
  1. XMLLaunchDescriptionSource(FrontendLaunchDescriptionSource)
Encapsulates the XML startup file, which can be loaded during the startup process.
  1. YAMLLaunchDescriptionSource(FrontendLaunchDescriptionSource)
Encapsulates the YAML startup file, which can be loaded during the startup process.
  1. Node(ExecuteProcess)
Execute the operation of a ROS node.

The Python code is as follows

# example_launch.py

import os

from ament_index_python import get_package_share_directory

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.actions import GroupAction
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
from launch.substitutions import TextSubstitution
from launch_ros.actions import Node
from launch_ros.actions import PushRosNamespace
from launch_xml.launch_description_sources import XMLLaunchDescriptionSource
from launch_yaml.launch_description_sources import YAMLLaunchDescriptionSource


def generate_launch_description():

    # args that can be set from the command line or a default will be used
    background_r_launch_arg = DeclareLaunchArgument(
        "background_r", default_value=TextSubstitution(text="0")
    )
    background_g_launch_arg = DeclareLaunchArgument(
        "background_g", default_value=TextSubstitution(text="255")
    )
    background_b_launch_arg = DeclareLaunchArgument(
        "background_b", default_value=TextSubstitution(text="0")
    )
    chatter_py_ns_launch_arg = DeclareLaunchArgument(
        "chatter_py_ns", default_value=TextSubstitution(text="chatter/py/ns")
    )
    chatter_xml_ns_launch_arg = DeclareLaunchArgument(
        "chatter_xml_ns", default_value=TextSubstitution(text="chatter/xml/ns")
    )
    chatter_yaml_ns_launch_arg = DeclareLaunchArgument(
        "chatter_yaml_ns", default_value=TextSubstitution(text="chatter/yaml/ns")
    )

    # include another launch file
    launch_include = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            os.path.join(
                get_package_share_directory('demo_nodes_cpp'),
                'launch/topics/talker_listener_launch.py'))
    )
    # include a Python launch file in the chatter_py_ns namespace
    launch_py_include_with_namespace = GroupAction(
        actions=[
            # push_ros_namespace to set namespace of included nodes
            PushRosNamespace('chatter_py_ns'),
            IncludeLaunchDescription(
                PythonLaunchDescriptionSource(
                    os.path.join(
                        get_package_share_directory('demo_nodes_cpp'),
                        'launch/topics/talker_listener_launch.py'))
            ),
        ]
    )

    # include a xml launch file in the chatter_xml_ns namespace
    launch_xml_include_with_namespace = GroupAction(
        actions=[
            # push_ros_namespace to set namespace of included nodes
            PushRosNamespace('chatter_xml_ns'),
            IncludeLaunchDescription(
                XMLLaunchDescriptionSource(
                    os.path.join(
                        get_package_share_directory('demo_nodes_cpp'),
                        'launch/topics/talker_listener_launch.xml'))
            ),
        ]
    )

    # include a yaml launch file in the chatter_yaml_ns namespace
    launch_yaml_include_with_namespace = GroupAction(
        actions=[
            # push_ros_namespace to set namespace of included nodes
            PushRosNamespace('chatter_yaml_ns'),
            IncludeLaunchDescription(
                YAMLLaunchDescriptionSource(
                    os.path.join(
                        get_package_share_directory('demo_nodes_cpp'),
                        'launch/topics/talker_listener_launch.yaml'))
            ),
        ]
    )

    # start a turtlesim_node in the turtlesim1 namespace
    turtlesim_node = Node(
        package='turtlesim',
        namespace='turtlesim1',
        executable='turtlesim_node',
        name='sim'
    )

    # start another turtlesim_node in the turtlesim2 namespace
    # and use args to set parameters
    turtlesim_node_with_parameters = Node(
        package='turtlesim',
        namespace='turtlesim2',
        executable='turtlesim_node',
        name='sim',
        parameters=[{<!-- -->
            "background_r": LaunchConfiguration('background_r'),
            "background_g": LaunchConfiguration('background_g'),
            "background_b": LaunchConfiguration('background_b'),
        }]
    )

    # perform remap so both turtles listen to the same command topic
    forward_turtlesim_commands_to_second_turtlesim_node = Node(
        package='turtlesim',
        executable='mimic',
        name='mimic',
        remappings=[
            ('/input/pose', '/turtlesim1/turtle1/pose'),
            ('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
        ]
    )

    return LaunchDescription([
        background_r_launch_arg,
        background_g_launch_arg,
        background_b_launch_arg,
        chatter_py_ns_launch_arg,
        chatter_xml_ns_launch_arg,
        chatter_yaml_ns_launch_arg,
        launch_include,
        launch_py_include_with_namespace,
        launch_xml_include_with_namespace,
        launch_yaml_include_with_namespace,
        turtlesim_node,
        turtlesim_node_with_parameters,
        forward_turtlesim_commands_to_second_turtlesim_node,
    ])

1.2 XML version

<!-- example_launch.xml -->

<launch>

    <!-- args that can be set from the command line or a default will be used -->
    <arg name="background_r" default="0" />
    <arg name="background_g" default="255" />
    <arg name="background_b" default="0" />
    <arg name="chatter_py_ns" default="chatter/py/ns" />
    <arg name="chatter_xml_ns" default="chatter/xml/ns" />
    <arg name="chatter_yaml_ns" default="chatter/yaml/ns" />

    <!-- include another launch file -->
    <include file="$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.py" />
    <!-- include a Python launch file in the chatter_py_ns namespace-->
    <group>
        <!-- push_ros_namespace to set namespace of included nodes -->
        <push_ros_namespace namespace="$(var chatter_py_ns)" />
        <include file="$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.py" />
    </group>
    <!-- include a xml launch file in the chatter_xml_ns namespace-->
    <group>
        <!-- push_ros_namespace to set namespace of included nodes -->
        <push_ros_namespace namespace="$(var chatter_xml_ns)" />
        <include file="$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.xml" />
    </group>
    <!-- include a yaml launch file in the chatter_yaml_ns namespace-->
    <group>
        <!-- push_ros_namespace to set namespace of included nodes -->
        <push_ros_namespace namespace="$(var chatter_yaml_ns)" />
        <include file="$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.yaml" />
    </group>

    <!-- start a turtlesim_node in the turtlesim1 namespace -->
    <node pkg="turtlesim" exec="turtlesim_node" name="sim" namespace="turtlesim1" />
    <!-- start another turtlesim_node in the turtlesim2 namespace
        and use args to set parameters -->
    <node pkg="turtlesim" exec="turtlesim_node" name="sim" namespace="turtlesim2">
        <param name="background_r" value="$(var background_r)" />
        <param name="background_g" value="$(var background_g)" />
        <param name="background_b" value="$(var background_b)" />
    </node>
    <!-- perform remap so both turtles listen to the same command topic -->
    <node pkg="turtlesim" exec="mimic" name="mimic">
        <remap from="/input/pose" to="/turtlesim1/turtle1/pose" />
        <remap from="/output/cmd_vel" to="/turtlesim2/turtle1/cmd_vel" />
    </node>
</launch>

1.3 YAML version

# example_launch.yaml

launch:

# args that can be set from the command line or a default will be used
- arg:
    name: "background_r"
    default: "0"
- arg:
    name: "background_g"
    default: "255"
- arg:
    name: "background_b"
    default: "0"
- arg:
    name: "chatter_py_ns"
    default: "chatter/py/ns"
- arg:
    name: "chatter_xml_ns"
    default: "chatter/xml/ns"
- arg:
    name: "chatter_yaml_ns"
    default: "chatter/yaml/ns"


# include another launch file
-include:
    file: "$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.py"

# include a Python launch file in the chatter_py_ns namespace
- group:
    - push_ros_namespace:
        namespace: "$(var chatter_py_ns)"
    -include:
        file: "$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.py"

# include a xml launch file in the chatter_xml_ns namespace
- group:
    - push_ros_namespace:
        namespace: "$(var chatter_xml_ns)"
    -include:
        file: "$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.xml"

# include a yaml launch file in the chatter_yaml_ns namespace
- group:
    - push_ros_namespace:
        namespace: "$(var chatter_yaml_ns)"
    -include:
        file: "$(find-pkg-share demo_nodes_cpp)/launch/topics/talker_listener_launch.yaml"

# start a turtlesim_node in the turtlesim1 namespace
-node:
    pkg: "turtlesim"
    exec: "turtlesim_node"
    name: "sim"
    namespace: "turtlesim1"

# start another turtlesim_node in the turtlesim2 namespace and use args to set parameters
-node:
    pkg: "turtlesim"
    exec: "turtlesim_node"
    name: "sim"
    namespace: "turtlesim2"
    param:
    -
      name: "background_r"
      value: "$(var background_r)"
    -
      name: "background_g"
      value: "$(var background_g)"
    -
      name: "background_b"
      value: "$(var background_b)"

# perform remap so both turtles listen to the same command topic
-node:
    pkg: "turtlesim"
    exec: "mimic"
    name: "mimic"
    remap:
    -
        from: "/input/pose"
        to: "/turtlesim1/turtle1/pose"
    -
        from: "/output/cmd_vel"
        to: "/turtlesim2/turtle1/cmd_vel"

2. Using Launch files from the command line

1. Launching

Any of the above launch files can be run via ros2 launch. To try them out locally, you can create a new package and then use

ros2 launch <package_name> <launch_file_name>

Or run the launch file directly by specifying the path to the file

ros2 launch <path_to_launch_file>

2. Set parameters

To set parameters passed to the startup file, the key:=value syntax should be used. For example, you can set the value of background_r in the following way:

ros2 launch <package_name> <launch_file_name> background_r:=255
ros2 launch <path_to_launch_file> background_r:=255

3. Control turtles

To test whether the remapping is working, you can run the following command in another terminal to control the turtle:

ros2 run turtlesim turtle_teleop_key --ros-args --remap __ns:=/turtlesim1

3. Python, XML or YAML: Which language should I use?

Startup files in ROS 1 are written in XML, so for users coming from ROS 1, XML is probably the most familiar.

For most applications, the choice of ROS 2 boot format depends on developer preference. However, if your startup file requires flexibility that isn’t possible with XML or YAML, you can use Python to write your startup file. Writing ROS 2 startup files in Python is more flexible for two reasons:

  • Python is a scripting language, so you can use the language and its libraries in your startup file.

  • Both ros2/launch (general launch functionality) and ros2/launch_ros (ROS 2 specific launch functionality) are written in Python, so you can access lower-level launch functionality that XML and YAML may not provide.

Still, startup files written in Python can be more complex and verbose than files written in XML or YAML.

4. Why use ROS 2 Launch

ROS 2 systems typically consist of multiple nodes running on multiple different processes (or even different machines). While each node can be run independently, this can quickly become cumbersome.

The startup system in ROS 2 is designed to automatically run multiple nodes with a single command. It helps users describe system configuration and then execute it as described. System configuration includes which programs are run, where they are run, which parameters are passed, and ROS-specific conventions that make it easy to reuse components throughout the system by providing different configurations for each component. It is also responsible for monitoring the status of started processes and reporting and/or reacting to changes in the status of these processes.

All of the above is specified in a startup file, which can be written in Python, XML, or YAML. After running this launch file using the ros2 launch command, all specified nodes will be running.

The design document details the design goals of the ROS 2 boot system (not all features are currently available).