ROS Launch Setup

Attention

The following tutorial is not meant as a step-by-step solution for the first assignment. These are just toy examples to demonstrate how to use ROS and interact with the simulated BlueROV in an easy to follow manner. Therefore, we do not claim that these code snippets are complete and we use some funny names at times. Please do not copy-paste them.

Prerequisites

We assume, we have the package created in the previous tutorial. Hence, the package structure should resemble this

~/fav/ros2/src/awesome_package
├── nodes
│   └── setpoint_publisher.py
├── CMakeLists.txt
└── package.xml

Let’s Start

We create a new directory called launch

$ cd ~/fav/ros2/src/awesome_package
$ mkdir launch

and create a launch file

$ touch launch/setpoint.launch.py

and start with a very minimal version of a launch file

1from launch import LaunchDescription
2
3def generate_launch_description() -> LaunchDescription:
4   launch_description = LaunchDescription()
5   return launch_description

Since now we have a launch directory, we have to tell our build system to install it.

Open CMakeLists.txt and add the following lines right before the ament_package() call.

install(
  DIRECTORY launch
  DESTINATION share/${PROJECT_NAME}
)

These give the build system instructions to install all the directories following DIRECTORY.

Note

We only have to add this once. Even if we add more launch files. The whole directory gets installed by this instruction.

If we try to run the launch file with

$ ros2 launch awesome_package setpoint.launch.py

we get an error message that the launch file could not be found. No reason to trust anyone blindly. Try it out yourself!

What did we forget? We did not rebuild our workspace. The instructions in CMakeLists.txt are only executed when we build the workspace with

$ build_ros

Now, try it again. The launch command above should succeed. Since it only consists of boilerplate code, not much will happen. Time to add some functionality.

Launch a Node

In the previous tutorial we have started our awesome setpoint_publisher.py via ros2 run. Let’s see how we would accomplish this with our launch file.

setpoint.launch.py
 1from launch_ros.actions import Node
 2from launch import LaunchDescription
 3
 4
 5def generate_launch_description() -> LaunchDescription:
 6    launch_description = LaunchDescription()
 7
 8    node = Node(executable='setpoint_publisher.py', package='awesome_package')
 9    launch_description.add_action(node)
10
11    return launch_description

Note

We do not have to rebuild anything. Just make sure the file has been saved. Rebuilding is only required when we add new files.

We again start the launch file

$ ros2 launch awesome_package setpoint.launch.py

and see the following output

[INFO] [launch]: All log files can be found ...
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [setpoint_publisher.py-1]: process started with pid [4991]

We observe our node has been started. And this time, the execution does not terminate by itself. Stop everything by hitting Ctrl + C.

What comes next?

A lot!

  • “pushing” nodes “into namespaces”

  • including other launch files

  • using launch arguments

The python-based launch workflow in ROS2 may appear quite complex and cumbersome when launch files get more complicated than our previous toy example. Do not feel discouraged by this and do not worry if you do not manage to understand everything immediately! You will get used to to it, step by step each time you work with it.

So, why are we using lanch files, you might ask. Because it greatly simplifies launching our setups. Write the launch file once and profit every time we start any setup. Trust me, you will start things many times.

Pushing Nodes into Namespaces

Why do we care about namespaces? We want to avoid topic name collisions. Just imagine we have more than one node publishing a debug topic, calling it debug. Or what about having multiple robots? We can easly imagine operating two BlueROVs at the same time. How can we distinguish between topics associated with the first and the second robot? Having different source code with manually changed topic names for both robots? Does not sound like a way anyone would like to go. Here, ROS namespaces come to the rescue! Simply pushing nodes to so-called namespaces can avoid all these problems.

We have a great overview on the topic of namespaces in Names and Namespaces. What we recommend is as a guideline:

  • Use namespaces where appropriate (in the course of this class: most likely everywhere).

  • Never use global topic names if you do not have a specific reason to do so.

  • yeah, that’s actually it…

Let’s illustrate that with the help of our setpoint_publisher.py we created in the previous section. We created the publisher with

self.create_publisher(ActuatorSetpoint, 'thrust_setpoint', 1)

