
04-心跳链路一套软件栈如何向MCU证明我还活着引子最致命的问题不是功能缺失是装死大家好我是黒漂技术佬。上一篇我们讲了 SLAM 和 Nav2那是扫地机器人聪明的部分。这一篇讲一个完全相反气质的主题当聪明的那部分死掉时谁来发现谁来兜底想想这个场景机器人正在满屋跑突然 ROS2 栈里某个节点挂了——内存泄漏被 OOM 杀掉、死锁、或者你自己写的清扫逻辑抛了未捕获异常。此刻机器人最后的/cmd_vel命令是什么如果没人管它会保持最后一次的速度设定朝着一个方向笔直地撞墙或者更糟——顶着充电座、爬上地垫边缘翻车。“软件死了但机器还在动”——这是所有移动机器人的头号事故模式。OOMWOO 为此设计了一条完整的健康监控与心跳链路Stack Health Monitor把软件栈还活着这件事变成一个可以持续验证的信号。这份设计文档不长但每个细节都在教你面向失败设计。今天逐条拆。一、总设计一条聚合的死开关先看全局。OOMWOO 的思路不是让每个话题自己做过期检查零散、易漏、难审计而是收敛出一条单一的聚合健康通道清扫任务管理器 ── /oomwoo/health/roster 本次任务的关键组件名单latched 关键组件 A/B/C ── /oomwoo/health/component 各自在干活路径上发心跳 | Health Monitor健康监视器 | ├── /oomwoo/health/stack 聚合状态arming/健康/故障 └── /oomwoo/health/mcu_heartbeat 唯一发往 MCU 的栈心跳 | MCU心跳一停 → 停电机规则极简roster 里登记的关键组件只要有一个心跳缺失、过期或不健康MCU 心跳就停MCU 心跳一停电机就停。这是典型的**死开关deadman switch**设计系统默认状态是停必须被持续证明健康才允许继续动。与出了问题再停下的异常处理思路完全相反——异常处理依赖你枚举出所有异常死开关只依赖一条不变式。二、四个话题各有各的讲究2.1 roster先声明谁重要再检查活没活/oomwoo/health/roster由任务启动方发布内容是本次任务的关键组件名单——哪些组件是任务执行必需的critical哪些只是锦上添花advisory。这个设计解决了一个隐含难题健康标准是随任务变化的。遥控模式下可能只需要底盘节点活着自动清扫模式下SLAM、定位、规划器缺一不可。用名单先行监视器就不用硬编码任何业务逻辑——它只管名单上的人名单说了算。2.2 component心跳必须从干活的路径里发这是整篇文档里我最想划重点的一条组件心跳必须从有用的工作路径发出emitted from useful work paths而不是从独立的定时器发出——独立定时器在组件卡死时还能继续跳。细品。最偷懒的实现是给每个节点加一个create_timer(1s, publish_heartbeat)——但一个死锁在回调里、或者事件循环已经被卡爆的节点它的定时器可能照跳不误。心跳在响人已经凉了。这跟心电监护仪还亮着病人已经不行了是同一个荒诞剧。正确做法心跳的发射点埋在真正的工作路径上——比如控制环每次成功迭代、规划器每次成功产出路径时顺手发。心跳因此天然携带了工作在推进的证据而不只是进程还在调度。做后端的同学想想你们服务里的/healthz是不是也是启动时注册、进程活着就 200liveness 探针检测不出活着但已失能这是 K8s 时代被广泛忽略的一课。2.3 stack聚合状态分五档监视器对外发布/oomwoo/health/stack状态机为no roster→arming→healthy→healthy with advisory faults→fault其中arming武装窗口很有仪式感监视器启动后并不立刻认为系统健康而是先 withhold扣住MCU 心跳直到整个关键名单被逐个确认健康才上膛。防止监视器自己刚起来、信息不全时误发心跳。advisory 故障只上报不停车——故障分级是可用性的关键不是所有异常都值得停一台正在工作的机器人。2.4 mcu_heartbeat链路终点权力移交/oomwoo/health/mcu_heartbeat是这条链路的最后一站只有当所有预期关键组件都新鲜且健康时监视器才向 MCU 转发栈心跳。MCU 收不到上一篇讲过就停电机、可复位 CPU。软件世界和物理世界在这里握手。从任何一个关键组件的一次成功工作到电机保持转动是一条完整的证明链。三、QoS 抠细节为什么这条 reliablevolatile那条要 transient-localOOMWOO 的 QoS 设计是教科书级的按语义选 QoS四条话题四种考量话题QoS理由rosterreliable transient-local depth 1监视器晚启动也能拿到当前名单stackreliable transient-local depth 1晚加入的诊断方能看到最新聚合状态component心跳reliable volatile心跳是现在时过期值毫无意义mcu_heartbeatreliable volatile旧的健康心跳绝不能被重放给晚订阅者重点在后两条。心跳和地图是两类语义相反的数据地图持久的状态晚来的人需要最近一次快照 → transient-local订阅即补发心跳瞬态的证明晚来的人绝不该收到 30 秒前的我很健康→ volatile错过就是错过。旧的健康证明被重放是一个能杀人的 bug。想象监视器重启的瞬间DDS 把缓存里上一条 healthy 心跳补发给 MCU 桥——MCU 恰好在这几秒里失去了对真实状态的确认却被一条陈旧心跳重新武装。volatile 的意义就在这里让新鲜度成为传输层的保证而不依赖应用层时间戳比对。还有一条容易漏的时钟统一。组件心跳的stamp_sec必须使用产生者的 ROS 时钟且与监视器同一时钟源——use_sim_timetrue时大家跟/clock绝对不允许墙钟和仿真钟混用。仿真环境里忘了这条超时判断全部错乱这种 bug 能让你调到怀疑人生。四、对照表这套设计和云原生监控的异同把 OOMWOO 的健康链路和后端同学熟悉的 Kubernetes 探针体系放在一起看会更清楚它解决的是什么层级的问题K8s liveness/readinessOOMWOO 心跳链路检测对象进程是否存活/可服务任务关键组件是否在推进工作心跳来源Kubelet 定期主动探测组件从工作路径自证失败动作重启容器停 MCU 心跳 → 电机停止数据新鲜度每次探测独立发起volatile QoS旧心跳不可重放分级liveness/readiness/startupcritical/advisory 两级 arming兜底层节点级 kubelet / 云商健康检查独立硬件看门狗STM32相同的思想内核默认不信任持续证明失败即隔离。不同的一点很本质云服务挂了可以重启顶多服务中断机器人挂了会撞墙物理后果所以 OOMWOO 在体系最底下垫了一层不依赖任何软件的硬看门狗——云原生监控的死穴是监控系统和被监控系统在同一台机器/同一套基础设施里机器人架构直接把这个死穴焊掉了。还有一个值得玩味的细节OOMWOO 文档规定心跳消费方要有武装窗口arming window且旧健康心跳不得重放。这对应分布式系统里两个经典攻击面——“监视器冷启动误判和陈旧状态重放”。前者 K8s 用 startupProbe 缓解后者靠心跳 token 新鲜度。安全机制的价值往往不在主流程而在冷启动和恢复这些没人看的时刻。五、为什么这套设计值得抄走把它从扫地机语境里抽出来这其实是一套通用的**“分层健康证明”**模式单一死开关优于零散防御。所有防卡死的逻辑收敛到一个监视器、一条心跳可审计、可测试而不是在每个节点里各自为战谁重要与活没活解耦。roster 机制让业务变化不影响监视逻辑心跳必须携带工作证据从干活路径发出拒绝进程级假活状态数据用 transient-local证明数据用 volatile——QoS 选择跟着数据语义走默认停机fail-safe没有名单→没心跳名单缺人→没心跳监视器没上膛→没心跳。停是唯一默认软件证明链的终点是物理执行器中间由一个不依赖软件栈的硬看门狗接手回扣 02 篇的双脑架构。顺带一提OOMWOO 文档里给出了参考实现社区贡献的 xbattlax/health-monitor——这就是接口契约先行 社区并行共建跑通的一个活例子。六、结语机器人的可靠性是证明出来的上一篇结尾说三层防线各管一段这一篇补上了最上层那条线软件栈的自证。至此整台机器人的安全体系闭环了——LiDAR 看得见的规划绕开看不见的碰撞条兜底智能栈失能的MCU 心跳超时急停而智能栈是否失能由这条心跳链路持续证明。设计文档里有句提纲挈领的话硬安全事件必须由 MCU 处理即使 ROS2 已经挂了。ROS2 可以请求运动、报告状态、执行恢复但陈旧的心跳、不安全的传感器状态必须在 MCU 层终止运动。下一篇进入动手篇不买 OOMWOO 的账我们自己怎么拥有一台能编程的扫地机器人三条路线、一份物料清单、一张攒机路线图安排上。