我排查取消后的偶发 ACK 异常时,最危险的假设是“收到一条格式正确的响应,就可以结束当前等待”。STOP 也是 CAN 命令,也会产生 ACK。如果 STOP 与业务命令复用 command-id,迟到的 STOP ACK 可能进入下一次业务等待窗口,被解释成“设备确认执行业务”。
这类错误不会总是表现为超时。它可能让任务更快成功,因此比明显失败更难发现。
协议已经冻结,怎么加相关性
当前协议没有再增加一个响应类型字段,而是划分 command-id 空间:
1 | 0x0001..0x7FFF application command |
普通业务 ID 超过 0x7FFF 后回绕到 1。STOP ID 从 0x8000 开始,在 uint16_t 上限后回绕到 0x8000。两个分配器不会合法地产生相同 ID。
1 | rg -n "kApplicationCommandIdMax|kStopCommandIdMin|kStopOpcode" ros2_ws/src/runtime_can |
只划分空间仍然不够。接收端的 receive_response() 必须绑定本次等待的 expected_command_id。帧能解码,不代表它属于当前请求。ID 不匹配时,代码记录 Ignoring unmatched response 并继续等待。
safe_stop_sent 阶段的自干扰
我把“STOP 已发送,正在等待或清理其响应”的阶段称为 safe_stop_sent。这是排障时使用的阶段标签,不是源码变量名。
进入这个阶段后,匹配当前 STOP ID,且设备模式为 STOPPED 的响应才能完成 STOP。其他 ACK 即使格式正确也要忽略。STOP 窗口结束后,迟到的 0x8000+ 响应若进入业务窗口,仍会因为 ID 不匹配被丢开。
这解决了一个自干扰问题:系统自己发出的安全停车帧及其响应不能被算成“意外业务 ACK”。否则安全动作会污染后续故障判断。
我怎样验证线上没有串线
协议单测先锁住命名空间边界:
1 | colcon test --packages-select runtime_can device_bridge |
E2E 使用 candump -L vcan0 保存 0x100 命令帧和 0x101 响应帧。assert_stop_can 从 opcode 0xFF 的命令中提取 STOP ID,再要求响应 command-id 与它一致。
cancel 场景要求恰有一条 STOP 响应,设备模式为 STOPPED。drop_stop_ack 场景要求没有对应响应,并得到 SAFE_STOP/204。ack_timeout 场景还检查业务 ACK 超时后发送 STOP,最终保留原业务故障 201,同时确认 STOP 已被设备应答。
日志中还有两条值得保留:
1 | STOP sent stop_command_id=... original_command_id=... |
前者证明 STOP 没有复用业务 ID,后者证明迟到帧不能抢占当前等待。实现与完整说明可在 Embodied Agent Runtime 提交 01960cd 中复核。
证据边界:这些结果证明软件接收端按 expected ID 过滤,以及 vcan 场景中的帧与 Action 结果一致。它不能证明真实控制器不会重排、重复或缓存帧。STOP ACK 也不等同于执行器失能或硬件急停动作。