3、设备影子数据结构:JSON格式详解、desired与reported区、metadata元数据

好,咱们今天来聊聊设备影子的数据结构。说实话,我刚接触IoT那会儿,觉得影子就是个“云端的缓存”,没啥技术含量。直到有一次,我在一个智能路灯项目里,因为搞混了desired和reported,导致路灯整夜没关……嗯,从那以后,我再也不敢小看这个JSON结构了。

3.1 影子文档的整体骨架

设备影子本质上是一个JSON文档。你想想看,它就像一张“状态快照”,同时记录了设备“想变成什么样”和“现在是什么样”。

一个标准的影子文档长这样:

{
  "state": {
    "desired": {
      "temperature": 25,
      "humidity": 60
    },
    "reported": {
      "temperature": 24.8,
      "humidity": 59.5,
      "led_status": "on"
    }
  },
  "metadata": {
    "desired": {
      "temperature": {
        "timestamp": 1623456789
      },
      "humidity": {
        "timestamp": 1623456789
      }
    },
    "reported": {
      "temperature": {
        "timestamp": 1623456790
      },
      "humidity": {
        "timestamp": 1623456790
      },
      "led_status": {
        "timestamp": 1623456790
      }
    }
  },
  "version": 5,
  "timestamp": 1623456790
}

看到没?最外层有三个核心字段:statemetadataversion。我个人习惯把version看作“版本锁”,防止并发写冲突。

3.2 desired区:云端对设备的“期望”

desired,说白了就是“云端想让设备变成的样子”。

举个例子:你在手机App上把空调温度调到26度。这个26度不会直接发给设备,而是先写到影子的desired区。设备上线后,自己来“拉”这个期望值。

这里有个坑,我曾经踩过:

注意:desired区不是必须存在的。如果设备只上报状态,不下发指令,完全可以没有desired。但反过来,如果你要远程控制设备,desired就是核心入口。

我建议这样设计desired:

  • 只放“可被设备执行的指令”,别放无关的调试信息
  • 每个属性尽量是原子值,别搞嵌套太深
  • null表示“清除该属性”,而不是删掉key

3.3 reported区:设备真实的“现状”

reported,就是设备自己上报的当前状态。它和desired是一对“镜像兄弟”。

我记得有一次调试一个温湿度传感器,设备上报的温度是24.8度,但desired里写的是25度。这0.2度的差值,就是“期望”和“现实”的差距。影子存在的意义,就是让云端知道这个差距。

reported区有几个要点:

  • 必须真实:别造假,否则控制逻辑全乱套
  • 尽量完整:每次上报最好带上所有关键状态,别只报变化的部分(除非你做了增量同步)
  • 频率控制:我见过有人每秒上报一次,结果影子更新频率被限流了。建议5秒以上间隔

3.4 metadata元数据:时间戳的“审计日志”

metadata,很多人觉得它多余。其实不然。它记录了每个属性最后一次被更新的时间戳

为什么需要这个?

你想想看,如果设备离线了3小时,重新上线后,云端怎么知道哪些指令是“过期”的?metadata里的时间戳就是答案。

metadata的结构和state完全对应:

"metadata": {
  "desired": {
    "temperature": {
      "timestamp": 1623456789
    }
  },
  "reported": {
    "temperature": {
      "timestamp": 1623456790
    }
  }
}

每个属性都有一个timestamp字段。注意,这个时间戳是服务器时间,不是设备本地时间。我曾经犯过一个错:用设备本地时间上报,结果设备时间不准,导致metadata全乱套了。

小技巧:在调试时,可以对比desired和reported的timestamp。如果desired的时间戳比reported新,说明设备还没执行完指令。这个逻辑可以用来做“指令执行超时检测”。

3.5 version字段:并发控制的“锁”

version是个递增的整数。每次影子文档被更新,version就+1。

它的作用很简单:防止覆盖

假设云端同时收到两个更新请求:

  • 请求A:version=5,把温度改成26度
  • 请求B:version=5,把湿度改成70%

服务器处理完A后,version变成6。再处理B时,发现B带的version是5,和当前version不匹配,直接拒绝。这就避免了“后写的覆盖先写的”。

嗯,这里要注意:设备端在更新影子时,一定要带上当前已知的version。否则服务器会认为你是“非法请求”。

3.6 实际项目中的JSON设计建议

说了这么多理论,咱们来点实战的。我在一个智能家居项目里,总结了一套影子JSON的设计规范:

字段 建议 原因
属性名 全小写+下划线 避免大小写敏感问题
属性值 尽量用基本类型 嵌套对象解析容易出错
desired 只放可控属性 减少无效同步
reported 包含所有关键状态 方便云端做状态判断
metadata 不要手动修改 服务器自动维护

核心总结:

  • desired是“目标”,reported是“现状”
  • metadata是“时间戳审计”,version是“并发锁”
  • JSON结构要扁平、清晰、可预测

好了,这一节的内容就到这里。下一节咱们会讲设备如何通过MQTT协议和影子交互,包括怎么上报、怎么拉取、怎么处理冲突。到时候我会拿一个真实的STM32代码来演示,敬请期待。