Topic names starting with / are global. Hence, the topic name stays always exactly what we defined, no matter what namespaces the node is in, or what the node’s name is.
But dude, I do not see a leading / here “.
True that. Thus, we have specified a relative topic (and not a global one). This means that the topic name will by resolved at runtime: prepending all nested namespaces of our node. We can quickly see this by pushing our node to different namespaces and check the resulting topic name with ros2 topic list.

$ ros2 run awesome_package setpoint_publisher.py

ros2 topic list will show the topic name /thrust_setpoint.

$ ros2 run awesome_package setpoint_publisher.py --ros-args -r __ns:=/my_namespace

ros2 topic list will show the topic name /my_namespace/thrust_setpoint. You can also try others namespaces if you like. Just note that namespaces have to start with a leading /.

But didn’t we want do this inside a launch file? We are in the launch file section!
Okay, we have two ways to push nodes into namespaces in launch files. For the first method, we hand over a namespace parameter when creating the Node action.

setpoint.launch.py
 1from launch_ros.actions import Node
 2from launch import LaunchDescription
 3
 4
 5def generate_launch_description() -> LaunchDescription:
 6    launch_description = LaunchDescription()
 7
 8    node = Node(executable='setpoint_publisher.py',
 9                package='awesome_package',
10                namespace='my_namespace')
11    launch_description.add_action(node)
12
13    return launch_description

When we start the launch file with

$ ros2 launch awesome_package setpoint.launch.py

we can observe that the node now publishes under the corresponding namespace. Isn’t this just awesome? We do not have to touch our actual source code at all and are still able to configure our node!

So now let us talk about the second method, which might look like it requires a bit more work. But at the same time it is more powerful. We make use of GroupAction and PushRosNamespace. The GroupAction is just a container for actions. Our Node is an action, so we will put it inside the GroupAction. PushRosNamespace is a special action, that pushes all other actions inside the same group into the defined namespace.

~/fav/ros2/src/awesomepackage/launch/setpoint.launch.py
 1from launch_ros.actions import Node, PushRosNamespace
 2
 3from launch import LaunchDescription
 4from launch.actions import GroupAction
 5
 6
 7def generate_launch_description() -> LaunchDescription:
 8    launch_description = LaunchDescription()
 9
10    node = Node(executable='setpoint_publisher.py',
11                package='awesome_package',
12                namespace='my_namespace')
13    group = GroupAction([
14        PushRosNamespace('pushed_to_this_namespace'),
15        node,
16    ])
17    launch_description.add_action(group)
18
19    return launch_description

Instead of directly adding the Node action to our launch description, we add the node to the GroupAction which in turn is then the action added to the launch description. When starting this launch setup, we get the following result

Did you recognize that we nested two namespaces this way? Since we are still defining a namespace in Node, and additionally push the node to another namespace with the name PushRosNamespace, we end up with a topic name that concatenates these namespaces. We do not need this for now, but we might want to keep this in mind. It might become useful in some situations.

This second approach is more flexible because we are not limited to Node actions that are pushed to our desired namespace. We can even push whole launch files to namespaces, since including launch files is done by using actions. The action of including other launch files can be put inside the GroupAction, just like any other action.

This brings us to our next topic.

Using Launch Arguments

We have seen that we can configure our node in some way (i.e. prepending a namespace to topic names) without touching its source code. The next step is to configure our launch file without the need of changing it. We do not want to hardcode the namespace. We can imagine that we would like to use the same launch setup, i.e. starting the same nodes, for different vehicles with different vehicle names. To differentiate between the vehicles, we would like to use the vehicle name as a namespace name. Without launch arguments this would mean that we would either have to change our launch file constantly between different launches or we would need almost identical launch files with just different values for the namespace for each setup. Both approaches are not that attractive.

Instead, we would like to pass the namespace via the command line during runtime. We need two things for that. First, we declare the argument we would like to pass via the DeclareLaunchArgument action and add this action to our launch description. Second, we access the value of this argument via LaunchConfiguration and use it as parameter for PushRosNamespace instead of hardcoding the value.

~/fav/ros2/src/awesome_package/launch/setpoint.launch.py
 1from launch_ros.actions import Node, PushRosNamespace
 2
 3from launch import LaunchDescription
 4from launch.actions import DeclareLaunchArgument, GroupAction
 5from launch.substitutions import LaunchConfiguration
 6
 7
 8def generate_launch_description() -> LaunchDescription:
 9    launch_description = LaunchDescription()
