本节摘要:节点多了以后,"开十个终端各敲一条命令"就成了生产事故的前奏。Launch 是 ROS2 的声明式编排系统:描述谁被启动、带什么参数、失败怎么办,而不是像 shell 脚本那样一步步指挥。本节覆盖动作、事件、条件三类机制,给一个可直接套用的完整模板,并讲清它与 4.1 生命周期编排的分工——后者是本章 4.4 雪崩复盘的排查地图。
很多团队的第一代启动方案是一个 bash 脚本:起驱动、sleep 几秒、起识别、再 sleep、起导航。它能跑,但把所有系统级问题都藏在了时序赌注里:sleep 5 秒够吗?驱动崩了导航知道吗?参数要改是不是得登机器改脚本?Launch 用声明式回答这三个问题:依赖用事件表达而不是用 sleep 赌,进程退出有事件可订阅而不是靠 ps 猜,配置是文件里的一等公民而不是散落的命令行。
Launch 的世界观很简单:一切都是动作(Action),动作发事件(Event),条件(Condition)决定动作是否执行。启动一个节点是动作,注册事件处理是动作,声明参数也是动作。理解了这个统一模型,语法只是细节。
# launch/my_bringup.launch.py from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, LogInfo, RegisterEventHandler from launch.conditions import IfCondition from launch.event_handlers import OnProcessStart, OnProcessExit from launch.substitutions import LaunchConfiguration from launch_ros.actions import Node def generate_launch_description(): use_sim = LaunchConfiguration('use_sim') camera = Node( package='my_drivers', executable='camera_node', name='camera', output='screen', parameters=[{'frame_id': 'optical_frame'}], ) detector = Node( package='my_vision', executable='detector', name='detector', output='screen', # 条件:仅仿真模式下启动这个仿真识别节点 condition=IfCondition(use_sim), parameters=[{'use_sim_time': use_sim}], ) # 事件:相机进程就绪后,打印提示并拉起处理器 on_camera_up = RegisterEventHandler( OnProcessStart(target_action=camera, on_start=[LogInfo(msg='相机已就绪,启动处理器')]), ) # 事件:检测器退出时执行善后 on_detector_down = RegisterEventHandler( OnProcessExit(target_action=detector, on_exit=[LogInfo(msg='检测器退出,检查日志')]), ) return LaunchDescription([ DeclareLaunchArgument('use_sim', default_value='false'), camera, on_camera_up, detector, on_detector_down, ])
这段模板覆盖了八成日常需求,三个细节值得指出。启动顺序不再靠 sleep:OnProcessStart 事件在进程真正就绪时触发,比任何固定延时都可靠;跨节点的语义级就绪(配置完成、数据可发)则交给 4.1 的生命周期机制。LaunchConfiguration 是惰性求值的:它先以占位符身份流入各处,执行时才解析,所以可以安全地作为参数值传给节点。output screen 别忘:不写它,节点日志进的是日志文件,现场排障时你会对着黑终端怀疑人生。

3.3 节的参数注入在 Launch 里有三种正规入口,按优先级从低到高:节点代码默认值、Launch 里 parameters 列表、外部 YAML。parameters 列表可以混装字典与文件路径:
detector = Node( package='my_vision', executable='detector', parameters=[ {'confidence': 0.6}, # 直接字典 PathJoinSubstitution([pkg_share, # 安装目录下的配置文件 'config', 'detector.yaml']), ], )
pkg_share 的写法值得单独记忆——配置文件要跟着包的安装走,用 get_package_share_directory 拼路径,别写绝对路径。这条纪律的直接受益在第 8 章容器化部署时兑现:包搬进容器,配置自动跟着走。命名空间与重映射也是同一节点的属性:namespaces 参数让同一份代码跑出多实例(多机场景的常规操作),remappings 解决话题名对不上——下游要 /scan 而驱动发的是 /laser_scan,一行重映射就能接上,不必改任何一方的代码。
4.1 节说 Nav2 的节点要"先全部 configure 再按序 activate",这个编排就是 Launch 与 lifecycle 的接力:lifecycle_manager 节点(一个普通 Launch 节点)持有节点清单与顺序,在系统启动后逐个调用它们的迁移服务。你在自己的系统里可以直接复用这个模式,不必手写迁移脚本:
lifecycle_mgr = Node( package='nav2_lifecycle_manager', executable='lifecycle_manager', parameters=[{ 'node_names': ['map_server', 'planner_server', 'controller_server'], 'autostart': True, # 拉起后自动按序 configure 并 activate 'bond_timeout': 4.0, # 存活监听,节点意外退出会触发系统级响应 }], )
bond_timeout 是个容易被忽略的保险丝:每个被管理的节点要定期向管理器报平安,超时未报,管理器知道系统不完整了。4.4 的雪崩复盘里,正是这类机制让故障从静默变成了可发现。
⚠️ 常见坑:Launch 文件里的事件处理器不会被自动触发去处理"已经发生"的事件——事件注册晚于事件发生就错过。所以 RegisterEventHandler 要放在被监听节点之后、依赖它的节点之前,顺序在 LaunchDescription 列表里是有意义的。
工具都齐了。下一节进入本章的正题:一次真实的启动链路雪崩,看看这些机制在故障现场如何逐一现身。