Lesson 6.2: Launch Files — Starting Multi-Node Systems
Imagine you built a robot system with 10 nodes:
- 3 sensor publishers (camera, LIDAR, IMU)
- 2 controllers (motion, arm)
- 3 monitors (battery, thermal, diagnostics)
- 2 safety nodes (emergency stop, collision avoidance)
Launching each node individually in separate terminals would be chaotic:
# Terminal 1
ros2 run sensor_pkg camera_node --ros-args --log-level debug
# Terminal 2
ros2 run sensor_pkg lidar_node --ros-args --log-level debug
# Terminal 3
ros2 run sensor_pkg imu_node
# ... etc
Launch files solve this problem. A single launch file can start all nodes with one command, pass parameters to each node, and coordinate initialization.
What is a Launch File?
A ROS 2 launch file is a Python script that:
- Defines a set of nodes to start
- Specifies parameters for each node
- Optionally remaps topic names
- Coordinates node startup and shutdown
Launch files use the launch and launch_ros packages to describe the system declaratively.
Basic Launch File Structure
Minimal Example: Two Nodes
Create a file: my_robot_pkg/launch/simple_system.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
# Node 1: Publisher
Node(
package='my_first_package',
executable='minimal_publisher',
name='sensor_publisher',
output='screen',
),
# Node 2: Subscriber
Node(
package='my_first_package',
executable='minimal_subscriber',
name='data_processor',
output='screen',
),
])
What this does:
package='my_first_package': Which ROS 2 package contains the executableexecutable='minimal_publisher': The entry point name (from package's setup.py)name='sensor_publisher': The node name when it runs (can differ from executable name)output='screen': Print logs to terminal (instead of silent mode)
Run it:
ros2 launch my_first_package simple_system.launch.py
Expected output:
[minimal_publisher-1] [INFO] [minimal_publisher]: Starting publisher
[data_processor-1] [INFO] [minimal_subscriber]: I heard: "Hello World: 0"
[data_processor-1] [INFO] [minimal_subscriber]: I heard: "Hello World: 1"
[minimal_publisher-1] [INFO] [minimal_publisher]: Publishing: Hello World: 2
[data_processor-1] [INFO] [minimal_subscriber]: I heard: "Hello World: 2"
Both nodes start together (prefixed with their index), communicate immediately, and both log to the same terminal. The publisher keeps publishing, the subscriber keeps receiving.
Launch Files with Parameters
The real power of launch files is passing parameters to nodes at startup.
Multi-Node System with Different Parameters
Create: my_robot_pkg/launch/robot_system.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
# Fast sensor: High publication rate
Node(
package='my_first_package',
executable='configurable_node',
name='camera_publisher',
output='screen',
parameters=[{
'publish_rate': 30.0, # 30 Hz for camera
'robot_name': 'my_robot',
'sensor_type': 'camera',
}],
),
# Slow sensor: Lower publication rate
Node(
package='my_first_package',
executable='configurable_node',
name='lidar_publisher',
output='screen',
parameters=[{
'publish_rate': 10.0, # 10 Hz for LIDAR
'robot_name': 'my_robot',
'sensor_type': 'lidar',
}],
),
# Data processor: Consumes both sensors
Node(
package='my_first_package',
executable='data_processor',
name='sensor_fusion',
output='screen',
parameters=[{
'timeout': 5.0,
}],
),
])
Key observations:
- Same executable (
configurable_node) runs twice with different names and parameters - Each node instance has its own parameter set
- Parameters are passed as a list of dictionaries to the
parametersargument
Run it:
ros2 launch my_first_package robot_system.launch.py
Verify parameter assignment:
ros2 param list /camera_publisher
# Output: /camera_publisher:
# publish_rate
# robot_name
# sensor_type
ros2 param get /camera_publisher publish_rate
# Output: publish_rate: 30.0
ros2 param get /lidar_publisher publish_rate
# Output: publish_rate: 10.0
Launch Files with Configuration Files (YAML)
For complex systems, hardcoding parameters in the launch file is messy. Instead, use YAML configuration files.
YAML Configuration File
Create: my_robot_pkg/config/robot_params.yaml
# Robot-wide configuration
robot_name: "physical_ai_bot"
robot_id: 001
# Camera node configuration
camera_publisher:
ros__parameters:
publish_rate: 30.0
resolution: "1920x1080"
encoding: "RGB8"
sensor_type: "camera"
# LIDAR node configuration
lidar_publisher:
ros__parameters:
publish_rate: 10.0
angle_range: 360.0
max_distance: 50.0
sensor_type: "lidar"
# Sensor fusion configuration
sensor_fusion:
ros__parameters:
timeout: 5.0
fusion_mode: "weighted"
Launch File Using YAML
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():
# Locate the config file
config_dir = os.path.join(
get_package_share_directory('my_first_package'),
'config'
)
config_file = os.path.join(config_dir, 'robot_params.yaml')
return LaunchDescription([
Node(
package='my_first_package',
executable='configurable_node',
name='camera_publisher',
output='screen',
parameters=[config_file], # Load from YAML
),
Node(
package='my_first_package',
executable='configurable_node',
name='lidar_publisher',
output='screen',
parameters=[config_file], # Load from YAML
),
Node(
package='my_first_package',
executable='data_processor',
name='sensor_fusion',
output='screen',
parameters=[config_file], # Load from YAML
),
])
Advantage: Change parameters without modifying the Python launch file. Just edit the YAML and relaunch.
Making Launch Files Flexible with Arguments
What if you want the same launch file to work in different modes (simulation vs reality, debug vs production)?
Launch File with Arguments
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 launch arguments
robot_name_arg = DeclareLaunchArgument(
'robot_name',
default_value='default_robot',
description='Name of the robot'
)
sim_mode_arg = DeclareLaunchArgument(
'sim_mode',
default_value='true',
description='Run in simulation (true) or real (false)'
)
return LaunchDescription([
robot_name_arg,
sim_mode_arg,
Node(
package='my_first_package',
executable='robot_controller',
name='controller',
output='screen',
parameters=[{
'robot_name': LaunchConfiguration('robot_name'),
'simulation_mode': LaunchConfiguration('sim_mode'),
'safety_enabled': True,
}],
),
])
Run with default arguments:
ros2 launch my_first_package robot_system.launch.py
# Uses: robot_name=default_robot, sim_mode=true
Run with custom arguments:
ros2 launch my_first_package robot_system.launch.py robot_name:=my_bot sim_mode:=false
# Uses: robot_name=my_bot, sim_mode=false
Setting Up Your Package for Launch Files
For launch files to work, you must tell ROS 2 where to find them. This is done in setup.py.
Minimal setup.py
from setuptools import setup
import os
from glob import glob
package_name = 'my_first_package'
setup(
name=package_name,
version='0.1.0',
packages=[package_name],
data_files=[
# Standard ROS 2 files
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
# Include launch files
(os.path.join('share', package_name, 'launch'),
glob('launch/*.launch.py')),
# Include config files
(os.path.join('share', package_name, 'config'),
glob('config/*.yaml')),
],
install_requires=['setuptools'],
entry_points={
'console_scripts': [
'minimal_publisher = my_first_package.minimal_publisher:main',
'minimal_subscriber = my_first_package.minimal_subscriber:main',
'configurable_node = my_first_package.configurable_node:main',
'data_processor = my_first_package.data_processor:main',
],
},
)
Key parts:
data_fileswith glob patterns tells ROS 2 to include all.launch.pyfiles from thelaunch/directory- Same for config files in the
config/directory
After adding launch files:
cd ~/ros2_ws
colcon build --packages-select my_first_package
source install/setup.bash
Guided Practice: Complete Multi-Node System
Let's build a small robot system launch configuration with:
- 2 sensor publishers (different rates)
- 1 data processor
- Configurable parameters
Step 1: Create Package Structure
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python robot_demo
cd robot_demo
mkdir -p launch config
Step 2: Create Nodes
Create three simple Python files in robot_demo/robot_demo/:
sensor_node.py (publishes fake sensor data):
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
class SensorNode(Node):
def __init__(self):
super().__init__('sensor_node')
self.declare_parameter('publish_rate', 1.0)
self.declare_parameter('sensor_name', 'sensor1')
rate = self.get_parameter('publish_rate').value
self.publisher_ = self.create_publisher(Float32, 'sensor_data', 10)
self.timer = self.create_timer(1.0/rate, self.timer_callback)
def timer_callback(self):
msg = Float32()
msg.data = 42.0
sensor_name = self.get_parameter('sensor_name').value
self.get_logger().info(f'{sensor_name} published: {msg.data}')
self.publisher_.publish(msg)
def main(args=None):
rclpy.init(args=args)
rclpy.spin(SensorNode())
rclpy.shutdown()
if __name__ == '__main__':
main()
processor_node.py (subscribes to sensor data):
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
class ProcessorNode(Node):
def __init__(self):
super().__init__('processor_node')
self.subscription = self.create_subscription(Float32, 'sensor_data', self.callback, 10)
def callback(self, msg):
self.get_logger().info(f'Processed: {msg.data}')
def main(args=None):
rclpy.init(args=args)
rclpy.spin(ProcessorNode())
rclpy.shutdown()
if __name__ == '__main__':
main()
Step 3: Create Launch File
Create robot_demo/launch/robot_demo.launch.py:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='robot_demo',
executable='sensor_node',
name='temperature_sensor',
output='screen',
parameters=[{
'publish_rate': 5.0,
'sensor_name': 'Temperature',
}],
),
Node(
package='robot_demo',
executable='sensor_node',
name='humidity_sensor',
output='screen',
parameters=[{
'publish_rate': 2.0,
'sensor_name': 'Humidity',
}],
),
Node(
package='robot_demo',
executable='processor_node',
name='data_processor',
output='screen',
),
])
Step 4: Build and Run
cd ~/ros2_ws
colcon build --packages-select robot_demo
source install/setup.bash
ros2 launch robot_demo robot_demo.launch.py
Expected output:
[sensor_node-1] [INFO] [temperature_sensor]: Temperature published: 42.0
[sensor_node-2] [INFO] [humidity_sensor]: Humidity published: 42.0
[processor_node-1] [INFO] [processor_node]: Processed: 42.0
[processor_node-1] [INFO] [processor_node]: Processed: 42.0
Independent Practice
Exercise 1: Parameter Configuration
Add a config/sensors.yaml file with sensor-specific parameters, and modify the launch file to load it.
Exercise 2: Launch with Arguments
Modify the launch file to accept a robot_mode argument (simulation/real) and pass it to nodes.
Exercise 3: Complex System
Create a system with:
- 3 sensor nodes (different rates)
- 2 processors (each subscribes to different sensors)
- 1 aggregator (subscribes to both processors)
Document each node's role in a comment.
Try With AI
Setup: Work with your AI to design and debug a multi-node launch system.
Prompt Set:
Prompt 1: "I have 5 nodes that need to start together, but they depend on each other (Node A must start before Node B). How do I structure this in a launch file? Can I specify startup order?"
Prompt 2: "I want to run the same node executable multiple times with different parameters. What's the best way to set up the launch file to avoid duplication?"
Prompt 3: "When I launch my system, one node fails to start. How would I debug this? What tools help identify which node crashed and why?"
Expected Outcomes:
- AI explains launch file execution order (usually parallel, but dependencies can be specified)
- AI shows parameter templating patterns to reduce duplication
- AI demonstrates debugging workflows (ros2 launch verbosity, ros2 node list, logs)
Safety Note: When launching robot systems, verify all nodes start successfully and are communicating before operating physical hardware. Use ros2 node list and rqt_graph to confirm connectivity.
Optional Stretch: Create a launch file that:
- Starts a system with 3+ nodes
- Has 3+ configurable parameters
- Accepts command-line arguments to switch between modes
- Includes logging configuration for debugging