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
}
看到没?最外层有三个核心字段:state、metadata、version。我个人习惯把version看作“版本锁”,防止并发写冲突。
3.2 desired区:云端对设备的“期望”
desired,说白了就是“云端想让设备变成的样子”。
举个例子:你在手机App上把空调温度调到26度。这个26度不会直接发给设备,而是先写到影子的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全乱套了。
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代码来演示,敬请期待。