Skip to main content

Chapter 1.8: Launch Files and Parameter Management

Learning Objectives

By the end of this chapter, students will be able to:

  • Create and configure launch files for complex robot systems
  • Manage parameters using YAML files and command-line arguments
  • Implement conditional node launching and remapping
  • Use launch arguments for flexible system configuration
  • Apply best practices for system deployment and configuration

Introduction

In complex robot systems, manually starting multiple nodes with appropriate configurations becomes impractical. Launch files provide a way to start multiple nodes simultaneously with predefined configurations, making system deployment and testing more manageable. Parameter management allows for runtime configuration of node behavior without recompilation, enabling flexible and adaptable robot systems.

In physical AI systems, proper launch and parameter management is crucial for deploying robots in different environments and configurations. This chapter will cover how to create robust launch files and manage parameters effectively in ROS 2.

Launch Files: Organizing Robot Systems

Basic Launch File Structure

Launch files are Python scripts that define how to start nodes and configure the system:

# launch/basic_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
import os


def generate_launch_description():
"""Generate the launch description for basic robot system."""
return LaunchDescription([
# Simple node launch
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim',
output='screen'
),

# Node with remapping
Node(
package='turtlesim',
executable='turtle_teleop_key',
name='teleop',
remappings=[
('/turtle1/cmd_vel', '/cmd_vel')
],
output='screen'
)
])

Advanced Launch File with Parameters

# launch/robot_system.launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, SetEnvironmentVariable
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare


def generate_launch_description():
"""Generate launch description with configurable parameters."""

# Declare launch arguments
namespace_launch_arg = DeclareLaunchArgument(
'namespace',
default_value='robot1',
description='Namespace for the robot nodes'
)

config_file_launch_arg = DeclareLaunchArgument(
'config_file',
default_value=PathJoinSubstitution([
FindPackageShare('my_robot_package'),
'config',
'robot_config.yaml'
]),
description='Path to configuration file'
)

# Get launch configurations
namespace = LaunchConfiguration('namespace')
config_file = LaunchConfiguration('config_file')

return LaunchDescription([
# Declare launch arguments
namespace_launch_arg,
config_file_launch_arg,

# Robot controller node
Node(
package='my_robot_package',
executable='robot_controller',
name='robot_controller',
namespace=namespace,
parameters=[config_file],
output='screen',
respawn=True, # Restart if node crashes
respawn_delay=5 # Wait 5 seconds before restarting
),

# Sensor processor node
Node(
package='my_robot_package',
executable='sensor_processor',
name='sensor_processor',
namespace=namespace,
parameters=[config_file],
output='screen'
)
])

Parameter Management

Using YAML Configuration Files

YAML files provide a structured way to configure parameters:

