侧边栏壁纸
  • 累计撰写 14 篇文章
  • 累计创建 37 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

ROS 2 控制命令偶发断流?QoS depth=1 排查记录

猿有味
2026-05-05 / 0 评论 / 0 点赞 / 7 阅读 / 0 字

摘要

现场碰到过一个底盘控制问题:车能动,但动作不连贯,设备侧偶尔报控制超时。

刚开始看,很容易怀疑发布频率不够、网络抖动,或者底盘超时保护设得太紧。最后查下来,真正卡住的是 ROS 2 订阅端的 QoS 队列。

当时底盘控制和货叉控制复用了同一个 Topic、同一个消息类型。订阅端 QoS 又配置成了:

auto qos = rclcpp::QoS(rclcpp::KeepLast(1));

KeepLast(1) 用在状态类消息上问题不大,反正只关心最新值。但底盘控制不是状态刷新,它靠连续控制帧维持动作。中间几帧如果被后来的消息顶掉,设备侧看到的就是控制命令间隔变大。间隔一大,超时保护就会触发。

这篇文章记录一次真实排查过程。重点很简单:

底盘能动但不流畅,设备侧又报控制超时,别只盯发布端和网络,订阅端队列也要查。

一、问题现象

当时不是底盘完全没反应,而是这种状态:

  • 底盘能按指令动作
  • 运动过程不够平滑,偶尔像断了一下
  • 设备侧打印控制超时
  • 上层看起来还在持续发布控制命令
  • 操作频繁,或者底盘和货叉接近同时控制时,更容易复现

底盘侧的超时保护并不知道发布端有没有执行 publish(),它只看自己有没有在规定时间内收到下一帧控制命令。

所以看到“控制超时”,只能先说明一件事:底盘收到的控制流断过。至于断在发布端、网络、订阅队列,还是设备发送线程,这时还不能拍板。

二、先不要急着放宽超时

看到设备报超时,第一反应往往是把底盘保护阈值调大。

这个办法可能会缓解现象,但排障时不建议先这么做。超时保护只是把链路上的间隔问题暴露出来,直接放宽阈值,容易把真正的问题藏起来。

我当时先把控制链路拆开看:

  1. 发布端是否按预期频率发
  2. 订阅端回调是否按同样节奏收到
  3. 回调到设备发送之间有没有阻塞
  4. 同一个 Topic 里有没有混入其它控制消息
  5. QoS 的 history / depth / reliability 是否符合控制语义

这次真正有用的是后两项。

继续往下看,底盘控制和货叉控制走的是同一个 Topic。

三、同一个 Topic 混了多类控制

这条链路里,底盘和货叉没有拆成两个控制入口,而是共用了:

  • 同一个话题名
  • 同一个消息类型
  • 同一个订阅端 QoS 队列

这就意味着,底盘控制帧和货叉控制帧都会进入同一个消息队列。

如果只是偶尔发一条动作命令,这种设计不一定马上出问题。但底盘控制更敏感,它不是发一条就结束,而是依赖连续控制帧保持运动。

这里怕的不是少一条日志,也不是少一次状态刷新,而是控制帧之间的节奏被打断。

四、问题落在 KeepLast(1)

当时 QoS 配置大致是这样:

auto qos = rclcpp::QoS(rclcpp::KeepLast(static_cast<size_t>(depth)));

实际配置里:

depth = 1;

也就是:

auto qos = rclcpp::QoS(rclcpp::KeepLast(1));

ROS 2 的 QoS 里,Keep last 表示只保存最近 N 个样本,Depth 就是这个队列大小,并且只在 keep last 策略下生效。

所以 KeepLast(1) 的含义很直白:队列里只留最近一条消息。

如果 Topic 传的是当前位置、电量、诊断摘要,这通常能接受。旧状态被新状态覆盖,大多数场景下没问题。

但底盘控制帧不是状态。它是连续控制输入。

中间帧被覆盖后,设备侧看到的不是“少刷新了一次”,而是两帧控制命令之间的间隔变长。只要这个间隔超过底盘保护阈值,超时日志就会出现,运动也会变得不连续。

这次问题基本就是:

底盘控制和货叉控制复用同一个 Topic,订阅端 QoS 又是 KeepLast(1)。多类控制消息短时间进入同一个队列时,前面的底盘控制帧可能还没被消费,就被后面的消息覆盖了。

五、为什么像网络抖动

这个问题容易误判,因为发布端和设备端看到的都不像“队列问题”。

发布端看起来正常:命令确实发出去了。

底盘侧也没报假:它确实没按时收到下一帧。

