Launch:XML/YAML 与 Python 的分工,不是信仰站队
稳定子图用声明式,条件装配用 Python;团队默认一种,迁移只换装配层。

1. 选型看变化率,不看阵营
XML/YAML launch 适合稳定、声明式的节点图:驱动、静态 TF、固定 remapping——diff 清晰,非 ROS 专家也能改参数路径。Python launch 适合要编程的场景:按环境变量选仿真/真机、循环生成多实例、运行时读 yaml 算 remapping、与 OS 交互(ExecuteProcess 拉起非 ROS 工具)。
用 Python 写「三个固定 Node」会让简单系统过度代码化——review 时要读 import 链才找到 serial_port 默认值。反过来,在 XML 里硬编码十层 if 等价物会写不下去。没有普适赢家,只有变化率与读者:谁改、多久改一次、要不要单元测试 launch 逻辑。
2. 声明式 launch 长什么样
ROS 2 常用 Python 写 launch,但叶子子系统可以用 YAML 封装为 launch/xxx.launch.yaml:
# lidar_bringup.launch.yaml
launch:
- node:
pkg: sicks300
exec: sick_node
name: lidar
param:
- name: ip_address
value: 192.168.0.10
remap:
- from: scan
to: /scan上层 Python 只负责 include:
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
def generate_launch_description():
return LaunchDescription([
IncludeLaunchDescription(
PythonLaunchDescriptionSource([
get_package_share_directory('my_robot'), 'launch', 'lidar.launch.yaml'
])),
])参数默认值在 yaml 里一眼可见——新人五分钟内能找到「激光从哪起」。
3. Python 该承担的逻辑
- 条件分支:
LaunchConfiguration('sim') == 'true'选 Gazebo 或 real driver - 多机:
GroupAction+ 命名空间循环 - 参数推导:从
robot_description解析 joint 列表生成 controller spawner
这些逻辑若塞进 XML,会变成不可测试的字符串拼接。Python 可以把纯函数抽出来 pytest——launch 逻辑也是代码,应有测试。例如 resolve_robot_name(env) 单独测,launch 文件只调用返回值,避免在 generate_launch_description 里堆 fifty 行业务 if。
4. 代价对比与混用约定
| 维度 | YAML/XML | Python |
|---|---|---|
| diff 可读性 | 高 | 中(逻辑散) |
| 表达力 | 低 | 高 |
| 非程序员可改 | 是 | 通常否 |
| 藏副作用 | 难 | 易(import 副作用) |
团队应锁定一种默认(如「新子系统先 yaml,需动态逻辑再 Python」),写进 CONTRIBUTING。例外开另一种必须在 MR 说明理由——口头约定等于没有约定,最后仓库里四种风格混战。
5. 迁移:只换装配层,不改接口
从全 Python 迁到 yaml 叶子时,节点参数文件、话题名、命名空间保持不变,只把 Node(...) 块挪进 yaml。用 launch 冒烟对比迁移前后 ros2 node list 与 ros2 topic info -v。避免「顺便」在迁移里改话题名——那是两个变更,回滚粒度混乱。
IncludeLaunchDescription 传参用 launch_arguments={'serial_port': LaunchConfiguration('port')} 显式映射,不要依赖 Python 全局变量泄漏——后者让 yaml 子文件无法单独 review。迁移 PR 描述里贴迁移前后 ros2 param dump diff,参数有效值不变才算只换装配层。
6. 可读性审查问题
审查 launch MR 时问三句:默认参数是否不在代码深处 buried?条件分支能否一屏读完?真机/仿真切换是否单一 launch arg?答不上来就该拆文件或收回声明式。表达力是成本,不是目标函数。
7. 反模式与验收
- 反模式:全仓库强制 Python「因为灵活」——简单图也 import 二十行。
- 反模式:yaml 里 copy-paste 四个几乎相同的 node 块——该用 xacro 式宏或 Python 循环。
- 验收:新人按 README 能在 10 分钟内改
ip_address并启动;迁移 MR 冒烟零 diff(图一致)。
8. 案例:XML 迁移暴露隐藏默认值
某驱动 port 默认写在 Python launch 第 87 行 DeclareLaunchArgument 的 default 里,yaml 迁移时被提到 yaml 顶层,现场才发现默认仍是 /dev/ttyUSB0 而非文档说的 USB1。声明式让默认值第一次被全组看见——这本身就是收益。
大型仓库可按子系统分目录:launch/perception/ 全 yaml、launch/fleet/ Python 组装——读者找文件成本低于按文件后缀站队。根 robot.launch.py 保持薄,只做 include 与全局 arg,不把业务逻辑塞在根文件。
9. 参数文件与 launch 的分界
节点行为参数(PID、帧名)放 config/*.yaml,由 launch 的 parameters=[...] 加载;图结构(起哪些 node、remap、namespace)放 launch。不要把「是否启动 nav2」藏进节点私有 yaml——运维需要在 launch 层看见图。Python launch 读 yaml 时用 PathJoinSubstitution,避免硬编码绝对路径导致 overlay 工作空间失效。
稳定图声明式,复杂装配 Python;默认一种,例外有理由。Launch 是运维界面,不是炫技场。
相关
也可以看看
- ·3 分钟阅读
Python Launch 重构:部署图,而不是第二套业务代码
按子系统拆分、参数外置、顶层组装;用冒烟守住入口,把业务逻辑赶回节点。
- ·15 分钟阅读
ContentFilteredTopic:把过滤下推到中间件还是应用层
对比订阅端丢弃、应用层条件判断与 DDS 内容过滤的 CPU/带宽边界,说明表达式能力与发现时序限制;用高频率噪声话题与延迟尖峰验收过滤位置选择。
- ·16 分钟阅读
ROS 2 Action 取消语义:收到 cancel 不等于执行器已经停下
梳理 goal handle 状态机、取消回调与硬件停止之间的异步边界;用可中断执行循环、截止时间和终态发布构建契约并以注入验收。
johan's blog