# config/robot_config.yaml
/**: # Applies to all nodes
ros__parameters:
use_sim_time: false
log_level: 'info'

robot_controller: # Applies to robot_controller node
ros__parameters:
max_linear_velocity: 0.5
max_angular_velocity: 1.0
safety_distance: 0.5
control_frequency: 10.0
battery_threshold: 20
navigation_mode: 'auto'

sensor_processor: # Applies to sensor_processor node
ros__parameters:
scan_topic: '/scan'
processing_rate: 5.0
filter_outliers: true
outlier_threshold: 2.0
min_distance: 0.1
max_distance: 10.0

Loading Parameters from Multiple Sources

# launch/parameter_demo.launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, GroupAction
from launch.substitutions import LaunchConfiguration, TextSubstitution
from launch_ros.actions import Node, PushRosNamespace
from ament_index_python.packages import get_package_share_directory
import os


def generate_launch_description():
"""Launch file demonstrating multiple parameter sources."""

# Declare launch arguments
use_sim_time_arg = DeclareLaunchArgument(
'use_sim_time',
default_value='false',
description='Use simulation time'
)

robot_name_arg = DeclareLaunchArgument(
'robot_name',
default_value='default_robot',
description='Name of the robot'
)

# Get configurations
use_sim_time = LaunchConfiguration('use_sim_time')
robot_name = LaunchConfiguration('robot_name')

# Get package share directory
pkg_share = get_package_share_directory('my_robot_package')

return LaunchDescription([
use_sim_time_arg,
robot_name_arg,

# Group with namespace
GroupAction(
actions=[
PushRosNamespace(robot_name),

# Node with multiple parameter sources
Node(
package='my_robot_package',
executable='parameter_demo_node',
name='parameter_demo',
parameters=[
# 1. Default parameters file
os.path.join(pkg_share, 'config', 'default_params.yaml'),

# 2. Robot-specific parameters
os.path.join(pkg_share, 'config', f'{robot_name}.yaml'),

# 3. Runtime parameters from launch args
{'use_sim_time': use_sim_time},
{'robot_name': robot_name.perform(None)}, # Evaluate launch config

# 4. Inline parameters
{
'startup_delay': 2.0,
'debug_mode': True
}
],
output='screen'
)
]
)
])

Parameter Validation and Handling

# my_robot_package/nodes/parameter_validated_node.py
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import ParameterDescriptor, ParameterType


class ParameterValidatedNode(Node):
def __init__(self):
super().__init__('parameter_validated_node')

# Define parameter descriptors with validation
velocity_descriptor = ParameterDescriptor(
type=ParameterType.PARAMETER_DOUBLE,
description='Maximum linear velocity',
additional_constraints='Must be between 0.0 and 5.0 m/s',
floating_point_range=[{
'from_value': 0.0,
'to_value': 5.0,
'step': 0.1
}]
)

# Declare parameters with descriptors
self.declare_parameter('max_linear_velocity', 0.5, velocity_descriptor)
self.declare_parameter('control_frequency', 10.0)
self.declare_parameter('robot_name', 'default_robot')

# Get parameter values
self.max_linear_vel = self.get_parameter('max_linear_velocity').value
self.control_freq = self.get_parameter('control_frequency').value
self.robot_name = self.get_parameter('robot_name').value

# Validate parameters
if not (0.0 <= self.max_linear_vel <= 5.0):
self.get_logger().warn(
f'Invalid max_linear_velocity: {self.max_linear_vel}, '
f'clamping to valid range [0.0, 5.0]'
)
self.max_linear_vel = max(0.0, min(5.0, self.max_linear_vel))
# Update parameter with clamped value
self.set_parameters([rclpy.parameter.Parameter(
'max_linear_velocity',
rclpy.Parameter.Type.DOUBLE,
self.max_linear_vel
)])

self.get_logger().info(f'Node initialized with parameters:')
self.get_logger().info(f' Robot name: {self.robot_name}')
self.get_logger().info(f' Max linear velocity: {self.max_linear_vel}')
self.get_logger().info(f' Control frequency: {self.control_freq}')

def on_parameter_event(self, parameter_list):
"""Handle parameter changes at runtime."""
for param in parameter_list:
if param.name == 'max_linear_velocity':
if 0.0 <= param.value.double_value <= 5.0:
self.max_linear_vel = param.value.double_value
self.get_logger().info(
f'Updated max_linear_velocity to: {self.max_linear_vel}'
)
else:
self.get_logger().warn(
f'Invalid max_linear_velocity: {param.value.double_value}, '
f'rejecting change'
)
# Revert to current valid value
self.set_parameters([rclpy.parameter.Parameter(
'max_linear_velocity',
rclpy.Parameter.Type.DOUBLE,
self.max_linear_vel
)])
return SetParametersResult(successful=True)


def main(args=None):
rclpy.init(args=args)
node = ParameterValidatedNode()

# Add parameter callback
node.add_on_set_parameters_callback(node.on_parameter_event)

try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Advanced Launch Features

Conditional Node Launching

# launch/conditional_launch.py
from launch import LaunchDescription, LaunchCondition
from launch.actions import DeclareLaunchArgument, OpaqueFunction
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from launch.conditions import IfCondition, UnlessCondition


def launch_setup(context, *args, **kwargs):
"""Setup nodes based on launch arguments."""

# Get launch configurations
use_sim = LaunchConfiguration('use_simulation', default='false')
enable_debug = LaunchConfiguration('enable_debug', default='false')

nodes = []

# Add simulation-specific nodes
if use_sim.perform(context) == 'true':
nodes.append(
Node(
package='gazebo_ros',
executable='spawn_entity.py',
arguments=[
'-entity', 'robot',
'-topic', 'robot_description',
'-x', '0', '-y', '0', '-z', '0.5'
],
output='screen'
)
)

# Add robot controller (always)
robot_controller = Node(
package='my_robot_package',
executable='robot_controller',
name='robot_controller',
parameters=[{'use_sim_time': use_sim}],
output='screen'
)
nodes.append(robot_controller)

# Add debug nodes conditionally
if enable_debug.perform(context) == 'true':
nodes.append(
Node(
package='rviz2',
executable='rviz2',
name='rviz',
arguments=['-d', 'path/to/config.rviz'],
output='screen'
)
)

return nodes


def generate_launch_description():
"""Generate launch description with conditional nodes."""

return LaunchDescription([
# Declare launch arguments
DeclareLaunchArgument(
'use_simulation',
default_value='false',
description='Use simulation environment'
),
DeclareLaunchArgument(
'enable_debug',
default_value='false',
description='Enable debug tools like RViz'
),

# Opaque function to conditionally add nodes
OpaqueFunction(function=launch_setup)
])

Node Remapping and Namespacing

# launch/remapping_demo.launch.py
from launch import LaunchDescription
from launch.actions import GroupAction
from launch_ros.actions import Node, PushRosNamespace, Remap


def generate_launch_description():
"""Launch file demonstrating remapping and namespacing."""

return LaunchDescription([
# Group with namespace
GroupAction(
actions=[
PushRosNamespace('robot1'),

# Robot 1 nodes
Node(
package='my_robot_package',
executable='sensor_processor',
name='sensor_processor',
remappings=[
('/scan', 'front_scan'), # Remap topic
('/imu', 'imu_data'),
],
parameters=[{'sensor_topic': 'front_scan'}],
output='screen'
),

Node(
package='my_robot_package',
executable='robot_controller',
name='robot_controller',
output='screen'
)
]
),

# Another group with different namespace
GroupAction(
actions=[
PushRosNamespace('robot2'),

# Robot 2 nodes (same executables, different namespace)
Node(
package='my_robot_package',
executable='sensor_processor',
name='sensor_processor',
remappings=[
('/scan', 'front_scan'), # Remap to different topic in namespace
('/imu', 'imu_data'),
],
parameters=[{'sensor_topic': 'front_scan'}],
output='screen'
),

Node(
package='my_robot_package',
executable='robot_controller',
name='robot_controller',
output='screen'
)
]
),

# Coordinator node that communicates with both robots
Node(
package='my_robot_package',
executable='coordinator',
name='coordinator',
remappings=[
('/robot1/cmd_vel', '/robot1/cmd_vel'),
('/robot2/cmd_vel', '/robot2/cmd_vel'),
],
output='screen'
)
])

Launch File Best Practices

Modular Launch Files

# launch/robot_bringup.launch.py
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import PathJoinSubstitution
from launch_ros.substitutions import FindPackageShare


def generate_launch_description():
"""Modular launch file that includes other launch files."""

return LaunchDescription([
# Include sensor drivers launch
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
PathJoinSubstitution([
FindPackageShare('my_robot_package'),
'launch',
'sensors.launch.py'
])
])
),

# Include navigation stack launch
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
PathJoinSubstitution([
FindPackageShare('my_robot_package'),
'launch',
'navigation.launch.py'
])
])
),

# Include robot controller launch
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
PathJoinSubstitution([
FindPackageShare('my_robot_package'),
'launch',
'controller.launch.py'
])
])
)
])

Error Handling in Launch Files

# launch/robust_launch.py
from launch import LaunchDescription, LaunchService
from launch.actions import RegisterEventHandler, LogInfo
from launch.event_handlers import OnProcessExit, OnProcessStart
from launch_ros.actions import Node


def generate_launch_description():
"""Launch file with error handling and logging."""

# Define nodes
robot_controller = Node(
package='my_robot_package',
executable='robot_controller',
name='robot_controller',
output='screen'
)

sensor_processor = Node(
package='my_robot_package',
executable='sensor_processor',
name='sensor_processor',
output='screen'
)

return LaunchDescription([
# Log when nodes start
RegisterEventHandler(
OnProcessStart(
target_action=robot_controller,
on_start=[
LogInfo(msg="Robot controller started successfully")
]
)
),

# Log when nodes exit
RegisterEventHandler(
OnProcessExit(
target_action=robot_controller,
on_exit=[
LogInfo(msg="Robot controller exited"),
LogInfo(msg=["Robot controller exited with code: ",
str(LaunchService.EXIT_CODE)]),
]
)
),

# Launch the nodes
robot_controller,
sensor_processor
])

Parameter Management Best Practices

Hierarchical Parameter Organization

# config/hierarchical_params.yaml
# System-wide parameters
/**:
ros__parameters:
use_sim_time: false
system_log_level: 'info'

# Robot-specific parameters
robot1:
ros__parameters:
robot_id: 'robot1'
max_speed: 0.5
safety_mode: true

robot1.robot_controller:
ros__parameters:
control_mode: 'velocity'
pid_gains:
linear: [1.0, 0.1, 0.05]
angular: [2.0, 0.2, 0.1]

robot1.sensor_processor:
ros__parameters:
scan_topic: 'front_scan'
processing_rate: 10.0

# Navigation stack parameters
robot1.nav2_stack:
ros__parameters:
planner_frequency: 1.0
controller_frequency: 20.0
use_sim_time: false

Runtime Parameter Management

# my_robot_package/nodes/dynamic_param_client.py
import rclpy
from rclpy.node import Node
from rclpy.parameter import Parameter
from rclpy.executors import MultiThreadedExecutor
import time


class DynamicParamClient(Node):
def __init__(self):
super().__init__('dynamic_param_client')

# Create a client to modify parameters of another node
self.cli = self.create_client(
SetParameters,
'/robot_controller/set_parameters'
)

# Wait for service
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Waiting for parameter service...')

# Timer to periodically change parameters
self.timer = self.create_timer(5.0, self.change_parameters)

def change_parameters(self):
"""Change parameters at runtime."""
# Create parameter change request
param_change = Parameter()
param_change.name = 'max_linear_velocity'
param_change.value = 0.8 # New value

# Send parameter change request
future = self.cli.call_async(
SetParameters.Request(parameters=[param_change])
)
future.add_done_callback(self.param_change_callback)

def param_change_callback(self, future):
"""Handle parameter change response."""
try:
response = future.result()
if response.results[0].successful:
self.get_logger().info('Parameter changed successfully')
else:
self.get_logger().error(
f'Failed to change parameter: {response.results[0].reason}'
)
except Exception as e:
self.get_logger().error(f'Error changing parameter: {e}')


def main(args=None):
rclpy.init(args=args)
client = DynamicParamClient()

executor = MultiThreadedExecutor()
executor.add_node(client)

try:
executor.spin()
except KeyboardInterrupt:
pass
finally:
client.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Hands-On Exercise: Complete System Launch

Objective

Create a complete robot system with launch files and parameter management.

Prerequisites

  • ROS 2 Humble installed
  • Understanding of ROS 2 nodes and packages
  • Basic knowledge of launch files

Steps

  1. Create a new ROS 2 package called robot_system_demo
  2. Implement nodes for:
    • Sensor processing with configurable parameters
    • Robot control with dynamic parameter adjustment
    • System monitoring
  3. Create multiple launch files:
    • A base launch file for core functionality
    • A simulation launch file with Gazebo
    • A real robot launch file
  4. Create parameter files for different configurations
  5. Implement parameter validation and runtime adjustment
  6. Test the system with different configurations

Expected Result

Students will have a complete robot system with modular launch files and comprehensive parameter management.

Assessment Questions

Multiple Choice

Q1: What is the primary purpose of launch files in ROS 2?

  • a) To compile source code
  • b) To start multiple nodes with predefined configurations
  • c) To define message types
  • d) To create documentation
Details

Click to reveal answer Answer: b
Explanation: Launch files provide a way to start multiple nodes simultaneously with predefined configurations, making system deployment more manageable.

Short Answer

Q2: Explain the difference between parameters loaded from YAML files and parameters passed as launch arguments.

Practical Exercise

Q3: Create a launch file that starts a robot system with the following requirements: (1) Uses a namespace based on a launch argument, (2) Loads parameters from a YAML file, (3) Conditionally launches debug tools based on another launch argument, (4) Remaps topics appropriately for the namespace.

Further Reading

  1. "ROS 2 Launch System Guide" - Comprehensive guide to launch files
  2. "Parameter Management in ROS 2" - Best practices for parameter handling
  3. "Robot System Architecture" - Designing scalable robot systems

Summary

In this chapter, we've explored launch files and parameter management in ROS 2, learning how to organize complex robot systems and manage their configurations effectively. We've covered basic and advanced launch file structures, parameter validation, conditional launching, and best practices for system deployment.

Proper launch and parameter management is crucial for deploying physical AI systems in different environments and configurations. The modular approach allows for flexible system deployment while maintaining consistent behavior across different scenarios. Understanding these concepts is essential for creating robust and maintainable robot systems.

In the next chapter, we'll explore URDF (Unified Robot Description Format) for describing robots, which is fundamental for simulation, visualization, and control in humanoid robotics.