ROS2 Launch文件编排:从单节点启动到多节点自动化管理
Launch是ROS2里管理多节点启动的核心工具。手动逐个ros2 run启动节点,三个以上就容易乱——Launch文件一次编排,一键拉起整套系统。本文基于ROS2 Jazzy Jalisco LTS,覆盖Launch参数、条件启动、嵌套引用、生命周期管理等实战场景。
一、Launch系统概述
Launch是什么
Launch是ROS2的多节点启动编排工具,解决三个问题:
- 一键启动:一个命令拉起多个节点,不用逐个终端手动
ros2 run - 参数传递:统一管理节点参数,支持命令行覆盖和配置文件加载
- 依赖编排:控制节点启动条件、重映射、命名空间,管理节点间依赖关系
三种格式对比
ROS2支持Python、XML、YAML三种Launch文件格式:
| 特性 | Python | XML | YAML |
|---|---|---|---|
| 灵活性 | 最高,可写任意Python逻辑 | 中等,标签式声明 | 最低,纯数据描述 |
| 可读性 | 中等,需懂Python | 高,结构清晰 | 高,简洁直观 |
| 条件启动 | 支持 | 支持 | 有限 |
| 自定义逻辑 | 支持 | 不支持 | 不支持 |
| 官方推荐 | 首选推荐 | 适合简单场景 | 适合纯参数场景 |
| 文件后缀 | .launch.py |
.launch.xml |
.launch.yaml |
Python格式最灵活,是实际项目中的首选。 本文全部使用Python格式。
ROS1 Launch vs ROS2 Launch
| 对比项 | ROS1 Launch | ROS2 Launch |
|---|---|---|
| 格式 | 仅XML | Python/XML/YAML |
| 参数机制 | <arg> + <param> |
DeclareLaunchArgument + LaunchConfiguration |
| 条件启动 | <if> / <unless> |
IfCondition / UnlessCondition |
| 嵌套引用 | <include> |
IncludeLaunchDescription |
| 事件系统 | 无 | 支持on_exit等事件 |
| 生命周期管理 | 无 | 支持自动configure/activate |
| 替换机制 | $(find pkg) |
FindPackageShare + PathJoinSubstitution |
ROS2 Launch比ROS1强在可编程性和事件驱动,不再是纯声明式配置。
二、Python Launch基础
最简Launch文件
启动demo_nodes_cpp的talker和listener:
# demo_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
# 发布节点
Node(
package='demo_nodes_cpp',
executable='talker',
name='talker_node',
output='screen',
),
# 订阅节点
Node(
package='demo_nodes_cpp',
executable='listener',
name='listener_node',
output='screen',
),
])
运行方式:
# 方式1:通过包名启动(需要安装到workspace)
ros2 launch demo_launch demo_launch.py
# 方式2:直接指定文件路径(开发调试用)
ros2 launch /path/to/demo_launch.py
LaunchDescription + Node核心参数
Node是最常用的Action,关键参数:
| 参数 | 类型 | 说明 |
|---|---|---|
package |
str | ROS2包名 |
executable |
str | 可执行文件名 |
name |
str | 节点名(覆盖代码中的名字) |
namespace |
str | 命名空间 |
parameters |
list/dict | 参数列表或字典 |
remappings |
list | 话题重映射 |
output |
str | 输出目标:screen/log |
arguments |
list | 传递给可执行文件的命令行参数 |
condition |
Condition | 启动条件 |
三、Launch参数
声明与读取参数
# param_launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
def generate_launch_description():
# 声明参数,带默认值和描述
declare_freq_arg = DeclareLaunchArgument(
'publish_freq',
default_value='10',
description='发布频率(Hz)',
)
declare_use_sim_arg = DeclareLaunchArgument(
'use_sim_time',
default_value='false',
description='是否使用仿真时间',
)
return LaunchDescription([
# 声明必须放在LaunchDescription中才能被识别
declare_freq_arg,
declare_use_sim_arg,
Node(
package='demo_nodes_cpp',
executable='talker',
name='talker_node',
output='screen',
# LaunchConfiguration读取参数值
parameters=[{
'publish_freq': LaunchConfiguration('publish_freq'),
'use_sim_time': LaunchConfiguration('use_sim_time'),
}],
),
])
命令行传参
# 传递单个参数
ros2 launch my_pkg param_launch.py publish_freq:=20
# 传递多个参数
ros2 launch my_pkg param_launch.py publish_freq:=20 use_sim_time:=true
# 查看Launch文件支持的所有参数
ros2 launch my_pkg param_launch.py --show-args
--show-args输出示例:
Arguments (pass arguments as 'name:=value'):
'publish_freq':
发布频率(Hz)
(default: '10')
'use_sim_time':
是否使用仿真时间
(default: 'false')
关键点:DeclareLaunchArgument的description字段就是--show-args显示的说明,务必写清楚。
四、条件启动
IfCondition / UnlessCondition
# conditional_launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
declare_sim_arg = DeclareLaunchArgument(
'use_sim',
default_value='false',
description='是否启动仿真环境',
)
return LaunchDescription([
declare_sim_arg,
# IfCondition:条件为真时启动
Node(
package='gazebo_ros',
executable='gazebo',
name='gazebo',
output='screen',
condition=IfCondition(LaunchConfiguration('use_sim')),
),
# UnlessCondition:条件为假时启动(即use_sim=false时启动真机驱动)
Node(
package='my_robot_drivers',
executable='lidar_driver',
name='lidar_driver',
output='screen',
condition=UnlessCondition(LaunchConfiguration('use_sim')),
),
# 两种条件下都启动的节点
Node(
package='my_robot_nav',
executable='nav2_controller',
name='nav2_controller',
output='screen',
),
])
运行效果:
# 启动仿真模式:gazebo启动,lidar_driver不启动
ros2 launch my_pkg conditional_launch.py use_sim:=true
# 真机模式:lidar_driver启动,gazebo不启动
ros2 launch my_pkg conditional_launch.py use_sim:=false
IfCondition和UnlessCondition互为补充,一个场景用哪个更直观就用哪个。
五、参数文件加载
加载YAML参数文件
实际项目中参数多且需要按场景切换,YAML文件是标准做法。
参数文件config/robot_params.yaml:
robot_controller:
ros__parameters:
control_freq: 50
max_speed: 1.5
pid_kp: 0.8
pid_ki: 0.01
pid_kd: 0.05
Launch文件加载:
# params_file_launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
# 方式1:硬编码路径(不推荐,可移植性差)
# params_file = '/path/to/config/robot_params.yaml'
# 方式2:通过FindPackageShare定位包内文件(推荐)
params_file = PathJoinSubstitution([
FindPackageShare('my_robot_nav'), # 包名
'config', # 子目录
'robot_params.yaml', # 文件名
])
return LaunchDescription([
Node(
package='my_robot_nav',
executable='robot_controller',
name='robot_controller',
output='screen',
# 加载YAML参数文件
parameters=[params_file],
),
])
混合加载:文件参数 + 命令行覆盖
# 混合参数加载:YAML文件做基础,命令行参数覆盖特定值
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
declare_speed_arg = DeclareLaunchArgument(
'max_speed',
default_value='2.0',
description='最大速度覆盖值(m/s)',
)
params_file = PathJoinSubstitution([
FindPackageShare('my_robot_nav'),
'config',
'robot_params.yaml',
])
return LaunchDescription([
declare_speed_arg,
Node(
package='my_robot_nav',
executable='robot_controller',
name='robot_controller',
output='screen',
parameters=[
params_file, # 先加载YAML文件
{ # 再用字典覆盖特定参数
'max_speed': LaunchConfiguration('max_speed'),
},
],
),
])
注意:parameters列表中后面的字典会覆盖前面YAML文件中的同名参数。
六、节点重映射与命名空间
remappings:话题重映射
节点代码写死了话题名,但实际部署需要改——不改代码,用重映射解决。
# remap_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='demo_nodes_cpp',
executable='talker',
name='talker_node',
output='screen',
# 将默认的/chatter重映射到/sensor/lidar_data
remappings=[
('/chatter', '/sensor/lidar_data'),
],
),
Node(
package='demo_nodes_cpp',
executable='listener',
name='listener_node',
output='screen',
# listener也要对应重映射
remappings=[
('/chatter', '/sensor/lidar_data'),
],
),
])
namespace:命名空间隔离
多机器人场景下,同一套驱动节点需要隔离话题和TF:
# namespace_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
# 机器人A的命名空间
robot_a_nodes = [
Node(
package='my_robot_drivers',
executable='lidar_driver',
name='lidar_driver',
namespace='robot_a', # 话题变为 /robot_a/scan
output='screen',
),
Node(
package='my_robot_nav',
executable='nav2_controller',
name='nav2_controller',
namespace='robot_a', # 话题变为 /robot_a/cmd_vel
output='screen',
),
]
# 机器人B的命名空间
robot_b_nodes = [
Node(
package='my_robot_drivers',
executable='lidar_driver',
name='lidar_driver',
namespace='robot_b', # 话题变为 /robot_b/scan
output='screen',
),
Node(
package='my_robot_nav',
executable='nav2_controller',
name='nav2_controller',
namespace='robot_b', # 话题变为 /robot_b/cmd_vel
output='screen',
),
]
return LaunchDescription(robot_a_nodes + robot_b_nodes)
命名空间效果:话题/scan → /robot_a/scan,TF的base_link → /robot_a/base_link。
七、IncludeLaunchDescription
嵌套Launch文件
把复杂系统拆成多个Launch文件,再用IncludeLaunchDescription组合:
# robot_bringup_launch.py(顶层编排文件)
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
declare_sim_arg = DeclareLaunchArgument(
'use_sim',
default_value='false',
description='是否使用仿真',
)
# 引用sensor_drivers包的Launch文件
sensor_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
PathJoinSubstitution([
FindPackageShare('my_robot_drivers'),
'launch',
'sensors.launch.py',
])
),
# 向子Launch文件传递参数
launch_arguments={
'use_sim': LaunchConfiguration('use_sim'),
}.items(),
)
# 引用navigation包的Launch文件
nav_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
PathJoinSubstitution([
FindPackageShare('my_robot_nav'),
'launch',
'navigation.launch.py',
])
),
launch_arguments={
'use_sim': LaunchConfiguration('use_sim'),
}.items(),
)
return LaunchDescription([
declare_sim_arg,
sensor_launch,
nav_launch,
])
好处:每个子系统独立维护自己的Launch文件,顶层只做编排,不关心内部细节。
八、生命周期节点自动管理
在Launch中触发configure + activate
生命周期节点(Lifecycle Node)启动后处于Unconfigured状态,需要手动触发状态转换。Launch提供了EmitEvent + RegisterEventHandler来自动完成:
# lifecycle_launch.py
from launch import LaunchDescription
from launch.actions import EmitEvent, RegisterEventHandler
from launch.events import matches_action
from launch.lifecycle_event import LifecycleEventMatch
from launch_ros.actions import LifecycleNode
from launch_ros.events import ChangeState
from launch_ros.event_handlers import OnStateTransition
from lifecycle_msgs.msg import Transition
def generate_launch_description():
# 声明生命周期节点
lifecycle_node = LifecycleNode(
package='my_robot_nav',
executable='map_saver',
name='map_saver',
namespace='',
output='screen',
)
# 事件1:节点启动后自动触发configure
configure_event = EmitEvent(
event=ChangeState(
lifecycle_node_matcher=matches_action(lifecycle_node),
transition_id=Transition.TRANSITION_CONFIGURE,
)
)
# 事件2:configure完成后自动触发activate
activate_after_configure = RegisterEventHandler(
OnStateTransition(
target_lifecycle_node=lifecycle_node,
goal_state='inactive', # configure成功后进入inactive
entities=[
EmitEvent(
event=ChangeState(
lifecycle_node_matcher=matches_action(lifecycle_node),
transition_id=Transition.TRANSITION_ACTIVATE,
)
)
],
)
)
return LaunchDescription([
lifecycle_node,
configure_event,
activate_after_configure,
])
状态转换链:Unconfigured → Inactive(configure)→ Active(activate)。节点进入Active状态后才开始正常工作。
九、Launch事件系统
on_exit事件:节点退出时触发动作
某个关键节点崩溃后,需要自动重启或清理资源:
# event_launch.py
from launch import LaunchDescription
from launch.actions import ExecuteProcess, RegisterEventHandler
from launch.event_handlers import OnProcessExit
from launch_ros.actions import Node
def generate_launch_description():
# 核心节点
core_node = Node(
package='my_robot_nav',
executable='nav2_controller',
name='nav2_controller',
output='screen',
)
# 辅助节点:等核心节点退出后再启动(比如做清理)
cleanup_node = ExecuteProcess(
cmd=['ros2', 'topic', 'pub', '/system/status',
'std_msgs/msg/String', '{data: "nav_shutdown"}', '--once'],
output='screen',
)
# 注册事件:core_node退出时启动cleanup_node
on_core_exit = RegisterEventHandler(
OnProcessExit(
target_action=core_node,
on_exit=[cleanup_node],
)
)
return LaunchDescription([
core_node,
on_core_exit,
])
OnProcessExit的on_exit列表里可以放任意Action,不限于启动节点——发通知、写日志、执行脚本都行。
十、完整实战:机器人启动编排
综合运用参数、条件启动、命名空间、生命周期管理、事件系统,编排一个完整的机器人启动流程:
# robot_bringup.launch.py
import os
from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
EmitEvent,
IncludeLaunchDescription,
RegisterEventHandler,
)
from launch.conditions import IfCondition, UnlessCondition
from launch.event_handlers import OnProcessExit
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import LifecycleNode, Node
from launch_ros.events import ChangeState
from launch_ros.event_handlers import OnStateTransition
from launch_ros.substitutions import FindPackageShare
from lifecycle_msgs.msg import Transition
def generate_launch_description():
# ========== 参数声明 ==========
declare_sim_arg = DeclareLaunchArgument(
'use_sim',
default_value='false',
description='是否使用仿真环境',
)
declare_robot_ns_arg = DeclareLaunchArgument(
'robot_namespace',
default_value='robot_a',
description='机器人命名空间',
)
# ========== 参数文件路径 ==========
nav_params = PathJoinSubstitution([
FindPackageShare('my_robot_nav'),
'config',
'nav_params.yaml',
])
# ========== 仿真环境(条件启动) ==========
gazebo_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
PathJoinSubstitution([
FindPackageShare('gazebo_ros'),
'launch',
'gazebo.launch.py',
])
),
condition=IfCondition(LaunchConfiguration('use_sim')),
)
# ========== 真机传感器驱动(条件启动)==========
lidar_driver = Node(
package='my_robot_drivers',
executable='lidar_driver',
name='lidar_driver',
namespace=LaunchConfiguration('robot_namespace'),
output='screen',
condition=UnlessCondition(LaunchConfiguration('use_sim')),
)
camera_driver = Node(
package='my_robot_drivers',
executable='camera_driver',
name='camera_driver',
namespace=LaunchConfiguration('robot_namespace'),
output='screen',
condition=UnlessCondition(LaunchConfiguration('use_sim')),
)
# ========== 控制器节点 ==========
controller = Node(
package='my_robot_nav',
executable='nav2_controller',
name='nav2_controller',
namespace=LaunchConfiguration('robot_namespace'),
output='screen',
parameters=[nav_params],
)
# ========== 生命周期节点:地图保存 ==========
map_saver = LifecycleNode(
package='my_robot_nav',
executable='map_saver',
name='map_saver',
namespace=LaunchConfiguration('robot_namespace'),
output='screen',
)
# 自动触发 configure 和 activate
configure_map_saver = EmitEvent(
event=ChangeState(
lifecycle_node_matcher=lambda node: node.node_name == 'map_saver',
transition_id=Transition.TRANSITION_CONFIGURE,
)
)
activate_after_configure = RegisterEventHandler(
OnStateTransition(
target_lifecycle_node=map_saver,
goal_state='inactive',
entities=[
EmitEvent(
event=ChangeState(
lifecycle_node_matcher=lambda node: node.node_name == 'map_saver',
transition_id=Transition.TRANSITION_ACTIVATE,
)
)
],
)
)
# ========== 事件:控制器退出时记录日志 ==========
on_controller_exit = RegisterEventHandler(
OnProcessExit(
target_action=controller,
on_exit=[
ExecuteProcess(
cmd=['ros2', 'topic', 'pub', '/system/status',
'std_msgs/msg/String',
'{data: "controller_shutdown"}', '--once'],
output='screen',
),
],
)
)
return LaunchDescription([
# 参数声明
declare_sim_arg,
declare_robot_ns_arg,
# 仿真环境
gazebo_launch,
# 真机驱动
lidar_driver,
camera_driver,
# 控制器
controller,
# 生命周期节点
map_saver,
configure_map_saver,
activate_after_configure,
# 事件处理
on_controller_exit,
])
启动依赖关系图:
十一、常见问题
Q1:Launch文件修改后不生效?
每次修改Launch文件都要重新colcon build太慢。开发阶段用--symlink-install创建符号链接,修改后直接生效:
colcon build --symlink-install
source install/setup.bash
ros2 launch my_pkg my_launch.py
Q2:节点启动顺序问题?
Launch不保证节点按声明顺序启动——所有节点几乎同时启动。如果B依赖A先就绪,有两种方案:
- 方案1:在B的代码里用
wait_for_service或wait_for_topic等待A就绪 - 方案2:用
OnProcessExit事件,A退出或A输出特定内容后再启动B
不要依赖Launch的声明顺序来控制启动顺序。
Q3:参数文件路径找不到?
FindPackageShare找不到包,报错package not found。排查步骤:
# 1. 确认包已编译安装
colcon list # 查看workspace中的包
# 2. 确认install目录中有share文件夹
ls install/my_robot_nav/share/my_robot_nav/
# 3. 确认source了setup.bash
echo $ROS_PACKAGE_PATH
# 4. 用ros2 pkg prefix确认路径
ros2 pkg prefix my_robot_nav
最常见的原因是忘了source install/setup.bash。
十二、总结
Launch文件是ROS2系统集成的核心工具,掌握以下要点就够了:
| 功能 | 关键API | 一句话 |
|---|---|---|
| 启动节点 | Node |
指定package+executable |
| 声明参数 | DeclareLaunchArgument |
带默认值和描述 |
| 读取参数 | LaunchConfiguration |
运行时替换 |
| 条件启动 | IfCondition / UnlessCondition |
根据参数决定是否启动 |
| 加载参数文件 | PathJoinSubstitution + FindPackageShare |
定位包内YAML文件 |
| 话题重映射 | remappings |
不改代码改话题名 |
| 命名空间 | namespace |
多机器人隔离 |
| 嵌套引用 | IncludeLaunchDescription |
组合子Launch文件 |
| 生命周期管理 | EmitEvent + ChangeState |
自动configure/activate |
| 事件处理 | OnProcessExit |
节点退出时触发动作 |
速查命令:
# 启动Launch文件
ros2 launch <pkg> <file.launch.py>
# 直接指定路径启动
ros2 launch /path/to/file.launch.py
# 传参
ros2 launch <pkg> <file.launch.py> arg:=value
# 查看参数
ros2 launch <pkg> <file.launch.py> --show-args
# 开发阶段符号链接
colcon build --symlink-install
本文首发于linuxros.cn,转载请注明出处。