返回专辑
·Johan·4 分钟阅读

Launch:XML/YAML 与 Python 的分工,不是信仰站队

稳定子图用声明式,条件装配用 Python;团队默认一种,迁移只换装配层。

Launch:XML/YAML 与 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

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:

python
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/XMLPython
diff 可读性中(逻辑散)
表达力
非程序员可改通常否
藏副作用易(import 副作用)

团队应锁定一种默认(如「新子系统先 yaml,需动态逻辑再 Python」),写进 CONTRIBUTING。例外开另一种必须在 MR 说明理由——口头约定等于没有约定,最后仓库里四种风格混战。

5. 迁移:只换装配层,不改接口

从全 Python 迁到 yaml 叶子时,节点参数文件、话题名、命名空间保持不变,只把 Node(...) 块挪进 yaml。用 launch 冒烟对比迁移前后 ros2 node listros2 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 是运维界面,不是炫技场。

← 全部文章

johan's blog