真正出问题的是中间这段:

sequenceDiagram participant P as 发布端 participant Q as 同一 Topic 队列 participant S as 订阅端 participant D as 底盘 P->>Q: 底盘控制帧 A P->>Q: 货叉控制帧 B Note over Q: KeepLast(1) 只保留最近一条 S->>Q: 取消息 Q-->>S: 可能只取到 B Note over D: 底盘控制帧间隔变大

如果只看两头,很容易把它归到网络抖动、DDS 丢包,或者发布频率不稳。

但这次节奏丢在订阅端队列里,不在两头。

六、调整 depth 后怎么验证

当时没有先改设备协议,也没有先放宽底盘超时保护,而是先把 QoS 深度调大。

原来是:

auto qos = rclcpp::QoS(rclcpp::KeepLast(1));

改成:

auto qos = rclcpp::QoS(rclcpp::KeepLast(10));

改完以后,底盘控制连续性明显改善,控制超时日志也消失或明显减少。

这个结果基本能说明,问题不是上层完全没发够,也不是底盘保护逻辑写错了。原来的 depth=1 不适合这条控制链路。

不过这里不能把结论说满。把 depth 调大只是给突发消息留出缓冲,让订阅端有机会消费前面的控制帧。如果发布速度长期超过消费速度,或者回调里有阻塞 IO、长时间持锁、同步等待,继续加 depth 也只是把问题推迟。

七、后续怎么改更稳

这件事不要只记成“把 1 改成 10”。

更该回头看 Topic 设计。这个坑之所以能出现,是因为两类控制挤在了一个入口里。

1. 能拆 Topic 就拆

底盘和货叉本来就是两类控制。能拆的话,直接拆开更清楚:

  • /chassis_cmd
  • /fork_cmd

这样至少不用抢同一个缓存队列。QoS 可以分别调,日志也更好判断。后面再查问题,不用先猜到底是哪类动作把队列顶掉了。

很多项目里,拆 Topic 比继续在一个消息类型里加动作字段更省事。

2. 不能拆,就补队列和确认

如果架构上必须保留统一控制 Topic,那就不能把它当普通状态流用。

至少要考虑这些东西:

  • 动作类型
  • 命令序号
  • 应用层队列
  • 顺序处理
  • 优先级仲裁
  • ACK 或状态回读
  • 超时和失败处理

一个 Topic 可以作为统一入口,但不能只靠 KeepLast(1) 和“最后一条消息”承载多类控制。

3. 回调里别做重 IO

还有一个常见坑:订阅回调里直接做串口同步写、CAN 阻塞发送、长时间持锁,甚至 sleep

这样写起来省事,出问题时很难查。

更稳一点的结构是:

  • ROS 回调只接收、校验、入队
  • 后台线程负责设备下发
  • 控制入口和设备 IO 解耦

这样至少能分清楚:ROS 层有没有收到,应用队列有没有积压,设备发送有没有卡住。

八、控制类 Topic 的 QoS 建议

控制类 Topic 不建议顺手写 depth=1

至少要根据控制频率、设备超时阈值和订阅端处理耗时,给突发场景留一点余量。比如:

auto qos = rclcpp::QoS(rclcpp::KeepLast(10));

具体是 5、10 还是 20,要看现场频率和消费速度。重点不是固定某个数字,而是别把连续控制当成状态刷新。

如果控制消息不能轻易丢,再考虑 reliable

auto qos = rclcpp::QoS(rclcpp::KeepLast(10)).reliable();

reliable 不等于设备一定执行成功,也不能替代 ACK。它只是让 ROS 2 通信语义更接近可靠送达。后面仍然要看订阅端是否处理、设备侧是否执行。

停车、升降停止、模式切换、故障恢复这类动作,最好再补应用层确认、状态回读、超时检测和必要的重发。ROS 2 的 QoS 只能解决通信层的一部分问题,不能替你保证设备真的执行了命令。

小结

这次问题表面上是底盘运动不流畅,设备侧报控制超时。看起来像发布频率不够,或者网络抖动。

根因是底盘和货叉共用了同一个 ROS 2 Topic,订阅端 QoS 又用了 KeepLast(1)。多类控制消息短时间进入同一个队列时,底盘控制帧可能还没被消费,就被后面的消息覆盖。

depth 从 1 调到 10 后,底盘控制连续性明显改善。这个结果说明,原先“只保留最新一条”的假设不适合这条控制链路。

最后记住两点:

  • 底盘连续控制不能简单按状态流配置 QoS
  • 看到控制超时,不要只查发布频率和网络,也要查订阅队列

参考

0

评论区