Launch Files and Configuration
The Problem: Starting Many Nodes
A real robot system has dozens of nodes running simultaneously:
# Manual startup (painful!)
ros2 run camera_driver camera_node &
ros2 run object_detector yolo_node &
ros2 run path_planner planner_node &
ros2 run motor_controller controller_node &
ros2 run rviz2 rviz2 -d config.rviz &
# ... 20 more nodes ...
Problems:
- Error-prone (forget a node → system breaks)
- No coordination (which node starts first?)
- Hard to configure (parameters scattered across commands)
- Can't reproduce (no record of how system was started)
Solution: Launch files start all nodes with one command, manage dependencies, and centralize configuration.
ROS 2 Launch System
Launch File Types
| Type | Extension | Use Case |
|---|---|---|
| Python | .launch.py | Recommended: Most flexible, programmatic control |
| XML | .launch.xml | Simpler syntax, less flexibility |
| YAML | .launch.yaml | Declarative, limited capabilities |
This course focuses on Python launch files for maximum power.
Basic Python Launch File
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='camera_driver',
executable='camera_node',
name='camera',
output='screen',
),
Node(
package='object_detector',
executable='yolo_node',
name='detector',
output='screen',
),
Node(
package='path_planner',
executable='planner_node',
name='planner',
output='screen',
),
])
Run:
ros2 launch my_package robot.launch.py
All three nodes start simultaneously with one command!
Node Configuration
Setting Parameters
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='camera_driver',
executable='camera_node',
name='camera',
parameters=[{
'frame_rate': 30,
'resolution': [1920, 1080],
'auto_exposure': True,
}],
output='screen',
),
])
Loading from YAML File
config/camera_params.yaml:
camera:
ros__parameters:
frame_rate: 30
resolution: [1920, 1080]
auto_exposure: true
exposure_time: 10.0
Launch file:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
# Get package path
pkg_path = get_package_share_directory('my_package')
# Load parameters from YAML
params_file = os.path.join(pkg_path, 'config', 'camera_params.yaml')
return LaunchDescription([
Node(
package='camera_driver',
executable='camera_node',
name='camera',
parameters=[params_file],
output='screen',
),
])
Remapping Topics
Scenario: camera_node publishes to /image, but detector_node expects /camera/image.
Solution: Remap topics in launch file:
Node(
package='camera_driver',
executable='camera_node',
name='camera',
remappings=[
('/image', '/camera/image'),
],
output='screen',
),
Namespaces
Run multiple instances of same node (e.g., two cameras):
Node(
package='camera_driver',
executable='camera_node',
name='left_camera',
namespace='left',
parameters=[{'device_id': 0}],
),
Node(
package='camera_driver',
executable='camera_node',
name='right_camera',
namespace='right',
parameters=[{'device_id': 1}],
),
Result:
- Left camera publishes to:
/left/image - Right camera publishes to:
/right/image
Launch Arguments (Command-Line Parameters)
Make launch files configurable:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
# Declare arguments
use_sim_time_arg = DeclareLaunchArgument(
'use_sim_time',
default_value='false',
description='Use simulation time if true'
)
camera_device_arg = DeclareLaunchArgument(
'camera_device',
default_value='0',
description='Camera device ID'
)
# Get argument values
use_sim_time = LaunchConfiguration('use_sim_time')
camera_device = LaunchConfiguration('camera_device')
return LaunchDescription([
use_sim_time_arg,
camera_device_arg,
Node(
package='camera_driver',
executable='camera_node',
name='camera',
parameters=[{
'use_sim_time': use_sim_time,
'device_id': camera_device,
}],
output='screen',
),
])
Usage:
# Default values
ros2 launch my_package robot.launch.py
# Override arguments
ros2 launch my_package robot.launch.py use_sim_time:=true camera_device:=1
Including Other Launch Files
Modular approach: Break complex system into multiple launch files.
robot_bringup.launch.py:
from launch import LaunchDescription
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():
sensors_pkg = get_package_share_directory('sensors_package')
perception_pkg = get_package_share_directory('perception_package')
navigation_pkg = get_package_share_directory('navigation_package')
return LaunchDescription([
# Include sensors launch file
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(sensors_pkg, 'launch', 'sensors.launch.py')
),
),
# Include perception launch file
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(perception_pkg, 'launch', 'perception.launch.py')
),
),
# Include navigation launch file
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(navigation_pkg, 'launch', 'navigation.launch.py')
),
),
])
Passing Arguments to Included Launch Files
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(sensors_pkg, 'launch', 'sensors.launch.py')
),
launch_arguments={
'use_sim_time': 'true',
'camera_device': '0',
}.items(),
),
Conditional Execution
Start nodes based on conditions:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
use_rviz_arg = DeclareLaunchArgument(
'use_rviz',
default_value='true',
description='Start RViz if true'
)
use_rviz = LaunchConfiguration('use_rviz')
return LaunchDescription([
use_rviz_arg,
# Always start these nodes
Node(
package='camera_driver',
executable='camera_node',
name='camera',
),
# Conditionally start RViz
Node(
package='rviz2',
executable='rviz2',
name='rviz',
condition=IfCondition(use_rviz),
arguments=['-d', '/path/to/config.rviz'],
),
])
Usage:
# Start with RViz (default)
ros2 launch my_package robot.launch.py
# Start without RViz
ros2 launch my_package robot.launch.py use_rviz:=false
Loading URDF/Robot Description
Common pattern: Load robot URDF and publish to /robot_description topic.
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.substitutions import Command
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
pkg_path = get_package_share_directory('my_robot_description')
xacro_file = os.path.join(pkg_path, 'urdf', 'robot.urdf.xacro')
# Process xacro file to URDF
robot_description = Command(['xacro ', xacro_file])
return LaunchDescription([
# Robot state publisher (publishes transforms and robot description)
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
name='robot_state_publisher',
output='screen',
parameters=[{
'robot_description': robot_description,
'use_sim_time': True,
}],
),
# Joint state publisher (publishes joint states)
Node(
package='joint_state_publisher',
executable='joint_state_publisher',
name='joint_state_publisher',
output='screen',
),
# RViz for visualization
Node(
package='rviz2',
executable='rviz2',
name='rviz',
arguments=['-d', os.path.join(pkg_path, 'rviz', 'robot.rviz')],
),
])
Practical Example: Humanoid Robot Launch
humanoid_bringup.launch.py (complete system startup):
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch.conditions import IfCondition
from launch_ros.actions import Node
def generate_launch_description():
# Package paths
robot_desc_pkg = get_package_share_directory('humanoid_description')
sensors_pkg = get_package_share_directory('humanoid_sensors')
control_pkg = get_package_share_directory('humanoid_control')
# Launch arguments
use_sim_time_arg = DeclareLaunchArgument(
'use_sim_time',
default_value='false',
description='Use simulation time'
)
use_rviz_arg = DeclareLaunchArgument(
'use_rviz',
default_value='true',
description='Start RViz'
)
# Configuration
use_sim_time = LaunchConfiguration('use_sim_time')
use_rviz = LaunchConfiguration('use_rviz')
# Robot description (URDF)
xacro_file = os.path.join(robot_desc_pkg, 'urdf', 'humanoid.urdf.xacro')
robot_description = Command(['xacro ', xacro_file])
# Parameter files
controller_params = os.path.join(control_pkg, 'config', 'controllers.yaml')
sensor_params = os.path.join(sensors_pkg, 'config', 'sensors.yaml')
return LaunchDescription([
# Arguments
use_sim_time_arg,
use_rviz_arg,
# Robot state publisher
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
name='robot_state_publisher',
output='screen',
parameters=[{
'robot_description': robot_description,
'use_sim_time': use_sim_time,
}],
),
# Sensor drivers
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(sensors_pkg, 'launch', 'sensors.launch.py')
),
launch_arguments={
'use_sim_time': use_sim_time,
'params_file': sensor_params,
}.items(),
),
# Controllers (balance, locomotion)
Node(
package='humanoid_control',
executable='balance_controller',
name='balance_controller',
parameters=[controller_params, {'use_sim_time': use_sim_time}],
output='screen',
),
Node(
package='humanoid_control',
executable='locomotion_controller',
name='locomotion_controller',
parameters=[controller_params, {'use_sim_time': use_sim_time}],
output='screen',
),
# VLA cognitive layer (AI planning)
Node(
package='humanoid_ai',
executable='vla_planner',
name='vla_planner',
parameters=[{'use_sim_time': use_sim_time}],
output='screen',
),
# RViz (conditional)
Node(
package='rviz2',
executable='rviz2',
name='rviz',
condition=IfCondition(use_rviz),
arguments=['-d', os.path.join(robot_desc_pkg, 'rviz', 'humanoid.rviz')],
),
])
Usage:
# Real robot
ros2 launch humanoid_bringup humanoid_bringup.launch.py
# Simulation (with RViz)
ros2 launch humanoid_bringup humanoid_bringup.launch.py use_sim_time:=true
# Simulation (headless, no RViz)
ros2 launch humanoid_bringup humanoid_bringup.launch.py \
use_sim_time:=true use_rviz:=false
Event Handlers
Execute actions based on events:
Start Node After Another Node is Ready
from launch import LaunchDescription
from launch.actions import RegisterEventHandler
from launch.event_handlers import OnProcessStart
from launch_ros.actions import Node
def generate_launch_description():
camera_node = Node(
package='camera_driver',
executable='camera_node',
name='camera',
)
# Start detector only after camera is started
detector_node = Node(
package='object_detector',
executable='yolo_node',
name='detector',
)
return LaunchDescription([
camera_node,
RegisterEventHandler(
OnProcessStart(
target_action=camera_node,
on_start=[detector_node],
)
),
])
Restart Node on Exit
from launch.event_handlers import OnProcessExit
from launch.actions import EmitEvent
from launch.events import Shutdown
# Shutdown entire system if critical node exits
RegisterEventHandler(
OnProcessExit(
target_action=balance_controller,
on_exit=[EmitEvent(event=Shutdown(reason='Balance controller died'))],
)
),
Logging Configuration
Control log output levels:
Node(
package='my_package',
executable='my_node',
name='my_node',
output='screen',
arguments=['--ros-args', '--log-level', 'debug'],
),
Log levels: debug, info, warn, error, fatal
Best Practices
1. Organize by Function
launch/
├── sensors.launch.py # All sensor drivers
├── perception.launch.py # Object detection, SLAM
├── navigation.launch.py # Path planning, Nav2
├── control.launch.py # Motor controllers
└── full_system.launch.py # Includes all of the above
2. Use Parameters Files
Don't:
Node(
parameters=[{
'param1': 10,
'param2': 20,
'param3': 'value',
# 50 more parameters...
}]
)
Do:
params_file = os.path.join(pkg_path, 'config', 'node_params.yaml')
Node(parameters=[params_file])
3. Provide Defaults, Allow Overrides
DeclareLaunchArgument(
'camera_device',
default_value='0', # Good default
description='Camera device ID'
)
4. Document Launch Arguments
DeclareLaunchArgument(
'use_sim_time',
default_value='false',
description='Use simulation (Gazebo) time instead of system time. '
'Set to true when running in simulation.'
)
Debugging Launch Files
Print Launch Configuration
ros2 launch my_package robot.launch.py --show-args
# Shows all available arguments and defaults
Check What Nodes Will Start
ros2 launch my_package robot.launch.py --print-description
# Shows nodes that will be launched
Verbose Output
ros2 launch -v my_package robot.launch.py
# Shows detailed launch process
Next Steps
Congratulations! You've completed Module 1: The Robotic Nervous System (ROS 2). You can now:
- Create ROS 2 nodes in Python
- Communicate via topics, services, and actions
- Define robot morphology with URDF
- Orchestrate complex systems with launch files
Next, dive into Module 2 to learn physics simulation with Gazebo and Unity.
👉 Next: Module 2 - The Digital Twin →
- Start small: Build launch files incrementally, test each addition
- Use IncludeLaunchDescription: Modularize for reusability
- Parameterize everything: Avoid hardcoded values
- Provide good defaults: Users shouldn't need to specify arguments
- Document: Clear descriptions for all launch arguments
Lab Exercise
Create a humanoid sensor launch system:
-
sensors.launch.py:
- RealSense camera driver
- LIDAR driver
- IMU driver
- Load parameters from
config/sensors.yaml
-
perception.launch.py:
- YOLO object detector (subscribes to camera)
- VSLAM node (subscribes to camera + IMU)
- Publishes to
/detectionsand/odometry
-
robot.launch.py (full system):
- Include sensors.launch.py
- Include perception.launch.py
- Start robot_state_publisher with URDF
- Conditionally start RViz
-
Test:
- Launch with:
ros2 launch my_package robot.launch.py - Verify all nodes running:
ros2 node list - Check topics:
ros2 topic list
- Launch with:
Bonus: Add launch arguments for camera resolution and YOLO model selection.