Skip to main content

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:

  1. Defines a set of nodes to start
  2. Specifies parameters for each node
  3. Optionally remaps topic names
  4. 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 executable
  • executable='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 parameters argument

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_files with glob patterns tells ROS 2 to include all .launch.py files from the launch/ 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