从这一节课开始,我们来设计和实现设备影子。

什么是设备影子

我最早是在 AWS IoT 上面看到设备影子功能的,后来国内主流云服务上的 IoT 套件中都包含了设备影子的功能。设备影子已经是 IoT 平台的标配功能了,所以 Maque IotHub 也需要实现设备影子功能。
首先让我们来看一下各个平台对设备影子的描述。
阿里云

物联网平台提供设备影子功能,用于缓存设备状态。设备在线时,可以直接获取云端指令;设备离线时,上线后可以主动拉取云端指令。 设备影子是一个 JSON 文档,用于存储设备上报状态、应用程序期望状态信息。 每个设备有且只有一个设备影子,设备可以通过 MQTT 获取和设置设备影子来同步状态,该同步可以是影子同步给设备,也可以是设备同步给影子。

腾讯云

设备影子文档是服务器端为设备缓存的一份状态和配置数据。它以 JSON 文本形式存储。

简单来说,设备影子包含了两种主要功能:
服务端和设备端数据同步:
设备影子提供了一种在网络情况不稳定、设备上线下线频繁的情况下,服务端和设备端稳定数据同步的功能。
这里要说明的是,在 IotHub 之前实现的数据/状态上传,指令下发功能都是可以在网络情况不稳定的情况下,稳定实现单向数据同步的。
设备影子主要解决的是,当一个状态或者数据可以被设备和服务器端同时修改时,在网络状态不稳定的情况下,如何保持其在服务端和设备端的状态一致性。所以当你需要双向同步时,就可以考虑使用设备影子了。例如,智能灯泡的开关状态既可以远程改变,也可以在本地通过物理开关来改变,那么这个状态就需要在服务端和设备端保持一致。
设备端数据/状态缓存:
设备影子还可以作为设备状态/数据在服务端的缓存,由于它保证了设备端和服务端的一致性,所以在业务系统需要在获取设备上的某个状态时,只需要读取服务端的数据就可以了,不需要和设备进行交互,实现了设备和业务系统的解耦。

设备影子的数据结构

我们引用的阿里云和腾讯云的文档里面说的那样,设备影子是一个 JSON 格式的文档,每个设备对应一个设备影子,下面是一个典型的设备影子:

  1. {
  2. "state": {
  3. "reported": {
  4. "lights": "on"
  5. },
  6. "desired": {
  7. "lights": "off"
  8. }
  9. },
  10. "metadata": {
  11. "reported": {
  12. "lights": {
  13. "timestamp": 123456789
  14. }
  15. },
  16. "desired": {
  17. "lights": {
  18. "timestamp": 123456789
  19. }
  20. }
  21. },
  22. "version": 1,
  23. "timestamp": 123456789
  24. }

state:

  • reported,指当前设备上报的状态,业务系统如果需要读取当前设备的状态,以这个值为准;
  • desired,是指服务端希望改变的设备状态,但还未同步到设备上。

metadata:状态的元数据,内容是state中包含的状态字段的最后更新时间。 version:设备影子的版本timestamp:设备影子的最后一次修改时间

设备影子的数据流向

阿里云和腾讯云的设备影子的数量流向大体是一致的,细节上略有不同,这里我总结了和简化了一下,在 Maque IotHub 里,影子设备的数据流向包含两个方向。

服务端向设备端同步

当业务系统通过服务端的接口修改了设备影子之后,IotHub 会向设备端进行同步,这个流程分为四步。
第一步,IotHub 向设备下发指令 UPDATE_SHADOW,指令中包含了更新后的文档,以上面的设备影子文档为例子,其中最重要的部分是 desiredversion

  1. {
  2. "state": {
  3. ...
  4. "desired": {
  5. "lights": "off"
  6. }
  7. },
  8. ...
  9. "version": 1,
  10. ...
  11. }

第二步,设备根据 desired 里面的值去更新设备的状态,比如像这里就应该关闭智能灯。
第三步,设备向 IotHub 回复状态更新成功的信息,例如:

  1. {
  2. "state": {
  3. "desired": null
  4. },
  5. "version": 1,
  6. }

这里设备必须使用第二步得到 version 值,当 IotHub 收到这个回复时,检查回复里的 version 是否和设备影子里的一致。

  • 如果一致的话,那么将设备影子中 reported 里面字段的值修改为 desired 对应的值,同时删除 desired,并修改 metadata 里面相应的值。例如:

    1. {
    2. "state": {
    3. "reported": {
    4. "lights": "off"
    5. }
    6. },
    7. "metadata": {
    8. "reported": {
    9. "lights": {
    10. "timestamp": 123456789
    11. }
    12. }
    13. },
    14. "version": 1,
    15. "timestamp": 123456789
    16. }
  • 如果不一致的话,说明这期间影子设备又被修改了,则回到第一步,重新执行。

第四步,设备影子更新成功后,IotHub 向设备回复一条消息 SHADOW_REPLY

  1. {
  2. status: "succss",
  3. "timestamp": 123456789,
  4. "version": 1
  5. }

设备端向服务端同步

设备端的流程有三步。
第一步,当设备连接到 IotHub 时,向 IotHub 发起数据请求,IotHub 收到请求后会下发 UPDATE_SHADOW 指令,执行一次服务端向设备端同步,设备需要记录下当前影子设备的 version。
第二步,当设备的状态发生变化,比如通过物理开关关闭掉智能电灯的时候,IotHub 发送 REPORT_SHADOW 数据,包含第一步获得的 version,例如:

  1. {
  2. "state": {
  3. "reported": {
  4. "temperature": 27
  5. }
  6. },
  7. "version": 1
  8. }

当 IotHub 收到这个数据,检查 REPORT_SHADOW 里的 version 是否和设备影子里的数据一致:

  • 如果一致,那么用 REPORT_SHADOW 里的 reported 值修改设备影子 reported 的字段;
  • 如果不一致,那么 IotHub 会下发指令 UPDATE_SHADOW,执行一次服务端向设备端的同步。

第三步,IotHub 在接收到 REPORT_SHADOW 数据并成功修改设备影子后,向设备回复一条消息 SHADOW_REPLY

在同步时,如果不是修改字段的值,而是删除字段,那么,将字段的值设为 null 就可以了。

设备影子 Server API

同时,IotHub 需要向提供设备影子相关的接口,业务系统可以通过这些接口,对设备影子进行查询和修改。
业务系统通过这些接口修改设备影子时,设备影子的 version 也应该相应的加一。

这一节我们讨论和设计了 IotHub 的设备影子功能,下一节我们开始来实现设备影子的服务端功能。