设备通信最危险的失败不是明确返回错误,而是:命令已发出,连接随后中断,上位机不知道设备是否执行。简单地“超时就重试”可能让运动轴重复移动、阀门重复动作或配方重复下发。
本篇建立一套与串口、TCP 都适用的故障模型。
1. 把一次调用拆成阶段
| |
客户端观察到超时,只能证明最后期限内没有得到期望响应,不能反推出失败发生在哪一步。尤其在“执行完成、响应丢失”时,无条件重试会重复副作用。
2. 三类结果
| 结果 | 含义 | 典型处理 |
|---|---|---|
| 已成功 | 收到可验证的成功响应 | 提交上层状态 |
| 已失败 | 收到明确拒绝,且设备保证未执行 | 修正参数或进入故障处理 |
| 未知 | 超时、断线、响应无法关联 | 查询状态、对账或人工确认 |
把“未知”强行映射成失败,是许多重复动作事故的根源。API 可以显式返回 Outcome.Unknown,迫使上层选择恢复策略。
3. 幂等键与命令语义
幂等不是“重试次数小”。同一个逻辑操作重复提交后,设备端应识别它并返回原结果,而不是再次执行。
常见设计:
| |
StableClientId 来自持久配置或已认证的客户端身份,不能使用每次重连都会变化的传输 Session ID。客户端生成的 CommandId 在该身份范围内保持唯一,设备在约定窗口内保存去重键、操作参数与结果:重复请求的操作和参数完全相同则重放原结果;同一去重键携带不同内容必须拒绝。若要求跨设备重启重试,设备端去重记录也必须按协议要求持久化;仅在内存中保存无法覆盖重启窗口。
并非所有命令天然幂等:
SetPosition(100)可能是幂等的,但仍受坐标系和会话状态影响;MoveRelative(+10)通常不是幂等的;Start()在已运行时是否成功,必须由契约定义;- “写配置”若伴随闪存计数或触发重启,也不能只看最终值。
4. 没有设备端幂等支持怎么办
可按风险从低到高选择:
- 重连后查询设备状态,确认目标是否已达到;
- 使用设备提供的任务号、事件号或最后命令记录对账;
- 将非幂等动作改造成“设置绝对目标 + 查询完成状态”;
- 无法证明安全时停止自动恢复,要求人工确认。
客户端本地去重无法覆盖“设备执行后客户端崩溃”的窗口,不能替代设备端协议支持。
5. 重试策略
只有同时满足以下条件时才自动重试:
- 错误被判定为瞬时;
- 操作天然幂等或具有端到端幂等键;
- 剩余截止时间足够;
- 重试不会违反设备状态机和安全约束。
退避可使用带抖动的指数策略,并设置最大间隔、最大次数和总体截止时间。所有客户端若固定每秒同时重连,会在设备恢复瞬间形成惊群。
协议错误、认证失败、参数非法和安全互锁通常不是“多试几次”能解决的。
6. 重连是新会话
连接恢复后,不要直接把状态标为 Ready。稳健流程通常是:
| |
每次连接分配 ConnectionGeneration。接收、超时和回调都携带世代号,旧连接迟到的完成事件不能改变新连接状态。
串口重开前还可能需要等待线路静默或丢弃残留输入;TCP 重连则得到新的字节流,旧连接的事务 ID 不能无条件沿用。
7. 超时预算而不是孤立超时
一次业务操作可能经过队列、发送、设备执行和响应。用单调时间计算每一步剩余预算:
| |
TimeProvider.GetTimestamp() 提供单调时间戳,避免墙上时钟校准造成跳变;测试中还可以注入可控的时间提供器。
8. 熔断与限流放在哪里
设备通常是低并发、强状态资源。比通用 HTTP 客户端更重要的是:
- 有界命令队列,满时明确拒绝或合并;
- 同一设备的并发度符合协议能力;
- 连续失败后进入 Faulted,避免无限轰炸设备;
- 恢复探测只允许少量安全命令;
- 急停和安全回路不依赖普通应用重试链路。
熔断器不能代替设备状态机,也不能把未知结果变成失败。设备级故障闭环见《设备软件架构与控制模型(六):故障处理、报警与安全恢复》。
9. 可观测性
一次操作至少关联这些字段:
| |
指标应区分连接失败、协议坏帧、设备拒绝、业务超时和未知结果。只统计一个“通信失败率”,无法指导恢复。
10. 测试故障窗口
模拟器应能在每个阶段注入故障:收到命令前断开、执行前断开、执行后响应前断开、发送半帧、重复响应、迟到响应、重启后去重表丢失。然后验证:
- 非幂等动作不会自动重复;
- 旧世代响应不能完成新请求;
- 队列和等待表最终清空;
- 未知结果被显式上报;
- 停止信号能中断退避和重连。
11. 小结
超时只是观察,不是结论;重连是新会话,不是恢复旧 socket;重试只有在端到端幂等成立时才安全。把“未知结果”建模出来,并通过查询、对账和连接世代恢复,设备软件才能在最棘手的故障窗口中保持可控。