3、设备影子数据模型:desired状态、reported状态、metadata元数据
好,咱们今天来聊聊设备影子的数据模型。说实话,这个模型是整个影子机制的核心骨架。你把它搞懂了,后面做远程控制、状态同步,基本就是手到擒来的事。
设备影子的 JSON 文档,说白了就三个主要部分:desired、reported 和 metadata。我习惯把它们比作「我想要什么」、「我实际是什么」和「这些状态的历史记录」。嗯,这样理解起来就直观多了。
3.1 reported 状态:设备当前的真实情况
reported 是设备端主动上报的状态。比如温度传感器测到 25.3℃,它就往 reported 里写一条 "temperature": 25.3。这是设备「告诉」平台的真实数据。
核心原则:reported 里的数据,必须由设备端写入。平台端只能读取,不能直接修改。
我在项目中遇到过一个问题:有个同事试图在服务端直接修改 reported 字段,结果发现设备下一次上报时又把值覆盖回来了。你想想看,这其实就乱套了——平台改的跟设备实际状态不一致,那影子还有什么意义?
来看一个典型的 reported 数据片段:
{
"reported": {
"temperature": 25.3,
"humidity": 68,
"switch": "on",
"firmwareVersion": "v2.1.0"
}
}
这里每个字段都是设备真实采集到的值。设备每次上报,都会用新值替换旧值。注意,是整体替换还是部分更新?嗯,这取决于你用的 API。ThingsBoard 默认是合并更新,只修改你传的那些字段。
3.2 desired 状态:平台期望的目标值
desired 正好反过来。它是平台端(或者用户通过 App)下发的目标状态。比如你想远程打开一盏灯,就往 desired 里写 "switch": "on"。设备端读到这个变化后,执行开灯操作,然后把实际结果写回 reported。
我的习惯:desired 字段命名尽量跟 reported 保持一致。比如 reported 里用 switch,desired 里也用 switch。这样设备端做状态比对时,代码写起来特别清爽。
举个例子,用户通过手机 App 把空调目标温度设为 26℃:
{
"desired": {
"targetTemperature": 26,
"mode": "cool"
}
}
设备端轮询到 desired 变化后,执行动作,然后更新 reported:
{
"reported": {
"targetTemperature": 26,
"mode": "cool",
"currentTemperature": 25.8
}
}
这里有个关键点:设备端处理完 desired 后,要不要把 desired 里对应的字段删掉? 我个人建议删掉。否则下次轮询时,设备会认为又有新指令,导致重复执行。ThingsBoard 的官方做法是,设备端上报 reported 后,平台自动清除对应的 desired 字段。但如果你自己实现影子服务,这个逻辑得想清楚。
3.3 metadata 元数据:状态的「身份证」
metadata 记录了每个字段的更新时间。它不存业务数据,只存「这个值是什么时候变的」。你想想看,这玩意儿在调试和审计时有多重要?
比如你发现设备温度一直没变,到底是设备死机了,还是温度真的稳定?看一眼 metadata 里 temperature 的更新时间,如果已经 2 小时没更新了,那大概率是设备出问题了。
metadata 的结构长这样:
{
"metadata": {
"reported": {
"temperature": {
"timestamp": 1698765432000
},
"humidity": {
"timestamp": 1698765432000
},
"switch": {
"timestamp": 1698765432000
}
},
"desired": {
"targetTemperature": {
"timestamp": 1698765435000
}
}
}
}
每个字段都带一个 timestamp,单位是毫秒级的时间戳。我曾经在排查一个「设备响应延迟」的问题时,就是靠 metadata 发现:desired 的下发时间和 reported 的更新时间差了整整 30 秒。后来定位到是设备端的轮询间隔设得太长了。
注意:metadata 是平台自动维护的,你不需要手动写代码去更新它。但读取它来分析设备行为,是每个 IoT 工程师的必备技能。
3.4 三者之间的关系:一张表说清楚
我把这三个部分的核心区别整理成了一张表,方便你对照:
| 属性 | 谁写入 | 谁读取 | 用途 | 更新方式 |
|---|---|---|---|---|
| reported | 设备端 | 平台端、App | 上报设备真实状态 | 设备主动推送 |
| desired | 平台端、App | 设备端 | 下发期望目标 | 平台主动写入 |
| metadata | 平台自动维护 | 调试、审计 | 记录更新时间 | 自动生成 |
你看,这个分工其实很清晰。设备只管 reported,平台只管 desired,metadata 是旁观者。各司其职,互不干扰。
3.5 一个完整的影子文档示例
最后,咱们看一个完整的设备影子 JSON。这样你就能把三个部分串起来了:
{
"deviceId": "sensor-001",
"reported": {
"temperature": 25.3,
"humidity": 68,
"switch": "on"
},
"desired": {
"targetTemperature": 26
},
"metadata": {
"reported": {
"temperature": { "timestamp": 1698765432000 },
"humidity": { "timestamp": 1698765432000 },
"switch": { "timestamp": 1698765432000 }
},
"desired": {
"targetTemperature": { "timestamp": 1698765435000 }
}
}
}
这个文档告诉我们什么?
- 设备当前温度 25.3℃,湿度 68%,开关是开的。
- 平台希望设备把温度调到 26℃。
- reported 数据是 2 分钟前更新的,desired 是 30 秒前下发的。
嗯,信息量是不是很足?你只要学会读这个 JSON,设备当前的状态、平台的目标、以及时间线,全都一目了然。
避坑指南:我曾经在项目里犯过一个低级错误——把 desired 和 reported 的字段名搞混了。设备端上报的是 temp,平台下发的是 temperature。结果设备一直读不到 desired,因为字段名对不上。所以,命名规范一定要提前定好,最好写在接口文档里。
好了,这一节的内容就到这里。数据模型是影子的地基,地基打牢了,后面盖楼才稳。下一节咱们聊聊设备端怎么跟影子交互,包括 MQTT 主题和消息格式。到时候我会拿一个真实的传感器项目来演示,敬请期待。