ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

ROS2 Launch实战:编排节点、传参与排障,告别多终端地狱

ROS2 Launch实战:编排节点、传参与排障,告别多终端地狱 在ROS2里跑一个稍微完整一点的功能包基本就是“终端地狱”左边终端起仿真、右边终端起控制器、后面还得再来一个开Rviz每个终端先source再敲ros2 run节点一多顺序错一个就全乱套。launch就是专门收拾这个局面的工具用一个启动脚本把一类功能涉及的所有节点、参数、启动顺序和条件判断一次性编排起来。这篇笔记来自我的ROS2实战系列第4.6节把它讲透launch格式怎么选、核心API怎么用、命令行传参怎么写、改了文件要不要重新colcon build以及排查launch启动失败时值得复现的完整思路。适合刚接触launch、或者已经被多终端启动折磨过一阵子的ROS2开发者。1. 为什么launch是ROS2的“总控台”手动开终端的坑我替你踩过了1.1 没有launch时的“多终端噩梦”先还原一下没有launch的日常。想跑一个小乌龟教学例程你得开两个终端# 终端1 source /opt/ros/humble/setup.bash source install/setup.bash ros2 run turtlesim turtlesim_node# 终端2 source /opt/ros/humble/setup.bash source install/setup.bash ros2 run turtlesim turtle_teleop_key两个节点尚且如此真正干活时的组合拳是robot_state_publisher、joint_state_publisher、激光雷达驱动、slam_toolbox、nav2、Rviz、Gazebo、各种tf转换节点。十个上下的节点开十来个终端每个都要记忆准确的手动指令还要保证启动顺序正确——比如控制器节点如果想等机器人模型描述robot_description话题出来之后再初始化手速不够快、节点起来太早后面就是一连串报错。这种重复劳动最浪费时间的点不在于“敲命令多”而在于“状态不可复现”。今天靠手动流程跑通了明天换台机器、换个工作区同样的手动步骤又得重来一遍。launch文件就是把这些手动操作沉淀成项目资产一次写好接下里所有人都能一键复现同一套启动流程。1.2 launch真正解决的问题不只是“一键启动”如果你以为launch只是把多个ros2 run合并到一个文件里那就把它的能力看小了。实际项目中我依赖launch解决的是四类问题参数集中管理每个节点需要一堆参数有些参数来自配置文件有些需要运行时从命令行传入。launch里用LaunchConfiguration把这些参数全部暴露出来启动命令后面直接追加param:value就能覆盖默认值。顺序与条件控制launch支持事件机制比如A节点退出后再启动B节点或者只有某个参数为true时才加载Rviz。这种能力在导航、仿真这类多阶段任务里几乎是刚需。跨包复用你的包可以直接IncludeLaunchDescription引用别的包写好的launch像Gazebo的gazebo.launch.py、Nav2的tb3_simulation_launch.py无需复制粘贴只要在launch里引用并传参即可。环境一致性开发机和部署机的环境变量、工作空间路径不同把source和路径处理写进launch的逻辑里别人拿到项目只需一条ros2 launch不用再对着README里的手动步骤挨个执行。后来我养成了一个习惯任何一个功能包的README最前面一定是“如何用launch启动”而不是“如何手动敲三条命令”。这就是把经验固化成工具的思维方式。2. Python、XML、YAML三种launch格式为什么社区默认选Python2.1 三种格式的直观对比ROS2的launch系统不像ROS1那样只有XML这一种形态它同时支持Python、XML、YAML三种格式。先说结论我几乎只写Python格式的launch除非项目里明确已有XML/YAML历史文件需要维护。三种格式放到一张表里看最清楚维度Python.launch.pyXML.launch.xmlYAML.launch.yaml表达能力强本质是Python代码可写循环、条件、import中适合固定结构中适合静态配置动态生成节点可用循环批量生成Node不支持得逐个写不支持得逐个写调试体验可print、try/except、断点报错不直观报错不直观社区生态官方文档与新包默认格式ROS1迁移过来的存量较多相对少上手成本需要一点Python基础标签学一遍就好语法稍简洁但资料少你在官网仓库、Nav2、Gazebo、MoveIt这些主流项目里看到的launch基本都是.launch.py后缀。比如热词里出现的tb3_simulation_launch.py、gazebo.launch.py全部是Python格式。这已经形成事实标准。2.2 为什么Python格式成了事实标准背后逻辑不复杂。launch系统本身的内部实现就是Python写的Python格式跟内部数据模型一一对应表达能力天然最强。XML和YAML能做的是“声明式描述”声明哪个节点、哪个参数、哪个include但一旦涉及“根据条件决定是否启动某组节点”、“循环生成10个同名不同namespace的节点”这类动态逻辑XML/YAML的标签会非常臃肿甚至得绕道去写复杂的条件表达式。Python格式的另一个优势是可以直接利用Python生态。我经常在launch文件顶部做这些事import os from ament_index_python.packages import get_package_share_directory然后动态拼接路径、读取环境变量、甚至调用自定义工具函数生成配置。这是纯声明式格式做不到的。还有一个很实际的点launch文件报错时Python格式可以打印完整堆栈定位到具体行而XML/YAML的解析错误往往只在终端留下很笼统的一句话。对于调试启动问题来说可读的报错比什么都重要。3. launch组件逐个拆Node、参数、条件、事件、嵌套一网打尽3.1 Nodelaunch里约九成场景的主角Node是launch_ros.actions里最核心的Action作用等价于手动执行一次ros2 run pkg exe但参数控制精细得多。一个典型写法from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( packageturtlesim, executableturtlesim_node, namemy_turtle, namespacesim1, parameters[{background_r: 200}], remappings[(/turtle1/cmd_vel, /cmd_vel_remap)], outputscreen, ), ])各参数含义package功能包名必须已build并在环境中source。executable可执行文件在CMakeLists或setup.py里注册过的程序名。name覆盖节点名。注意它不是可执行文件名而是ROS图中显示的节点名称同名节点会产生冲突同包多次启动时很有用。namespace给节点加命名空间同一套节点在多个机器人上复用时避免话题冲突的关键。parameters两种形式字典{key: value}或者YAML配置文件路径。多个参数源会按顺序合并后面覆盖前面。remappings话题重映射列表格式是[(old, new)]或[old:new]。outputscreen把stdout输出到终端log写进日志文件both两者都要。arguments传给executable的命令行参数不是ROS参数用法别混淆。output这个字段我补一句经验开发调试期务必设成outputscreen否则节点里的printf/LOG_INFO都进了日志文件终端看到一片安静排查问题要多绕一步。3.2 参数声明让launch从“写死”变“可调”一个launch文件如果所有值都写死那它只是一份启动清单复用价值有限。想让它可配置就用DeclareLaunchArgumentLaunchConfiguration这对组合。from launch import LaunchDescription from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration def generate_launch_description(): use_sim_time LaunchConfiguration(use_sim_time, defaultfalse) return LaunchDescription([ DeclareLaunchArgument( use_sim_time, default_valuefalse, descriptionWhether to use simulation time ), Node( packageturtlesim, executableturtlesim_node, parameters[{use_sim_time: use_sim_time}], ), ])这里use_sim_time先被定义为LaunchConfiguration它是一个“可替换”的对象value在launch真正执行时才被求值。DeclareLaunchArgument则负责声明这个参数是外部可传入的入口。命令行传参时ros2 launch my_pkg demo.launch.py use_sim_time:true注意:前后不能有空格这是ROS2命令行的通用规则。3.3 条件、事件与OpaqueFunctionlaunch的“程序化”开关launch不只是“顺序启动”它还能表达逻辑。条件控制常用IfCondition和UnlessConditionfrom launch.conditions import IfCondition from launch.substitutions import LaunchConfiguration Node( packagerviz2, executablerviz2, namerviz, conditionIfCondition(LaunchConfiguration(with_rviz)), )这样with_rviz:false时Rviz节点就不启动非常适合在launch里同时管理“轻量测试”和“完整可视化”两种模式。事件机制解决的是“启动顺序”。例如仿真主进程检查到机器人状态发布器退出后再执行清理命令from launch.actions import RegisterEventHandler, ExecuteProcess from launch.event_handlers import OnProcessExit RegisterEventHandler( OnProcessExit( target_actionrobot_state_publisher_node, on_exit[ExecuteProcess(cmd[echo, robot_state_publisher exited])], ) )OnProcessExit是其中一个事件处理器还有OnProcessStart、OnShutdown等。这个机制解决了我前面说的“启动顺序不可控”问题不用靠人肉sleep等待而是让节点自然触发后继动作。OpaqueFunction是更进阶的玩法——它允许你在launch执行期间调用一个Python函数根据运行时信息动态构建Action列表。比如从参数文件读取机器人名称列表循环生成一组Nodefrom launch.actions import OpaqueFunction def generate_nodes(context): names [robot1, robot2, robot3] nodes [] for n in names: nodes.append(Node(packagemy_bot, executablebot_node, namen)) return nodes LaunchDescription([ OpaqueFunction(functiongenerate_nodes), ])这类动态逻辑在XML/YAML里基本无解也是我坚持Python格式的最强理由之一。3.4 嵌套启动在launch里再include一个launch项目做大之后launch文件本身也需要模块化。IncludeLaunchDescription可以在当前launch里引用另一个launch就像函数调用另一个函数import os from launch import LaunchDescription from launch.actions import IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource from ament_index_python.packages import get_package_share_directory def generate_launch_description(): nav2_launch os.path.join( get_package_share_directory(nav2_bringup), launch, tb3_simulation_launch.py ) return LaunchDescription([ IncludeLaunchDescription( PythonLaunchDescriptionSource(nav2_launch), launch_arguments{headless: false}.items(), ), ])launch_arguments会把参数透传给被include的launch等于实现了launch之间的“函数传参”。靠这种组合方式一个项目可以由很多小的launch拼装成一个大的总入口每层只关心自己那一层职责可维护性提升一个量级。这里有个常见坑引用launch文件时最好用get_package_share_directory()去定位install目录下的路径不要写死src/...相对路径。因为发布后的正式运行环境可能根本没有src目录只有install目录。4. 从零写一个可跑的launch脚本并学会在命令行传参4.1 最小可跑launch把小乌龟一起拉起来直接写一个最小文件放在功能包的launch/目录下比如turtlesim_demo.launch.pyfrom launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( packageturtlesim, executableturtlesim_node, outputscreen, ), Node( packageturtlesim, executableturtle_teleop_key, outputscreen, ), ])启动方式ros2 launch turtlesim_demo.launch.py这条命令不需要包名直接给文件路径也行。如果你的launch文件已经放进了某个包的launch/目录并且已在setup.py里声明下一章细说还可以用包名方式启动ros2 launch package_name turtlesim_demo.launch.py整个文件的核心就是generate_launch_description()函数launch系统会调用它并期望返回一个LaunchDescription对象。文件里的Action列表就是最终要被执行的语义树。4.2 加入参数与命令替换让launch更有用最小可跑版本只能说明“能动”实际项目里更常见的场景是把参数可配置化、组合化。下面这个launch同时演示了参数声明、命名空间、ExecuteProcess命令替换import os from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, ExecuteProcess from launch.substitutions import LaunchConfiguration from launch_ros.actions import Node def generate_launch_description(): turtle_name LaunchConfiguration(turtle_name) return LaunchDescription([ DeclareLaunchArgument( turtle_name, default_valueturtle1, descriptionName of the turtle instance ), Node( packageturtlesim, executableturtlesim_node, nameturtle_name, outputscreen, ), ExecuteProcess( cmd[ros2, run, turtlesim, turtle_teleop_key], outputscreen, ), ])ExecuteProcess是另一个常用Action它可以启动任意外部程序不限于ROS2节点。比如想顺手开个Rviz或执行一段shell命令放到这里统一管理。4.3 命令行传参的两种方式写好了上面的launch之后启动时传参有两种方式。方式一在命令尾部直接覆盖launch参数ros2 launch my_pkg turtlesim_demo.launch.py turtle_name:my_turtle注意:前后绝对不能有空格。如果launch里没有对应的DeclareLaunchArgument传这个参数会直接报错。方式二用launch实现内部的参数转发上面已经见过launch_arguments{headless: false}.items()这是在被include的launch之间传递参数。如果你写了一个总入口launch它会接收用户输入再转发给多个子launch使用headless LaunchConfiguration(headless) IncludeLaunchDescription( PythonLaunchDescriptionSource(gazebo_launch), launch_arguments{headless: headless}.items(), )这么做的价值在于对外暴露一个统一入口用户只需要记住总launch的参数内部细节封装起来。4.4 ros2 launch进阶参数--show-args和--log-level接手别人写的launch最头疼的问题是不知道它支持哪些参数。--show-args参数就是解决这个痛点的ros2 launch --show-args my_pkg turtlesim_demo.launch.py它会列出当前launch能接收的所有参数名、默认值、描述。这个命令我几乎每次拿到新包时都会先跑一遍胜过翻半天文档。调试时另一个有用的是--log-levelros2 launch --log-level debug my_pkg turtlesim_demo.launch.py能输出launch框架内部的执行细节包括每个Action的实例化过程、条件求值结果等。当launch本身行为诡异、节点没按预期启动时用这个能看到框架层面发生了什么。5. “launch改完要不要colcon build”这个经典问题背后的安装机制5.1 launch文件到底算“代码”还是“配置”热词里有一个问题特别典型“launch文件修改了需要编译吗”。直接回答不需要“编译”因为Python格式的launch文件本身不是编译型代码但很多时候你确实需要重新colcon build一次才能让修改生效。这个看似矛盾的说法核心在于搞懂colcon build到底做了什么。很多初学者以为colcon build等于“编译”跟C源码需要编译一样。对C包来说build阶段确实会调用CMake编译产生二进制但对Python写的launch文件来说不存在“编译”这个过程。问题出在复制安装这一步colcon build会把源代码目录里的launch文件“安装”到install/目录下而ros2 launch在运行时默认找的是install/下的版本不是src/下的原始文件。5.2 ament_python的构建过程build到底干了什么一个典型Python ROS2包的目录结构是my_pkg/ ├── package.xml ├── setup.py ├── setup.cfg ├── resource/my_pkg ├── launch/ │ ├── demo.launch.py │ └── another.launch.py ├── my_pkg/ │ └── __init__.pysetup.py里如果声明了data_files才会把launch文件复制到install目录import os from glob import glob from setuptools import setup package_name my_pkg setup( namepackage_name, version0.0.0, packages[package_name], data_files[ (share/ament_index/resource_index/packages, [resource/ package_name]), (os.path.join(share, package_name), [package.xml]), (os.path.join(share, package_name, launch), glob(os.path.join(launch, *launch.py))), ], install_requires[setuptools], entry_points{ console_scripts: [], }, )注意最后一行data_fileslaunch目录下的*.launch.py文件会被安装到install/my_pkg/share/my_pkg/launch/下。ros2 launch my_pkg demo.launch.py查找的目标就在这里。所以如果你改了src下的launch文件但没有重新buildros2 launch仍然会跑到install目录下执行旧文件表现就是“我改了但没生效”。这不是bug是安装机制决定的。5.3 --symlink-install和普通构建的差异理解上面的机制之后就有两种优化路径。第一种普通构建colcon build --packages-select my_pkginstall目录是src文件的“副本”修改src需要重新build才能同步。第二种符号链接构建colcon build --packages-select my_pkg --symlink-install这种情况下install目录里的launch文件是指向src目录的软链接。修改src文件后启动时读到的就是修改后的内容不需要重新build。这不只对launch文件有效对Python包里的所有.py文件同样有效开发循环会变得非常快。我个人的工作习惯是普通开发调试期用--symlink-install打包发布或者换机器部署前再做一次普通colcon build确保install目录是完整的独立文件不依赖src的存在。5.4 到底什么时候需要重新build列一个实操判断清单修改内容普通buildsymlink-install修改launch文件内容已有文件需要重新build才能生效直接生效新增launch文件且setup.py已用glob匹配需要重新build需要重新build一次glob安装时已扫描修改launch文件里import的本地Python模块需要重新build需要区分情况若install里的模块是软链接则直接生效修改package.xml/CMakeLists/setup.py需要重新build需要重新buildlaunch文件根本没被glob匹配需要改setup.py后重新build需要改setup.py后重新build在这个问题上的总结是一句话launch文件不编译但需要在你当前的ROS2环境下“可用”。判断标准就是看install目录里那份文件是不是你想要的版本。不确定时直接ls install/pkg/share/pkg/launch/看看内容比靠猜快得多。6. launch跑不起来的典型套路按这条链路查问题最省时间6.1 第一梯队包都找不到节点自然起不来launch报错最常见的一类是Package my_pkg not found或者Package my_pkg not found, searching: [/opt/ros/humble]这类错误本质是环境变量没有把install目录加进ROS2的搜索路径。按顺序排查当前终端是否source过工作空间source install/setup.bash包是否真的build成功colcon build --packages-select my_pkg确认没有报错包名是否写对包名和目录名不一定一致以package.xml里的name为准是否在不同终端用了不同工作空间导致多个install互相覆盖如果多个工作空间叠加注意source顺序后面的source会覆盖前面的同名包配置非常容易出现“明明build了却还是Not found”的诡异问题。6.2 第二梯队launch文件能找到启动就报错包能找到但启动时出现[ERROR] [launch]: Caught exception in launch (see debug for traceback): ...或者更常见的FileNotFoundError: [Errno 2] No such file or directory: xxx.launch.py第一反应去检查setup.py的data_files里有没有launch目录的glob配置。很多人新建了launch/目录和launch文件但忘了在setup.py里加配置(os.path.join(share, package_name, launch), glob(os.path.join(launch, *launch.py))),加完之后需要重新colcon build这个前面已经强调过。还有一种情况是launch文件内部include了其他包的文件找不到目标路径。排查时可以直接在launch文件里加打印print(nav2_launch path:, nav2_launch)然后重新运行launch去看终端输出的路径是否存在。确认路径文件在不在比反复猜省时间。6.3 第三梯队节点启动后秒退launch本身执行成功但节点一个个起来了立刻退出。此时需要去翻日志ls ~/.ros/log/ ls ~/.ros/log/latest/终端通常会有类似输出[INFO] [launch]: all log files: ~/.ros/log/2024-01-01-12-00-00-123456~/.ros/log/latest是一个软链接指向最近一次的日志目录。查看目录下的日志文件cat ~/.ros/log/latest/launch.log节点崩溃时的核心原因、段错误位置、Python traceback都会写在这里。如果日志信息不足可以先脱离launch手动单独ros2 run那个节点看是否本身就起不来。launch的职责是编排不是修节点内部bug先手动排除单个节点故障再考虑是不是编排的问题。6.4 调试技巧把launch当成普通Python来调launch文件本质是Python所以常规Python调试手段都适用。我常用的三个办法print加在关键位置def generate_launch_description(): print( generating launch description ) return LaunchDescription([...])启动时终端会打印这些临时日志比完全黑盒强太多。用--log-level debug观察launch框架行为。当同一份launch在某些机器上正常、在某些机器上异常时开启debug日志能看出执行顺序和条件判断的实际结果差异。单独验证被include的launch。我遇到过多次问题不在外层launch、而在内层被include的launch上。这时候直接先单独启动子launch确认它能跑通外层问题才值得继续查。检查~/.ros/log/latest是整个启动过程的最后一根救命稻草。任何“看上去没报错但行为不对”的情况日志里通常都能找到线索。launch自身打出的[INFO] [launch]: all log files: ...那一行很多人忽略它就是最直接的日志入口。我个人排查launch故障的习惯是先看环境变量能否解析再确认install目录文件是否为预期版本然后开debug日志最后看日志文件。按这个顺序很少卡壳。
返回列表