2. ROS2基础回顾
在进入任务通信与行为编排之前,有必要把 ROS2 的通信骨架重新梳理一遍。ROS2 不是“一个进程跑到底”的框架,而是由大量可独立部署、可动态组合的计算单元通过中间件互相协作。理解这些单元各自解决什么问题、在什么场景下使用,是后续设计任务编排逻辑的前提。
一、核心概念与通信模型
ROS2 中最基本的运行单元是节点(Node)。一个节点通常承担一项相对独立的职责,例如读取激光雷达、发布里程计、执行路径规划。节点通过话题(Topic)、服务(Service)、动作(Action)和参数(Parameter)四类接口与外界交互,它们分别对应不同的通信语义。
- 话题:发布/订阅模型,单向、异步、多对多,适合高频连续数据流,如传感器数据、状态广播。
- 服务:请求/响应模型,同步、一对一,适合短时、确定性的查询或命令,如“查询地图”“切换模式”。
- 动作:基于话题与服务构建的长时任务模型,支持目标、反馈、结果和取消,适合导航、抓取等耗时任务。
- 参数:节点级配置项,可在启动时设置,也可在运行时动态调整,如速度上限、控制增益。
这四类接口并非彼此替代,而是互补。选择错误的通信方式,往往会导致系统在实时性、可观测性或可取消性上出现问题。
二、执行器:回调如何被调度
节点中注册的订阅回调、服务回调、定时器回调并不会自动并发执行,它们由执行器(Executor)统一调度。ROS2 提供单线程执行器和多线程执行器两种基本形态。单线程执行器按顺序处理回调,逻辑简单但容易因某个耗时回调阻塞整个节点;多线程执行器允许并行处理,但需要配合回调组(Callback Group)来避免数据竞争。
常见的做法是把互斥的回调放入同一个 MutuallyExclusive 回调组,把可并行的回调放入 Reentrant 回调组。若不加区分地把所有回调都设为可重入,共享状态就可能被并发修改,导致难以复现的偶发故障。
// 创建互斥回调组,保证同一组内回调不会并发执行
auto cb_group = create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
// 订阅时绑定回调组
auto sub_opt = rclcpp::SubscriptionOptions();
sub_opt.callback_group = cb_group;
subscription_ = create_subscription<std_msgs::msg::String>(
"topic", 10, callback, sub_opt);
三、生命周期节点:让启动过程可控
普通节点一旦启动就立即开始工作,这在复杂系统中会带来隐患:传感器尚未就绪、参数尚未校验、下游节点尚未上线,数据就已经开始流动。生命周期节点通过状态机解决这一问题,其典型状态包括:
- Unconfigured:节点已创建,但尚未配置。
- Inactive:已完成配置,但不处理数据。
- Active:正常处理数据、发布消息。
- Finalized:已终止,等待销毁。
状态之间通过 configure、activate、deactivate、cleanup、shutdown 等转换触发。这样,系统可以按依赖顺序逐个激活节点,避免“半成品”数据进入计算链路。
四、典型场景与选型对照
假设我们要实现一个“移动机器人巡逻”功能:激光雷达节点持续发布扫描数据,定位节点订阅扫描并发布位姿,巡逻控制节点接收目标点并控制底盘。这里,雷达到定位、定位到控制适合用话题;控制节点查询“当前是否允许移动”适合用服务;而“前往某个巡逻点并返回结果”则适合用动作。
| 通信方式 | 语义 | 典型场景 | 注意事项 |
|---|---|---|---|
| 话题 Topic | 发布/订阅,异步 | 传感器数据、状态广播 | 注意队列深度与丢帧 |
| 服务 Service | 请求/响应,同步 | 查询、模式切换 | 避免长阻塞,防止死锁 |
| 动作 Action | 目标/反馈/结果 | 导航、抓取、巡检 | 需处理取消与超时 |
| 参数 Parameter | 键值配置 | 速度上限、增益 | 动态修改需回调校验 |
五、常见误区与复盘
- 用服务做长任务:服务调用会阻塞客户端,若任务耗时较长,容易造成超时或界面卡死,应改用动作。
- 忽视执行器配置:默认单线程执行器下,一个耗时回调会拖慢整个节点,必要时使用多线程执行器并划分回调组。
- 生命周期节点只配不用:若没有按依赖顺序激活,仍可能出现数据竞争,应结合 launch 的 event handler 控制激活顺序。
- 参数修改不校验:运行时修改参数若不做范围检查,可能直接导致控制异常,应设置参数回调进行合法性验证。