RobotForge
Published·~16 min

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.

by RobotForge
#ros2#launch#python

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.

bringup.launch.py your orchestrator camera_node USB driver lidar_node SICK 1kHz slam_node ORB-SLAM3 nav2 planner + ctrl tf_publishers.launch.py included launch one command → fifteen processes
One launch file brings up the camera driver, lidar, SLAM, Nav2, and can include other launch files as sub-launches.

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

  1. Declare → Read: every launch argument is declared once, read through LaunchConfiguration.
  2. Include: compose subsystems with IncludeLaunchDescription.
  3. Condition: IfCondition/UnlessCondition for sim-only, real-only, or optional nodes.
  4. Namespace + remap: duplicate a node for multiple sensors without topic collisions.
  5. Event handlers: RegisterEventHandler to 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.