10
11    arg = DeclareLaunchArgument('vehicle_name')
12    launch_description.add_action(arg)
13
14    node = Node(executable='setpoint_publisher.py', package='awesome_package')
15    group = GroupAction([
16        PushRosNamespace(LaunchConfiguration('vehicle_name')),
17        node,
18    ])
19    launch_description.add_action(group)
20
21    return launch_description

If we just start our setup with the usual

$ ros2 launch awesome_package setpoint.launch.py

We will get an error message

[ERROR] [launch]: Caught exception in launch (see debug for traceback): Included launch description missing required argument 'vehicle_name' (description: 'no description given'), given: []

The launch system complains that we do not have provided our recently declared vehicle_name argument. In general, We can pass arguments with <argument_name>:=<argument_value>. Thus, our launch command becomes

$ ros2 launch awesome_package setpoint.launch.py vehicle_name:=my_vehicle_name

Verify that the topic name gets changed accordingly to how you define the vehicle_name argument in the command line.

Including Launch Files

Okay, okay. Admittedly, we introduced a lot of new and maybe not that easy to understand concepts regarding launch files in ROS. But stay with us for this very last subsection.

It is not only possible to combine sets of nodes in a launch file, but also to combine launch files themselves. Remember the launch file we used to verify that our workspace setup is working?

# do not run this now
$ ros2 launch fav simulation.launch.py vehicle_name:=bluerov00

Let us include this launch file in our awesome setpoint.launch.py launch file. We will need PythonLaunchDescriptionSource and IncludeLaunchDescription to accomplish this.

~/fav/ros2/src/awesome_package/launch/setpoint.launch.py
 1from ament_index_python.packages import get_package_share_path
 2from launch_ros.actions import Node, PushRosNamespace
 3
 4from launch import LaunchDescription
 5from launch.actions import (
 6    DeclareLaunchArgument,
 7    GroupAction,
 8    IncludeLaunchDescription,
 9)
10from launch.launch_description_sources import PythonLaunchDescriptionSource
11from launch.substitutions import LaunchConfiguration
12
13
14def generate_launch_description() -> LaunchDescription:
15    launch_description = LaunchDescription()
16
17    arg = DeclareLaunchArgument('vehicle_name')
18    launch_description.add_action(arg)
19
20    node = Node(executable='setpoint_publisher.py', package='awesome_package')
21    group = GroupAction([
22        PushRosNamespace(LaunchConfiguration('vehicle_name')),
23        node,
24    ])
25    launch_description.add_action(group)
26
27    package_path = get_package_share_path('fav')
28    launch_path = str(package_path / 'launch/simulation.launch.py')
29    source = PythonLaunchDescriptionSource(launch_path)
30    launch_args = {'vehicle_name': LaunchConfiguration('vehicle_name')}
31    action = IncludeLaunchDescription(source,
32                                      launch_arguments=launch_args.items())
33    launch_description.add_action(action)
34
35    return launch_description

Are you wondering what launch_arguments in line 32 is needed for? This is required because the included launch file declares launch arguments as well. If we do not provide it with the arguments that it declares, it will complain about it. Usually we always use the vehicle_name parameter as namespace for all vehicle related nodes.

To conveniently find out what arguments are declared by a launch file or in any of its included launch files, we can pass -s to the launch command. We can inspect the launch arugments declared by the launch file we included in our setpoint.launch.py, we run

$ ros2 launch fav simulation.launch.py -s

The result will list many arguments. The only parameter without default value is vehicle_name. Therefore, we need to pass it to our launch file as we have seen above.

Also, we will run across an argument called use_sim_time quite often. For the simulation, we hardcoded it to true. Hence, it is not necessary to manually set this argument in our example launch file. This parameter controls the time source of a node. If set to true, nodes will automatically subscribe to a special topic which provides the current time. In this case, the actual time of the computer (wall time) is ignored. Instead, a simulated time, starting at 0 each time you restart the simulation, is used. This is obviously very useful for simulations.

Depending on the performance of our computers, the simulation might be slower than real-time. If your computer is very fast, you might even simulate faster than real-time! By using the simulated time as time source, the simulation (gazebo) can control how fast time passes by from the perspective of the nodes.

As a simple rule, the value should always be true for simulation setups and always be false for real world experiments.