4. 设备心跳与在线状态建模

在农业物联网场景中,大棚卷帘机、水肥一体机、田间气象站、土壤墒情传感器往往分布在几公里甚至几十公里的范围内,网络条件参差不齐。系统要判断一台设备"是否还能正常工作",不能依赖人工巡检,而必须依靠设备主动上报的心跳来推断其在线状态。本节围绕心跳机制设计、心跳周期与超时阈值的取值、在线/离线/恢复三态状态机,以及状态如何存储与查询展开,目标是让读者能够独立设计一套可落地、可调参、可查询的设备在线状态模型。

心跳机制:让"沉默"变成可判断的信号

心跳的本质是设备按固定节奏向平台发送一条轻量报文,平台据此刷新该设备的"最后活跃时间"。常见实现有三种:一是设备主动向 MQTT Broker 发布心跳主题消息,例如 agri/dev/{deviceId}/heartbeat,载荷只包含时间戳、固件版本、信号强度等少量字段;二是利用 MQTT 协议自带的 Keep Alive 与遗嘱消息(LWT),设备异常断连时由 Broker 代发离线消息;三是设备周期调用 HTTP 接口上报。对于电池供电的田间传感器,还应区分"心跳"与"数据上报"——可以把每次数据上报都视为一次隐式心跳,以减少空报文带来的功耗。

一个关键设计点是心跳报文要足够小且幂等。平台收到心跳后只做一件事:更新该设备的最后活跃时间戳,并触发状态机判断。心跳报文不应携带业务数据,避免网络抖动时业务与存活判断相互牵连。

心跳周期与超时阈值:一对必须匹配的参数

心跳周期 T 决定设备多久"说话"一次,超时阈值 Timeout 决定平台多久没听到就判定离线。二者关系应满足 Timeout = k × T,其中 k 通常取 2.5~3。若 k 太小,一次网络抖动就会误判离线;若太大,设备真正掉线后平台迟迟不告警,失去意义。

取值要结合设备类型与供电方式。以下为一组教学示例参数(属于合理假设,非实测数据):

设备类型心跳周期 T超时阈值说明
市电供电的控制器(卷帘机、水肥机)30 s90 sk=3,响应快,适合需要及时告警的执行设备
太阳能供电的气象站5 min15 min兼顾功耗与可观测性
电池供电的土壤传感器30 min90 min以数据上报代替心跳,降低唤醒次数

需要强调的是,超时判定应由平台侧的定时扫描完成,而不是依赖设备"报告自己离线"。平台每隔一个扫描周期(例如 10 s)查询所有设备的最后活跃时间,若 now - lastSeen > Timeout 且当前状态为在线,则迁移到离线。扫描周期应明显小于最小超时阈值,否则判定精度会被扫描粒度拖累。

三态状态机:在线、离线与恢复

设备状态不能只有"在线/离线"两态,否则无法区分"从未上线"和"掉线后重新连上"。推荐建模为三态:在线(ONLINE)、离线(OFFLINE)、恢复中(RECOVERING)。设备首次注册后处于"未激活"或直接进入离线;收到心跳即进入在线;超时未收到心跳迁移到离线;离线设备再次上报心跳时先进入恢复中,待连续收到 N 次(例如 2 次)心跳或完成一次配置同步后,才确认回到在线,避免网络闪断造成的状态抖动。

设备在线状态机(心跳驱动) 在线 ONLINE 持续收到心跳 离线 OFFLINE 超时未收到心跳 恢复中 RECOVERING 连续 N 次心跳确认 now − lastSeen > Timeout 收到首个心跳 确认恢复 心跳刷新 lastSeen 未达 N 次,继续等待 扫描周期 10 s Timeout = k × T

状态迁移必须由明确的事件触发:HEARTBEAT(收到心跳)、TIMEOUT(扫描发现超时)、CONFIRM(恢复确认)。每次迁移都应记录迁移时间、触发原因和当时的最后活跃时间,这些记录是后续排障和告警抑制的依据。

状态存储与查询:从内存到可追溯

在线状态是高频写入、低频读取的典型数据。推荐采用"内存 + 持久化"两层结构:内存中用哈希表保存 deviceId → {state, lastSeen, stateChangedAt},供扫描任务和实时查询使用;同时将状态变更事件异步写入数据库(如设备状态表或时序库),用于历史追溯和报表。不要每次心跳都写数据库,否则在数千台设备、30 秒心跳的场景下会产生大量无意义写入。

查询接口通常需要支持三类需求:单设备当前状态、按区域或设备类型批量查询离线设备、查询某设备的状态变更历史。下面是一个状态表的结构示例:

CREATE TABLE device_status (
  device_id      VARCHAR(64) PRIMARY KEY,
  state          VARCHAR(16) NOT NULL,   -- ONLINE / OFFLINE / RECOVERING
  last_seen      DATETIME    NOT NULL,
  state_changed  DATETIME    NOT NULL,
  offline_reason VARCHAR(64)             -- TIMEOUT / LWT / MANUAL
);

查询离线设备时,应直接以 state = 'OFFLINE' 为条件,而不是每次用 now - last_seen > Timeout 现算,后者无法利用索引且语义容易与状态机不一致。状态字段由状态机统一维护,查询只读,保证"判定逻辑唯一"。

工程要点与常见坑

  • 时钟不同步:设备时间与平台时间可能有偏差,判定超时应统一使用平台接收心跳的服务器时间,而不是报文里的设备时间戳。
  • 批量抖动:大量设备若在同一秒上线,心跳会形成尖峰。可在设备端对心跳周期加入 ±10% 的随机抖动。
  • 告警抑制:设备进入离线后应只触发一次离线告警,恢复时触发一次恢复通知,避免扫描周期内重复告警。
  • 弱网地区:对偏远田块可适当放大 k 值或提高 T,但需同步调整告警文案中的"预计离线判定时长",让运维人员有合理预期。

把这四部分串起来看:心跳机制提供原始信号,周期与阈值把信号转成可判定的时间条件,状态机把条件转成有语义的状态迁移,存储与查询则让状态可被检索和追溯。四者参数必须联动调整——改心跳周期就要复核超时阈值,改阈值就要复核扫描周期,任何单点修改都可能让在线状态失真。