Launch files: starting 20 nodes without losing your mind
Launch files are the recipe that brings a whole robot stack up with one command. Here's the Python launch API, when to use conditionals vs sub-launch, and the patterns that scale past 5 nodes.
Running a robot is not running one node. It's running fifteen: the camera driver, the lidar driver, the state publisher, the SLAM node, the planner, the controller, the safety monitor, the logger, and more. Typing ros2 run fifteen times is a path to madness. Launch files are the recipe.
XML vs Python launch
ROS 2 supports both. They do the same thing; the syntax differs. Industry has converged on Python launch because you can compute things (conditionals, string formatting, loops) that XML can't do cleanly. XML is fine for static stacks; Python is required for anything that needs to adapt at launch time.
The minimal Python launch file
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim',
),
Node(
package='turtlesim',
executable='turtle_teleop_key',
name='teleop',
prefix='xterm -e', # launch in its own terminal
),
])
Save as my_launch.py. Run with ros2 launch my_launch.py. Two nodes start; Ctrl-C kills both. That's a launch file.
Arguments and substitutions
Real launch files need to be parameterized — different log levels in dev vs prod, sim time vs wall time, different robot names. Use DeclareLaunchArgument and LaunchConfiguration:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
use_sim_time = LaunchConfiguration('use_sim_time')
return LaunchDescription([
DeclareLaunchArgument('use_sim_time', default_value='false'),
Node(
package='slam_toolbox', executable='async_slam_toolbox_node',
parameters=[{'use_sim_time': use_sim_time}],
),
])
Launch with ros2 launch my_launch.py use_sim_time:=true. The whole stack flips to sim time without editing code. This single pattern eliminates 80% of "works on my machine" bugs.
Composing launch files
Don't put 15 nodes in one file. Break by subsystem and compose:
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
pkg = get_package_share_directory('my_robot')
return LaunchDescription([
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg, 'launch', 'drivers.launch.py'))),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg, 'launch', 'perception.launch.py'))),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg, 'launch', 'nav.launch.py'))),
])
Each sub-launch file owns one domain. One file, one responsibility. Parameter passing up and down through launch arguments.
Conditionals
Common pattern: start a node only in sim, or only when a camera is present.
from launch.conditions import IfCondition
Node(
package='my_robot', executable='fake_hw',
condition=IfCondition(LaunchConfiguration('use_sim')),
)
Namespaces and remapping
Running two of the same node without collisions:
Node(package='camera_driver', executable='driver',
namespace='camera_front',
remappings=[('image_raw', 'front/image_raw')])
The node's /image_raw topic becomes /camera_front/front/image_raw. Two cameras, two namespaces, no conflict.
The 5 launch patterns you'll use over and over
- Declare → Read: every launch argument is declared once, read through
LaunchConfiguration. - Include: compose subsystems with
IncludeLaunchDescription. - Condition:
IfCondition/UnlessConditionfor sim-only, real-only, or optional nodes. - Namespace + remap: duplicate a node for multiple sensors without topic collisions.
- Event handlers:
RegisterEventHandlerto start a second node after the first successfully launches.
Common mistakes
- Hardcoded paths. Always use
get_package_share_directory; never hardcode/home/ubuntu/.... - Forgetting
use_sim_time. When running in sim, every node in your stack needs the parameter. One missed node and your TF tree breaks. - Nesting more than 3 levels deep. If your top-level launch includes a launch that includes a launch that includes a launch, refactor.
- Passing giant dicts as parameters. Use YAML config files loaded with
PathJoinSubstitution.
Next
Parameters and remapping in depth — the runtime configuration system that makes your launch files portable across robots.
Comments
Sign in to post a comment.