平台显示“启动成功”,现场设备却没有动作;用户重试一次,设备又在稍后连续执行。
远程设备控制最容易产生误解的地方,是把传输层成功当成了业务成功。MQTT 帮助传递消息,但真正的执行结果,需要设备、业务服务与界面共同表达。
从充电桩到教学机房的批量设备管理,这个问题有一个共同点:网络会断,回执会晚,设备可能在执行动作后立刻重启。
把“消息到了”和“事情做完了”分开
MQTT QoS 1 允许重复传递;QoS 2 的一次传递语义也有协议交互范围,不能直接推出“电机只启动一次”或“费用只结算一次”。应用层还需要自己的命令 ID 与执行记录。MQTT 5.0 标准
一个清楚的状态模型可以是:
1 | CREATED → DISPATCHED → ACCEPTED → RUNNING → SUCCEEDED |
DISPATCHED 只表示平台已经发出;ACCEPTED 表示设备理解并接受;SUCCEEDED 则要求设备提供执行结果。对于重启命令,断线是预期现象之一,不能立刻视为失败,也不能单凭断线就认定成功。
命令身份必须跨重试保持稳定
一条启动命令至少关联设备、设备会话、命令 ID、有效期和业务会话:
1 | { |
重试不能生成新的 commandId。设备收到同一命令时,应返回已有处理状态;同一 ID 的载荷变了,应拒绝执行。身份和摘要的记录必须覆盖允许重试的时间范围,不能在重启后全部丢失。
如果设备在动作完成后、保存成功回执前掉电,单纯的“去重表”仍然存在空隙。对启动充电这种状态型动作,应查询当前充电会话,尽量把命令设计为“确保会话 X 处于运行状态”,而不是无条件触发一次开关动作。无法核对的不可逆动作,恢复后应停在结果未知,不能盲目补发。
过期命令不能在重连后突然生效
设备离线期间积压的启动、关机等指令,在恢复网络后执行可能已经不符合用户意图。MQTT 5 的消息过期能力可以帮助控制消息寿命,但业务层仍要校验命令有效期和设备当前会话。
sessionEpoch 可以由服务端在认证重连时分配,并在下发与回执中携带,用于隔离旧会话消息。设备时钟不可靠时,还需要定义校时、允许偏差和无法确认时的拒绝规则,而不是完全信任一个本地时间戳。
一次性控制命令不要使用 retained 消息。设备状态可以按业务需要保留最新值,但历史状态必须带采样时间、会话和序号,不能因为刚订阅就收到了 retained 状态而认定设备此刻在线。
Redis 保存快照,数据库保留业务事实
设备最新电流、电压、在线状态适合在 Redis 中维护读快照;充电会话、费用、指令结果和结算流水需要落在持久业务存储中。
状态上报可携带 (sessionEpoch, sequence),在同一会话内只接受更大的序号。跨会话时先确认是服务端认可的新会话,再替换快照。不能直接比较设备时间,也不能认为重启后从零开始的序号一定是“旧数据”。
在线状态同样需要分层:连接存在、最近收到心跳、最近收到遥测、最近一次命令可执行,含义并不相同。业务界面应展示更新时间与失联状态,避免把很久以前的绿色图标当成实时事实。
结算依据不能只是“充电了几分钟”
充电会话需要保存起止计量值、费率版本、必要的分段计量与设备回执。设备上传累计量时,不要把每次累计值再相加;迟到、重复数据以及计量表归零,都要单独处理。
按电量和时段计费时,只有起止两个读数未必足以还原跨费率时段的费用,需要具备对应的分段依据。计量异常应先进入核对状态,不能把差值为负直接当成零元结算。
结算以充电会话为业务幂等边界,在数据库事务内生成账务记录。Redis 键过期不能成为“用户已经付过钱”的唯一证据。关于这个边界,可以继续看余额、幂等与账本设计。
批量控制需要逐台收尾
批量启动或批量分发不能只返回一个“成功”。每台设备都应展示已发出、执行中、成功、失败或结果未知,允许对可安全重试的目标单独恢复。
连接层使用独立设备身份和 Topic 权限,平台从认证连接推导设备身份,不直接信任载荷里的 deviceId。控制、遥测和回执可以拆分 Topic,避免高频遥测堵住关键操作;消费侧设置有界队列,对可以合并的快照更新丢弃旧值,对业务回执保留完整处理路径。
验证这套设计时,主动测试“收到指令即断网”“完成动作但回执丢失”“设备重启后重放旧指令”“新会话先到、旧会话后到”。真正需要看见的结果,是每台设备都能被解释和恢复,而不是平台单方面显示一排成功。