Introduction: Where the Magic Happens
Part 1 covered the infrastructure. Now we cover what you actually came here for: launching ROS 2 nodes.
If you skipped Part 1, go back and read the section on “Actions.” This part assumes you understand that Node is an Action, not a ROS node itself.
Part 1 Quick Recap
LaunchDescription = Container
Action = Thing that gets executed
Node = Action that launches a ROS node
Your launch file generates actions.
The launch manager executes them.
1. Node: The Workhorse
Node is the action that launches an actual ROS 2 node. You’ll use this constantly.
Basic Structure
from launch_ros.actions import Node
from launch import LaunchConfiguration
def generate_launch_description():
my_node = Node(
package='my_package', # ROS package name
executable='my_executable', # Executable name (not the node name!)
name='my_node_name', # Name in the ROS graph
namespace='my_namespace', # Namespace (optional)
output='screen', # Where output goes
)
return LaunchDescription([my_node])Crucial distinction:
package+executable= “Find and run this program”name= “What to call it in the ROS graph”
# Your launch file says:
Node(package='nav2_bringup', executable='nav2_node', name='nav2')
# This creates a ROS node named `/nav2`
# If you add namespace='robot':
Node(..., namespace='robot')
# Creates `/robot/nav2`Full Node Configuration
Node(
package='pkg',
executable='exe',
# Identification
name='node_name',
namespace='namespace',
# Where to find files
cwd='directory', # Working directory
# Output & Debugging
output='screen', # 'screen', 'log', None
emulate_tty=True, # Better formatting
prefix='xacro', # Prefix for the command
shell=True, # Run through shell
# Parameters
parameters=[
{'param1': 'value1'},
'/path/to/params.yaml',
LaunchConfiguration('param2'),
],
# Topic/Service Remapping
remappings=[
('/old_topic', '/new_topic'),
('service', 'new_service'),
],
# Environment Variables
env={
'MY_VAR': 'value',
'ROS_LOG_LEVEL': 'debug',
},
# Lifecycle Control
respawn=False, # Restart if it dies
respawn_delay=0.0, # Wait before restart
# Conditional Launch
condition=IfCondition('true'), # Conditional execution
# Cleanup
on_exit=LogInfo(msg='Node exited'), # What to do when it stops
)Real-World Example: Multi-Robot Setup
from launch import (
LaunchDescription,
DeclareLaunchArgument,
LaunchConfiguration,
)
from launch_ros.actions import Node
from launch.conditions import IfCondition
def generate_launch_description():
# Arguments
robot1_name = DeclareLaunchArgument('robot1', default_value='robot_1')
robot2_name = DeclareLaunchArgument('robot2', default_value='robot_2')
use_nav = DeclareLaunchArgument('nav', default_value='true')
robot1_cfg = LaunchConfiguration('robot1')
robot2_cfg = LaunchConfiguration('robot2')
nav_cfg = LaunchConfiguration('nav')
# Robot 1
robot1_motor = Node(
package='motor_controller',
executable='motor_node',
name='motors',
namespace=robot1_cfg,
output='screen',
parameters=[{
'max_velocity': 1.5,
'wheel_radius': 0.05,
}],
)
robot1_lidar = Node(
package='lidar_driver',
executable='lidar_node',
name='lidar',
namespace=robot1_cfg,
remappings=[
('/scan', [robot1_cfg, '/scan']), # Namespace the topic
],
)
# Robot 2
robot2_motor = Node(
package='motor_controller',
executable='motor_node',
name='motors',
namespace=robot2_cfg,
output='screen',
)
robot2_lidar = Node(
package='lidar_driver',
executable='lidar_node',
name='lidar',
namespace=robot2_cfg,
remappings=[
('/scan', [robot2_cfg, '/scan']),
],
)
# Navigation (optional)
nav2 = Node(
package='nav2_bringup',
executable='nav2_node',
name='nav2',
condition=IfCondition(nav_cfg),
)
return LaunchDescription([
robot1_name,
robot2_name,
use_nav,
robot1_motor,
robot1_lidar,
robot2_motor,
robot2_lidar,
nav2,
])Execution:
# Launch with defaults
ros2 launch my_pkg multi_robot.py
# Custom names
ros2 launch my_pkg multi_robot.py robot1:=bot1 robot2:=bot2 nav:=false
# Check available arguments
ros2 launch my_pkg multi_robot.py --show-args2. LifecycleNode: Nodes with Lifecycle Management
Regular Node actions have no control over node state. LifecycleNode gives you explicit state management: configuring → activating → running → deactivating → cleaningup.
from launch_ros.actions import LifecycleNode
def generate_launch_description():
# A lifecycle node (must be written to support lifecycle)
lifecycle_node = LifecycleNode(
package='my_pkg',
executable='my_lifecycle_node',
name='my_node',
namespace='robot',
output='screen',
# Lifecycle-specific
emulate_tty=True,
parameters=[
{'param1': 'value'}
],
remappings=[
('/old', '/new'),
],
)
return LaunchDescription([lifecycle_node])The Lifecycle State Machine:
[Unconfigured]
|
| ros2 lifecycle set <node> configure
v
[Inactive] (configured, not running)
|
| ros2 lifecycle set <node> activate
v
[Active] (running)
|
| ros2 lifecycle set <node> deactivate
v
[Inactive]
|
| ros2 lifecycle set <node> cleanup
v
[Unconfigured]
When to use: For nodes that need initialization, hardware setup, or graceful shutdown. Navigation stack, hardware drivers, etc.
When NOT to use: Simple processing nodes that don’t need special setup.
3. ComposableNodeContainer & LoadComposableNodes
This is for advanced scenarios where you want to load multiple nodes into a single process instead of spawning separate processes.
Why? Performance. Inter-process communication has overhead. Composable nodes communicate in-process.
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode
def generate_launch_description():
# Create a container
container = ComposableNodeContainer(
name='my_container',
namespace='robot',
package='rclcpp_components',
executable='component_container',
composable_node_descriptions=[
# Nodes run INSIDE this container
ComposableNode(
package='sensor_drivers',
plugin='sensor_drivers::LidarDriver',
name='lidar',
),
ComposableNode(
package='sensor_fusion',
plugin='sensor_fusion::PointCloudProcessor',
name='processor',
),
],
output='screen',
)
return LaunchDescription([container])Result:
Single Process (my_container)
|- lidar (LidarDriver plugin)
`- processor (PointCloudProcessor plugin)
All communication happens in-process = fast
Alternative: Load Composable Nodes Dynamically
from launch_ros.actions import LoadComposableNodes
from launch_ros.descriptions import ComposableNode
def generate_launch_description():
# Container must already be running!
load_nodes = LoadComposableNodes(
target_container='my_container',
composable_node_descriptions=[
ComposableNode(
package='my_pkg',
plugin='my_pkg::MyPlugin',
name='my_node',
),
],
)
return LaunchDescription([load_nodes])Reality check: This is advanced. Most projects don’t need composable nodes. If you’re not sure, use regular Node.
4. PushROSNamespace: Namespace Scoping
Apply a namespace to a group of nodes.
from launch.actions import GroupAction
from launch_ros.actions import PushROSNamespace, Node
def generate_launch_description():
# Everything inside this group gets '/robot' namespace
robot_group = GroupAction(
actions=[
PushROSNamespace('robot'),
# This becomes /robot/motors
Node(package='driver', executable='motor_node', name='motors'),
# This becomes /robot/lidar
Node(package='driver', executable='lidar_node', name='lidar'),
]
)
return LaunchDescription([robot_group])Equivalent to:
Node(..., name='motors', namespace='robot'),
Node(..., name='lidar', namespace='robot'),When to use: When you have many nodes that should share a namespace. Cleaner than repeating namespace= for each node.
5. SetParameter & SetParametersFromFile: Pre-Node Configuration
Parameters available to nodes before they start.
from launch.actions import SetParameter, SetParametersFromFile
from launch.substitutions import FindPackageShare, PathJoinSubstitution
def generate_launch_description():
# Set individual parameters
param1 = SetParameter(name='my_param', value=42)
param2 = SetParameter(name='debug_mode', value=True)
# Load parameters from YAML
pkg = FindPackageShare('my_pkg')
params_file = PathJoinSubstitution([
pkg,
'config',
'robot_params.yaml'
])
load_params = SetParametersFromFile(parameter_file=params_file)
return LaunchDescription([
param1,
param2,
load_params,
])YAML format:
# robot_params.yaml
robot_controller:
ros__parameters:
wheel_radius: 0.033
wheel_separation: 0.16
max_velocity: 1.5Timing is important:
SetParameter (parameters set)
↓
Node (node starts, sees parameters)
If you set parameters AFTER a node starts, the node might not see them.
Part 2 Summary: Launching ROS Nodes
You now understand:
- Node - Basic ROS node launching (90% of use cases)
- LifecycleNode - Nodes with state management
- ComposableNodeContainer - In-process node composition
- PushROSNamespace - Namespace scoping
- SetParameter - Pre-node parameter setting
The Reality of Node Launching
Here is what actually happens when you launch a node:
1. Launch file declares: Node(package='pkg', executable='exe', name='node_name')
2. Launch manager:
a) Finds the executable in 'pkg'
b) Prepares command: /opt/ros/humble/lib/pkg/exe
c) Sets up ROS environment
d) Spawns new process
3. Child process:
a) Initializes rclcpp
b) Creates node named 'node_name'
c) Starts spinning (ROS callbacks)
4. Launch file:
a) Tracks the child process
b) Shows output (if output='screen')
c) Waits for shutdown signal
Common Pitfalls
Pitfall 1: Node Won’t Start
# ❌ Wrong package or executable name
Node(package='sensor_driver', executable='lidar_node') # Can't find it
# ✅ Verify first
$ ros2 pkg prefix sensor_drivers
$ ls /opt/ros/humble/lib/sensor_drivers/Pitfall 2: Parameters Not Set
# ❌ Node starts before parameters are set
my_node = Node(package='pkg', executable='exe')
my_param = SetParameter(name='param', value=100)
LaunchDescription([my_node, my_param]) # Wrong order!
# ✅ Parameters before node
LaunchDescription([my_param, my_node]) # Right orderPitfall 3: Namespace Confusion
# ❌ What's the actual node name?
Node(package='pkg', executable='exe', name='node', namespace='robot')
# Answer: /robot/node
# ❌ But remappings might override it:
Node(
package='pkg',
executable='exe',
name='node',
namespace='robot',
remappings=[('/node', '/other_node')]
)
# The remapping is relative to the node, so: /robot/node → /robot/other_node
# Not /node → /other_node
# ✅ Check with ros2 node list
ros2 node listPitfall 4: Respawn Loops
# ❌ Node crashes and respawns forever
Node(
package='buggy_pkg',
executable='buggy_node',
respawn=True, # Will restart forever!
)
# ✅ Only use respawn for stable nodes
# Only use on nodes that are known to be stableTroubleshooting Launch
# 1. List available launch files
ros2 launch my_pkg --help
# 2. Show launch arguments
ros2 launch my_pkg my_launch.py --show-args
# 3. Show full launch description (what will be executed)
ros2 launch my_pkg my_launch.py --show
# 4. Launch with debug output
ros2 launch my_pkg my_launch.py -v
# 5. Monitor running nodes
ros2 node list
ros2 topic list
ros2 service list
ros2 param list
# 6. Check a specific node's parameters
ros2 param list /my_node
ros2 param get /my_node my_param
# 7. Check connections
ros2 topic info /my_topic
ros2 service info /my_serviceAdvanced: Custom Node Wrappers
For maximum reusability, wrap complex node configurations:
def create_lidar_driver(robot_name: str, use_sim: bool) -> Node:
"""Factory function for lidar driver node"""
return Node(
package='lidar_driver',
executable='lidar_node',
name='lidar',
namespace=robot_name,
output='screen',
parameters=[{
'frame_id': f'{robot_name}/laser',
'use_sim_time': use_sim,
}],
remappings=[
('/scan', f'/{robot_name}/scan'),
],
)
def generate_launch_description():
robot1 = create_lidar_driver('robot_1', use_sim=True)
robot2 = create_lidar_driver('robot_2', use_sim=True)
return LaunchDescription([robot1, robot2])This reduces code duplication when you have multiple instances of the same node.
Quick Reference
# Minimal Node
Node(package='pkg', executable='exe')
# Node with namespace
Node(package='pkg', executable='exe', name='node', namespace='robot')
# Node with parameters
Node(
package='pkg',
executable='exe',
parameters=[{'param': value}]
)
# Node with remapping
Node(
package='pkg',
executable='exe',
remappings=[('/old', '/new')]
)
# Node with environment
Node(
package='pkg',
executable='exe',
env={'MY_VAR': 'value'}
)
# Lifecycle node
LifecycleNode(package='pkg', executable='exe', name='node')
# Composable node container
ComposableNodeContainer(
name='container',
package='rclcpp_components',
executable='component_container',
composable_node_descriptions=[
ComposableNode(package='pkg', plugin='pkg::Plugin'),
]
)Next: Part 3: Substitutions, Conditions & Advanced Patterns →