引言
在一个典型的 ROS2 项目中,总有一个巨大的 launch/ 文件夹和一个臃肿到几乎让人望而却步的主 launch 文件。但 ROS2 的 Python launch 系统其实是一把功能强大的"瑞士军刀",远远不止于按顺序启动节点那么简单。
本文将从实战出发,深入探索几个高频活用场景:利用 LaunchConfiguration 结合 IfCondition 实现多模式启动(模拟 vs 真实硬件);利用事件机制实现节点崩溃自动重启;通过 OpaqueFunction 从 YAML 动态生成节点群;以及 launch 嵌套时的命名空间隔离技巧。读完这篇文章,你将学会如何"编排"launch 文件,让 ROS2 系统运转得更加优雅。
一、多模式启动:条件逻辑与参数配置
在实际开发中,经常需要让同一套代码在仿真环境和真实硬件上运行。为每一种模式写独立的 launch 文件会导致代码重复。ROS2 提供了 LaunchConfiguration 和 IfCondition,让一个 launch 文件轻松应对多种场景。
LaunchConfiguration 是一个占位符,在 launch 解析时不求值,只在运行时从上下文中获取真正的值。配合 DeclareLaunchArgument 声明参数,用户可以通过命令行 hardware_type:=real 动态切换模式。而 IfCondition 则像一个开关,决定某个节点或动作是否被执行。
利用 PythonExpression 还能实现更复杂的逻辑判断,例如 IfCondition(PythonExpression(['not ', is_sim])) 表示"非仿真模式"。此外,DeclareLaunchArgument 的 choices 参数可以限制用户只能输入预定义的值,避免参数错误。
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, GroupAction
from launch.conditions import IfCondition
from launch.substitutions import LaunchConfiguration, PythonExpression
from launch_ros.actions import Node
def generate_launch_description():
use_sim = LaunchConfiguration('use_sim')
hardware = LaunchConfiguration('hardware_type')
simulation_group = GroupAction(
condition=IfCondition(use_sim),
actions=[
Node(package='gazebo_ros', executable='gzserver'),
Node(package='gazebo_ros', executable='gzclient'),
Node(package='robot_sim', executable='joint_publisher'),
]
)
real_group = GroupAction(
condition=IfCondition(PythonExpression('not ', use_sim)),
actions=[
Node(package='robot_hw', executable='hardware_interface'),
Node(package='robot_hw', executable='motor_controller'),
]
)
return LaunchDescription([
DeclareLaunchArgument('use_sim', default_value='true'),
DeclareLaunchArgument('hardware_type', default_value='simulation',
choices='simulation', 'real_robot'),
simulation_group,
real_group,
])
这个示例展示了如何将一组节点打包成 GroupAction,并为整个组附加条件。运行时,通过 ros2 launch my_pkg robot.launch.py use_sim:=false 即可切换到真实硬件模式。
二、事件驱动:节点崩溃自动重启
复杂系统中,关键节点的意外退出可能导致整个任务失败。ROS2 launch 提供了 RegisterEventHandler 和 OnProcessExit 事件,可以监听进程死亡并作出反应。
注册一个事件处理器:当目标节点退出时,重新将它加入启动队列。这里的关键是,不能复用同一个 ExecuteProcess 或 Node 对象的引用 ,而应在回调中动态创建一个新的节点实例,并通过 context.extend_queue() 将其添加到执行队列。如果你希望某个脚本出错(非零返回码)时直接终止整个 launch,可以在回调里检查 event.returncode 并发射 Shutdown 事件。
from launch import LaunchDescription
from launch.actions import RegisterEventHandler
from launch.event_handlers import OnProcessExit
from launch.launch_context import LaunchContext
from launch.events import Shutdown
from launch_ros.actions import Node
def rebuild_critical_node():
return Node(package='safety', executable='watchdog', name='watchdog')
def on_exit_restart(event, context: LaunchContext):
如果进程异常退出(返回码非0)则重启,否则仅记录日志
if event.returncode != 0:
print(f"Process {event.process_name} died with code {event.returncode}, restarting...")
context.extend_queue(rebuild_critical_node())
else:
正常退出,也可以选择关闭整个系统
context.launch_service._emit_event_sync(Shutdown(reason="Watchdog exited normally"))
def generate_launch_description():
watchdog = rebuild_critical_node()
return LaunchDescription([
watchdog,
RegisterEventHandler(
OnProcessExit(target_action=watchdog, on_exit=on_exit_restart)
)
])
这种模式特别适用于保证机器人安全节点、状态监控节点的持续运行。注意,在较新版本的 ROS2 中,ExecuteProcess 已经自带了 respawn=True 属性,对于简单的进程重启可以直接使用,但事件机制提供了更灵活的定制能力,比如重启前执行清理动作或发送告警。
三、动态生成节点:从 YAML 配置到批量启动
如果你有 10 台机器人,难道要手写 10 遍相似的节点定义吗?当然不。利用 OpaqueFunction,你可以在 launch 执行阶段读取一个 YAML 或 JSON 配置文件,然后根据文件内容动态生成任意数量的节点并返回。
OpaqueFunction 允许你推迟 launch 描述的生成,直到运行时。在回调函数中,你可以读取外部配置文件、扫描 USB 设备列表、查询网络服务,然后根据这些动态信息构建节点列表。这实现了真正的零代码修改------增加或减少机器人数量只需要编辑 YAML 文件。
import yaml
from launch import LaunchDescription
from launch.actions import OpaqueFunction
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
def generate_launch_description():
return LaunchDescription([
OpaqueFunction(function=load_and_launch_nodes)
])
def load_and_launch_nodes(context):
config_path = get_package_share_directory('multi_robot') + '/config/robots.yaml'
with open(config_path, 'r') as f:
robots = yaml.safe_load(f)'robots'
nodes = \[\]
for robot in robots:
slam_node = Node(
package='slam_toolbox',
executable='async_slam_toolbox_node',
name=f'slam_{robot"id"}',
namespace=robot'namespace',
parameters=robot\['slam_params'],
remappings=('/map', f'/{robot\["namespace"}/map')]
)
nodes.append(slam_node)
if robot.get('has_navigation', True):
nav_node = Node(
package='nav2_bringup',
executable='bringup_launch.py',
name=f'nav2_{robot"id"}',
namespace=robot'namespace',
arguments=robot\['nav_params']
)
nodes.append(nav_node)
return nodes
示例中的 YAML 文件结构大致为:
robots:
- id: rover1
namespace: /rover1
slam_params: /path/to/slam_params.yaml
has_navigation: true
nav_params: /path/to/nav_params.yaml
- id: rover2
...
这种方法不仅适用于多机器人,也适用于动态数量的传感器(如根据连接的激光雷达数量自动创建节点)。它使得 launch 文件从"静态列表"进化为"自适应的编排器"。
四、模块化与命名空间隔离:避免 Include 时的冲突
当系统膨胀到需要复用其他 launch 文件时,IncludeLaunchDescription 是最好的工具。但如果多次 include 同一个子 launch(例如启动两台机器人),topic 和节点名就会冲突。解决方案是结合 GroupAction、PushRosNamespace 和 SetRemap。
关键步骤:先为每个实例压入独立的命名空间(如 PushRosNamespace('robot1')),再用 SetRemap 重映射关键的 topic(例如将所有 /cmd_vel 映射到 /robot1/cmd_vel),最后才 include 子 launch 文件。这样,子 launch 文件中的所有节点都会自动运行在指定的命名空间下,topic 也被隔离开。
这种模式也适用于参数传递:你可以在 launch_arguments 中为每个实例传递不同的参数文件路径。注意 launch_arguments 的求值发生在 include 时,而命名空间设置在运行时生效,因此顺序不可颠倒。
由于篇幅限制,此处不展示完整代码,但核心模式如下:
from launch.actions import GroupAction, PushRosNamespace, SetRemap
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
group1 = GroupAction(actions=[
PushRosNamespace('robot1'),
SetRemap(src='/cmd_vel', dst='/robot1/cmd_vel'),
IncludeLaunchDescription(
PythonLaunchDescriptionSource('robot_bringup.launch.py'),
launch_arguments={'param_file': 'robot1.yaml'}.items()
)
])
将此 GroupAction 复制两份(robot1 和 robot2),即可安全地启动两个完全独立的机器人系统,彼此不会干扰。
五、可移植的路径处理与自定义 Action 简述
最后两点简短一提:可移植路径 建议始终使用 FindPackageShare + PathJoinSubstitution,避免硬编码。例如:
PathJoinSubstitution(FindPackageShare('my_pkg'), 'config', 'params.yaml')
这能确保路径在源码空间和安装空间都能正确解析。
自定义 Action 适用于突破内置功能的场景,如 SSH 远程启动、等待 TCP 服务等。继承 Action 类并实现 execute 方法即可。社区中也有现成的 SshMachine 可供使用。
结语
ROS2 的 Python launch 系统是一个被低估的流程编排引擎。通过 LaunchConfiguration 与条件、事件回调、动态生成、命名空间隔离等手段,你可以写出可维护、可扩展、可自愈的智能启动脚本。记住两条原则:逻辑尽量参数化,路径尽量可移植。这样,你的 launch 文件就能从"又长又臭的节点列表"真正进化为"系统编排的魔法"