设备影子与离线指令的状态对账
额定 25A(峰值 40A)、采用 FOC 驱动方式的 AQMD6030BLS-E2 这类设备经常处于弱网或停机状态,云端"下发即生效"的假设不成立。设备影子(期望状态/实际上报状态)的价值就是把"我想让它怎样"和"它现在怎样"分开,但用不好会出现指令丢失与状态回退。
一、两个状态、一个版本号
影子模型的核心是三元组:desired(期望)、reported(上报)、version(版本)。典型时序:
| 时刻 | 事件 | desired | reported | version |
|---|---|---|---|---|
| t0 | 设备在线,运行中 | run=1 | run=1 | 10 |
| t1 | 云端下发停机 | run=0 | run=1 | 11 |
| t2 | 设备断网,未收到 | run=0 | run=1 | 11 |
| t3 | 设备重连,读影子 | run=0 | run=1 | 11 |
| t4 | 设备执行并上报 | run=0 | run=0 | 12 |
关键在 t3:设备重连后必须主动读一次 desired,而不是只等实时下行。实时下行在离线期间会丢,影子才是可靠载体。
二、三个典型故障
故障一:状态回退。 设备上报时不带版本或时间戳,云端旧数据后到,把新状态覆盖。对策:reported 更新带单调递增版本或设备时间戳,云端做"只前进不回退"合并。
故障二:指令被吞。 设备执行成功但上报失败,云端仍显示旧状态,运维以为没执行而重复下发。对策:执行与上报解耦,执行结果落盘,上报成功后才清除;配合指令 nonce 幂等(重复下发只回上次结果)。
故障三:期望状态堆积。 设备长期离线,desired 被连续改写多次,重连后执行了一堆过期指令。对策:desired 只保留最新目标态,不排队;对时效性强的指令(如"立即停机")另设有效期,过期丢弃并告警。
/* 影子合并:仅当版本前进才接受,避免旧数据覆盖新状态 */
typedef struct {
uint32_t ver; uint8_t run; uint8_t fault; } shadow_t;
int shadow_merge(shadow_t *cur, const shadow_t *in)
{
if (in->ver <= cur->ver) return 0; /* 旧版本,丢弃 */
cur->ver = in->ver;
cur->run = in->run;
cur->fault = in->fault;
return 1;
}
/* 执行结果落盘,上报成功后才清除,防止"执行成功但云端不知" */
void result_persist(uint32_t nonce, uint8_t ok)
{
kv_write("last_nonce", &nonce, sizeof(nonce));
kv_write("last_ok", &ok, sizeof(ok));
}
三、离线期间的可读性设计
运维侧要能区分三种情况:设备在线且已达成、设备离线但期望已下发、设备在线但执行失败。建议在云端物模型里显式加一个"期望达成状态"字段(pending / satisfied / failed),由 desired 与 reported 比对得出,而不是让前端各自推断。
四、验证方法
一是断网演练:离线期间连续改写 desired 5 次,重连后确认设备只执行最终目标态;二是上报失败注入:执行成功后强制上报失败 3 次,确认状态不丢失且不重复执行;三是乱序测试:让旧版本 reported 晚到,确认云端不回退。
五、小结
影子的正确用法是:desired 只存最新目标态、reported 带版本只前进不回退、执行结果落盘后再清、重连主动读 desired。运维侧要显式区分"已下发/已达成/失败",并用断网、上报失败、乱序三类注入测试闭环。