写ROS2项目最烦什么对我来说不是接口对不上也不是编译报错而是每次调试都要打开一堆终端一个跑master虽然ROS2已经没有中心节点了但习惯还在、一个跑节点、一个跑rviz2手上还得留一个终端随时敲命令查话题。后来学会了launch这个烦恼直接消失大半。launch是ROS2里用来批量启动节点和配置运行环境的机制相当于把“手动开十个终端再挨个敲ros2 run”这件事收敛成一个文件、一条命令。这篇笔记就是围绕ros2 launch的实战记录适合刚学完节点和话题、准备把手动启动换成脚本启动的朋友也适合被launch文件里的各种写法绕晕的人。我自己是用鱼香ROS的教程入的门再加上啃官方文档踩了不少坑之后才把launch的套路理顺。这篇文章不打算写成文档翻译而是按我自己从零开始用launch的路径来写先理解它到底解决什么问题然后写第一个launch.py再把参数、事件、条件这些进阶用法过一遍最后把常见报错和排查技巧整理成清单。这些都是我在实际工程里验证过的内容可以直接拿去用。1. launch是什么为什么ROS2离不开它1.1 告别一条条手敲命令先说说不用launch的原始状态。假设你写了一个机器人底盘驱动节点robot_base一个激光雷达驱动节点lidar_node还有一个导航节点navigation手动启动大概是这样的# 终端1 ros2 run my_robot robot_base # 终端2 ros2 run my_robot lidar_node # 终端3 ros2 run nav2_bringup navigation这只是三个节点如果再加上参数配置、话题重映射、命名空间、条件判断命令会变得非常长。比如导航节点的启动命令有时能写好几行每加一个参数都要在原命令后面拼字符串既不直观也容易漏。launch文件解决的就是这个问题。它把“启动哪些节点”、“每个节点用什么参数”、“节点之间的启动顺序”、“要不要开rviz2”全部写进一个脚本里。之后你只需要ros2 launch my_robot my_robot.launch.py一键完成。而且launch不是简单把命令排在一起它还能管理生命周期、监听事件、传播参数是个完整的启动编排系统不是脚本拼接。1.2 ROS2 launch与ROS1 launch的差别如果你是从ROS1过来的会很容易认为launch文件就是那个XML格式的东西。ROS2里情况变了官方推荐的launch写法是Python脚本文件名通常是.launch.py虽然也支持XML和YAML格式但社区和官方示例基本都以Python为主。Python写launch的优势很明显它是真正的编程语言可以做条件判断、循环、读环境变量、处理字符串路径这些在ROS1的XML里做起来很别扭。比如你想根据环境变量决定启动哪个机器人模型XML里只能堆if语句Python里一个if os.environ.get(...)就解决了。ROS2的launch还引入了Event机制。举个例子你希望节点A退出之后自动启动节点B或者等某个服务可用了再继续这些都可以用事件注册来实现。ROS1的launch基本没有这种能力通常是靠节点自己等待或睡眠来硬撑。1.3 launch文件放在哪里新手最容易困惑的一点launch文件到底应该放到包里的哪个目录ROS2官方推荐放在包的launch目录下然后在CMakeLists.txt或setup.py里把它标记为待安装文件。对于ament_python类型的包典型的目录结构是my_robot/ ├── my_robot/ │ ├── __init__.py │ └── robot_node.py ├── launch/ │ └── my_robot.launch.py ├── package.xml ├── setup.py └── setup.cfg然后在setup.py里把launch目录加进去from setuptools import setup import os from glob import glob setup( namemy_robot, version0.0.0, packages[my_robot], data_files[ (share/ament_index/resource_index/packages, [resource/my_robot]), (share/my_robot, [package.xml]), (os.path.join(share, my_robot, launch), glob(launch/*.launch.py)), ], ... )如果你是ament_cmake类型的包则在CMakeLists.txt里安装launch文件install(DIRECTORY launch DESTINATION share/${PROJECT_NAME})这一步非常容易忘。忘了之后直接运行ros2 launch my_robot xxx.launch.py会提示找不到文件因为ROS2运行时是从install目录读取文件的不是从源码目录。2. 写一个最小可用的launch.py2.1 第一个文件启动两个节点先给一个最基础的例子启动两个节点一个是发布者一个是订阅者。from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( packagedemo_nodes_cpp, executabletalker, nametalker ), Node( packagedemo_nodes_cpp, executablelistener, namelistener ), ])把这个文件保存为demo.launch.py放在上面说的launch目录里编译安装后运行ros2 launch 你的包名 demo.launch.py如果包还没编译安装过也可以直接用绝对路径指定launch文件来测试ros2 launch 路径/demo.launch.py这里有两个细节值得注意。第一generate_launch_description这个名字不能改ROS2的launch工具会调用这个函数来获取LaunchDescription对象。第二Node里的name参数可以手动指定节点名如果不写就用可执行文件的名字。每个Node类可以通过parameters参数给节点传参数写法是Node( packagemy_robot, executablerobot_base, namerobot_base, parameters[{max_speed: 1.0, odom_frame: odom}], )这里的参数是发给节点的最终会通过ROS2参数机制注入到节点里不需要节点启动后再手动ros2 param set。2.2 从命令行参数到launch参数实际使用中你经常希望同一个launch文件既能启动仿真环境又能启动真实机器人。这时候就需要launch参数也就是launch argument不是节点参数。launch参数通过DeclareLaunchArgument声明用LaunchConfiguration读取。典型写法from launch import LaunchDescription from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration from launch_ros.actions import Node def generate_launch_description(): use_sim_time LaunchConfiguration(use_sim_time, defaulttrue) robot_name LaunchConfiguration(robot_name, defaultturtlebot) return LaunchDescription([ DeclareLaunchArgument( use_sim_time, default_valuetrue, descriptionUse simulation time or wall clock time ), DeclareLaunchArgument( robot_name, default_valueturtlebot, descriptionName of the robot ), Node( packagemy_robot, executablerobot_base, namerobot_name, parameters[{use_sim_time: use_sim_time}], ), ])运行时可以这样覆盖默认值ros2 launch my_robot robot.launch.py use_sim_time:false robot_name:my_robot这里要注意launch参数的传递语法是参数名:值不是:前带空格写错会导致launch工具把use_sim_time:false当成一个未知的action来解析。我再补充一个实用技巧如果想在launch内部读取环境变量可以用EnvironmentVariable代替LaunchConfiguration比如from launch.substitutions import EnvironmentVariable log_level EnvironmentVariable(ROS_LOG_LEVEL, default_valueinfo)这种方式适合根据运行环境自动切换调试级别。2.3 用GroupAction管理命名空间和前缀当多个节点要统一加命名空间或者统一加gazebo前缀时用GroupAction比每个节点单独写namespace更整洁。from launch.actions import GroupAction from launch_ros.actions import PushRosNamespace def generate_launch_description(): return LaunchDescription([ GroupAction([ PushRosNamespace(robot1), Node(packagemy_robot, executablerobot_base, namebase), Node(packagemy_robot, executablelidar_node, namelidar), ]), ])启动后这两个节点的完整话题名会变成/robot1/base/...和/robot1/lidar/...相当于把一组节点隔离到了独立命名空间下非常适合多机器人仿真场景。GroupAction还能配合条件判断使用后面在讲条件时会再提到。3. launch文件修改了需要编译吗答案和你想的不一样3.1 Python launch不需要编译的本质“launch文件修改了需要编译吗”这个问题我看到很多新手在问我最早也纠结过。答案分两层。如果你的launch文件是纯Python逻辑里面没有引用自定义的消息、服务、动作接口类型那么它本质是一个Python脚本。Python脚本不需要编译成机器码直接从源码读取即可。ROS2运行时读取launch文件的逻辑是launch工具根据包名找到share/目录下安装好的launch文件然后用Python解释器执行。所以从“代码是否需要编译”这个层面讲答案是不需要。但是注意如果你修改了launch文件之后没有重新安装也就是没有让install/目录里的副本更新那运行时读到的还是旧文件。在开发工作区里你需要重新执行colcon build --packages-select my_robot这样才能把修改后的launch文件同步到install目录。如果你嫌每次build慢也可以只单独安装launch文件或者用colcon build --packages-select my_robot --symlink-install用符号链接方式安装之后修改源码和launch文件都不需要重新build直接生效这在开发阶段非常推荐。3.2 ament_python包怎么处理安装我见过一种情况用ament_python创建的包setup.py里根本没写launch文件的安装规则结果launch文件虽然存在但ros2 launch就是找不到。这时候不是编译问题是打包配置问题。ament_python包安装文件靠data_files。除了launch目录通常还需要把resource目录、package.xml加进去。如果你用ros2 pkg create创建的包会自动生成一份可用的setup.py但launch目录往往不在其中需要自己加。这也是为什么很多人把launch文件放在包里却无法启动。3.3 资源路径问题才是真正的坑比编译更隐蔽的问题是launch文件里引用的其他资源路径不对。比如你要在launch里加载URDF文件from launch.substitutions import Command, FindExecutable robot_description Command([ xacro , os.path.join(pkg_share, urdf, robot.urdf.xacro), is_sim:, use_sim_time ])这里的pkg_share通常这样获取import os from ament_index_python.packages import get_package_share_directory pkg_share get_package_share_directory(my_robot)这条命令会在install/my_robot/share/my_robot下找资源。如果你的URDF文件没有通过CMake或setup.py安装到那里运行时就会报文件不存在。这和launch文件是否需要编译完全是两个问题但报错现象常常被误认为是“没有重新编译”。排查这类问题时有一个快速判断方法ros2 pkg prefix my_robot可以查看包的安装前缀然后手动检查share/my_robot目录下有没有对应文件。如果文件缺失十有八九是安装规则没写全。3.4 修改launch文件后如何验证我自己修改launch之后的常规验证顺序是这样的打开launch文件检查语法。可以先单独执行python3 你的launch文件.py如果语法有错会直接报错需要注意launch文件里用到ROS2相关的导入时直接执行可能因为环境变量没source而报ImportError这是正常的。重新编译安装对应包colcon build --packages-select my_robot或者用了--symlink-install就省略这步。重新source环境source install/setup.bash。这个也容易漏不source的话ROS2可能还在用旧环境。执行ros2 launch my_robot xxx.launch.py --show-args这条命令只打印launch支持的参数列表不会真正启动节点。如果这里能正常输出参数说明launch文件能被正确解析。最后再正式启动观察日志输出。4. 让launch更实用参数、重映射、事件与条件4.1 参数与重映射除了节点参数launch里还经常用到主题重映射。比如某个雷达节点默认发布/scan但你希望它发布到/robot1/scan或者你的导航节点订阅的是/scan_filtered需要把雷达话题映射过去。Node( packageurg_node, executableurg_node, namelidar, remappings[ (scan, robot1/scan), ], )重映射的机制是修改节点内部的topic名称映射表对通信层透明比在代码里写死话题名灵活得多。多传感器融合时用launch统一管理重映射能避免每次改代码重新编译。参数这块如果你有大量参数不建议全部写在launch文件里。官方推荐用YAML参数文件。launch里这样写Node( packagerobot_navigation, executablenavigation_node, namenavigation, parameters[os.path.join(pkg_share, config, nav_params.yaml)], )注意这里的路径必须是安装后的路径。YAML文件同样需要添加到CMake或setup.py的安装规则中。另外一个节点也可以同时加载多个参数文件后加载的同名参数会覆盖先加载的这个顺序特性有时候可以用来做“默认参数环境覆盖参数”的层级配置。4.2 事件注册启动后做什么ROS2 launch的事件机制一开始不太好懂但用熟之后非常有用。最常用的场景是启动几个核心节点之后再启动rviz2或gazebo并确保它们在其他节点之后启动更稳妥。from launch.actions import RegisterEventHandler, ExecuteProcess, TimerAction return LaunchDescription([ ..., TimerAction( period3.0, actions[ ExecuteProcess( cmd[rviz2, -d, rviz_config_path], outputscreen ) ] ), ])TimerAction是简单粗暴的延迟执行。更精细的做法是用RegisterEventHandler监听节点启动事件from launch.event_handlers import OnProcessStart RegisterEventHandler( OnProcessStart( target_actioncore_node, on_start[ ExecuteProcess(cmd[rviz2, ...], outputscreen) ] ) )它的含义是等core_node这个进程启动成功之后再去启动rviz2。这种写法比固定延迟更可靠因为如果节点启动花了5秒你只延迟3秒rviz2可能因为话题还没数据而显示空白。事件机制最复杂的部分在于不同类型事件和handler的配合但大多数情况下你只需要记住这几种OnProcessStart进程启动、OnProcessExit进程退出、TimerAction定时触发。能用这三个满足需求就已经超过大多数launch脚本了。4.3 条件判断一个launch适配不同场景launch里的条件判断用IfCondition和UnlessCondition。典型例子仿真时启动robot_state_publisher并加载URDF而不启动真实底盘驱动在真机上则相反。from launch.conditions import IfCondition, UnlessCondition from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration use_sim LaunchConfiguration(use_sim, defaulttrue) ... Node( packagegazebo_ros, executablespawn_entity.py, arguments[-topic, robot_description, -entity, my_robot], conditionIfCondition(use_sim) ), Node( packagemy_robot, executablerobot_base, conditionUnlessCondition(use_sim) )这里use_sim是字符串形式的launch参数值为true或false。IfCondition会把字符串解析成布尔值。我第一次写条件的时候犯了个错以为可以用Python的True/False直接判断结果launch参数传进来的默认是字符串导致条件永远为真。后来才理解IfCondition接收的是一个可替换对象或者字符串不是Python的布尔值。这一点对新手来说是个隐蔽的坑。5. 常见问题排查与调试经验5.1 最常见的问题找不到包、找不到launch文件launch启动失败绝大多数报错集中在下面几种报错信息可能原因解决方法Package my_robot not found没有编译包或没有source install目录colcon build --packages-select my_robotsource install/setup.bashlaunch file not found: xxx.launch.pylaunch文件没有安装到share目录检查CMakeLists.txt或setup.py是否安装launch目录ModuleNotFoundErrorlaunch文件里import了未安装的Python模块安装对应依赖或确认环境正确AttributeError: NoneType object has no attribute...launch文件里获取共享路径时包名写错检查get_package_share_directory的包名是否正确程序启动即退出且无错误输出节点运行的依赖环境不完整或动态库缺失用ros2 run单独运行该节点排查其中第一类“Package not found”最常见的原因不是包不存在而是当前终端没有source工作区。这个问题在开发多工作区时尤其明显我自己的习惯是在~/.bashrc里只source一个基础环境其他工作区需要时再手动source避免环境变量混乱。5.2 报错“package not found”怎么办如果你确认包已经编译也source了环境还是报Package not found那么排查思路是这样第一步用ros2 pkg list | grep my_robot看下当前环境能不能识别到包。如果不能说明环境变量AMENT_PREFIX_PATH没有包含该工作区的install目录。第二步检查echo $AMENT_PREFIX_PATH确认路径是否指向了你刚编译的工作区。如果指向了别的工作区或者为空那问题就出在source顺序或source路径。第三步如果ros2 pkg list能看到包但launch就是找不到那很可能不是包的问题而是launch文件路径问题。尝试直接用绝对路径运行ros2 launch /path/to/xxx.launch.py如果这样能启动就说明launch文件本身没问题问题在于launch文件在包内的安装路径不符合ROS2的搜索规则。ROS2的launch工具默认按包名到share/package_name/launch目录下找.launch.py文件如果你的文件没安装到这个固定位置它当然找不到。5.3 source和环境的坑很多人包括我刚开始开发时都经历过这样的困惑明明已经build成功了为什么运行时可执行文件还是旧版本这里有个容易被忽略的点colcon build之后必须重新source环境尤其是当你修改了包的安装路径或者新增了可执行文件时。source install/setup.bash这条命令不是可选项它的作用是把当前工作区里的包覆盖到ROS2的搜索路径中。如果你build完不source可能会遇到使用了旧的可执行文件或者根本找不到新包的情况。这种问题在多个工作区重叠时更明显比如你在基础环境里装了一个旧版本包又在自己工作区里编译了新版本如果不source自己工作区ROS2会优先加载基础环境的旧版本。调试环境问题的一个实用命令是printenv | grep -E AMENT|ROS|COLCON它会把当前环境变量中与ROS2相关的所有配置打出来比一个个echo高效很多。5.4 GUI程序rviz2、gazebo启动失败的典型原因用launch启动rviz2或gazebo时很多人会遇到“命令执行了但界面一闪而过”或者“显示黑屏”的情况。如果是黑屏大概率是rviz2启动太早话题数据还没发布出来。用TimerAction延迟几秒或者等核心节点完全启动后再启动rviz2画面就会有内容了。如果是界面一闪而过常见原因是display环境变量问题。远程连接或容器环境中$DISPLAY未设置会导致GUI程序启动失败。另外在容器里跑带界面的launch时需要把宿主机的/tmp/.X11-unix挂载进去还要设置好XAUTHORITY。这是容器化开发中一个很经典的坑。还有一类情况是launch里用了gazebo_ros的spawn_entity.py但gazebo实体服务没起来或者模型文件路径不对。实战中用ros2 service list看一下/spawn_entity服务是否存在能快速定位。5.5 快速排查技巧用好命令行工具我调试launch时最常用的四条命令# 查看launch文件的参数列表不启动节点 ros2 launch my_robot xxx.launch.py --show-args # 查看launch的完整日志输出 ros2 launch my_robot xxx.launch.py --log-level debug # 单独运行包中的可执行文件确认节点本身没毛病 ros2 run my_robot robot_base # 查看当前环境识别的包 ros2 pkg list | grep my_robot如果launch启动后节点崩溃日志里通常会有回溯信息--log-level debug会让launch系统打印更多内部信息包括它调用了哪些action、每个action的返回状态。这比自己在launch文件里加print要高效得多也更符合ROS2的日志规范。另外launch的输出日志路径也可以关注下。每次运行launch它都会提示日志存放位置像这样[INFO] [launch]: All log files can be found below /home/xxx/.ros/log/...如果节点输出信息太多冲掉了关键日志可以去这个目录找完整的输出文件或者用--log-level把不需要的模块日志关掉。6. 一套可直接复用的launch模板最后分享一个我自己用过很多次的模板它整合了前面提到的参数、命名空间、条件判断、延迟启动、YAML参数文件、URDF模型加载这些常用功能可以直接改成你自己的机器人启动脚本。import os from launch import LaunchDescription from launch.actions import DeclareLaunchArgument, TimerAction, GroupAction from launch.conditions import IfCondition from launch.substitutions import LaunchConfiguration from launch_ros.actions import Node, PushRosNamespace from ament_index_python.packages import get_package_share_directory def generate_launch_description(): pkg_share get_package_share_directory(my_robot) use_sim_time LaunchConfiguration(use_sim_time, defaulttrue) namespace LaunchConfiguration(namespace, defaultrobot1) start_rviz LaunchConfiguration(start_rviz, defaulttrue) robot_description_file os.path.join( pkg_share, urdf, robot.urdf.xacro ) robot_state_publisher Node( packagerobot_state_publisher, executablerobot_state_publisher, namerobot_state_publisher, parameters[{ use_sim_time: use_sim_time, robot_description: robot_description_file, }], ) core_group GroupAction([ PushRosNamespace(namespace), Node( packagemy_robot, executablerobot_base, namebase, parameters[os.path.join(pkg_share, config, base_params.yaml)], ), Node( packagemy_robot, executablelidar_node, namelidar, remappings[(scan, scan_filtered)], ), ]) rviz2 Node( packagerviz2, executablerviz2, namerviz2, arguments[-d, os.path.join(pkg_share, config, display.rviz)], conditionIfCondition(start_rviz), ) return LaunchDescription([ DeclareLaunchArgument( use_sim_time, default_valuetrue, descriptionUse simulation clock ), DeclareLaunchArgument( namespace, default_valuerobot1, descriptionNamespace prefix for all nodes ), DeclareLaunchArgument( start_rviz, default_valuetrue, descriptionWhether to start rviz2 ), robot_state_publisher, core_group, TimerAction(period3.0, actions[rviz2]), ])这个模板里的几个设计点值得单独说一下。robot_state_publisher的robot_description参数直接传了xacro文件路径。这里要注意ROS2有些版本要求先对xacro做预处理但大多数情况下robot_state_publisher内部会调用xacro命令解析前提是你安装了xacro包。如果你遇到URDF解析失败的问题可以先手动跑一下xacro robot.urdf.xacro看有没有报错。core_group用PushRosNamespace把两个核心节点都放进了命名空间这样话题会自动带上前缀。如果你有多台机器人只要修改namespace参数就能复用同一个launch非常方便。TimerAction(period3.0, actions[rviz2])是防止rviz2启动太早、导致初始视角没有话题数据。3秒是我在测试平台上调出来的经验值如果你的节点启动慢可以适当加大。我个人在实际调试中还发现一个细节launch文件里的outputscreen不加的话节点的printf输出不会到终端而是会被rosout日志系统捕获。为了方便观察节点日志我一般都会在每个Node里显式加上outputscreen。最后再分享一个小技巧。如果你经常要在多个launch之间切换可以在~/.bashrc里加几个alias比如alias sim_upros2 launch my_robot robot.launch.py use_sim_time:true alias bot_upros2 launch my_robot robot.launch.py use_sim_time:false start_rviz:true调试的时候少敲很多字。launch用顺手之后会形成肌肉记忆但真正花时间研究它的人并不多。这篇笔记把我自己走过的弯路和积累的技巧都写出来了希望能帮你省去一些摸索的时间。