Once a robot has more than two nodes, starting them one terminal at a time stops being practical. Launch files start everything with one command and set each node's parameters, topic names and namespace. This guide builds a Python launch file step by step, including a parameter gotcha that catches almost everyone who uses namespaces. Everything was tested on ROS 2 Jazzy.
The smallest useful launch file
We start from the talker and listener from our publisher and subscriber tutorial. Save this as launch/demo.launch.py in the package:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(package='robot_demo', executable='talker', output='screen'),
Node(package='robot_demo', executable='listener', output='screen'),
])
ROS looks for a function named generate_launch_description that returns a LaunchDescription. Each Node action starts one executable; output='screen' shows its log in your terminal.
Install the launch file
Launch files run from the install space, so the package must install them. In an ament_python package, add a data_files entry in setup.py (with from glob import glob at the top):
('share/' + package_name + '/launch', glob('launch/*.launch.py')),
In an ament_cmake package, use install(DIRECTORY launch DESTINATION share/${PROJECT_NAME}). Also add <exec_depend>launch_ros</exec_depend> to package.xml. Rebuild, source, and run:
colcon build --symlink-install --packages-select robot_demo
source install/setup.bash
ros2 launch robot_demo demo.launch.py
Arguments, namespaces and remapping
A real launch file takes arguments, gives the robot its own namespace and renames topics to fit the rest of the system:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
robot_name = LaunchConfiguration('robot_name')
params_file = PathJoinSubstitution([FindPackageShare('robot_demo'), 'config', 'talker.yaml'])
return LaunchDescription([
DeclareLaunchArgument('robot_name', default_value='robot1',
description='Namespace for this robot'),
Node(
package='robot_demo',
executable='talker',
namespace=robot_name,
parameters=[params_file],
remappings=[('chatter', 'status')],
output='screen',
),
Node(
package='robot_demo',
executable='listener',
namespace=robot_name,
remappings=[('chatter', 'status')],
output='screen',
),
])
DeclareLaunchArgumentdeclares an argument with a default and a description;LaunchConfigurationreads its value. Pass a value withros2 launch robot_demo demo.launch.py robot_name:=rover_a, and list a file's arguments with--show-args.namespaceprefixes the node's name and its relative topic names, so the nodes become/rover_a/talkerand/rover_a/listener.remappingsrename topics without touching code: herechatterbecomesstatus, so the robot publishes on/rover_a/status.FindPackageSharefinds the package's installed share directory, so the launch file works wherever the workspace lives.
Parameters from a YAML file, and the namespace gotcha
Put parameters in config/talker.yaml (and install the config folder the same way as launch). The obvious way to write it is:
talker:
ros__parameters:
rate_hz: 5.0
On our test robot this file was silently ignored: the talker kept publishing at its default 2 Hz. The reason is the namespace. The top-level key in a ROS 2 parameter file is matched against the node's full name, and with a namespace the node is /rover_a/talker, not talker. Use a wildcard so the file works with any namespace:
/**/talker:
ros__parameters:
rate_hz: 5.0
With that change the talker published at 5 Hz, and ros2 param get /rover_a/talker rate_hz returned 5.0. To apply parameters to every node in a file, use /**: as the key. Inline parameters in the launch file, such as parameters=[{'rate_hz': 5.0}], apply to that node whatever its name.
When a parameter "does not work", check it on the running node with ros2 param get before debugging your code. It tells you immediately whether the value arrived.
Reusing launch files: two robots from one file
Launch files can include other launch files and pass them arguments. This starts the same demo twice, for two robots:
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import PathJoinSubstitution
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
demo = PathJoinSubstitution([FindPackageShare('robot_demo'), 'launch', 'demo.launch.py'])
return LaunchDescription([
IncludeLaunchDescription(PythonLaunchDescriptionSource(demo),
launch_arguments={'robot_name': 'rover_a'}.items()),
IncludeLaunchDescription(PythonLaunchDescriptionSource(demo),
launch_arguments={'robot_name': 'rover_b'}.items()),
])
Each robot's listener hears only its own talker, because the namespaces keep their topics apart. This is the same pattern used to run several simulated robots, or a fleet of real ones, from shared launch files.
Troubleshooting
- "file 'demo.launch.py' was not found in the share directory of package": the file is not installed. Check
data_filesorinstall(DIRECTORY …), rebuild and source. - Parameters ignored: check the YAML key against the node's full name, as above.
- Topics not connecting: list them with
ros2 topic list. A leading slash (/chatter) makes a topic name absolute, so it ignores the namespace. - Edits to the launch file have no effect: without
--symlink-install, the installed copy is used until you rebuild.
The ROS 2 Package Generator creates a package with a working launch file and the install rules already in place. For build problems, see common colcon errors.