这一节我们来设计和实现 IotHub 设备影子服务端的功能
服务端需要对设备影子进行存储,在业务系统修改设备影子时,需要将设备影子同步到设备端,同时还需要处理来自于设备的设备影子同步消息来将设备端的数据同步到数据库中。
最后服务端还要提供接口供业务系统查询和修改设备影子。

存储设备影子

我们在 Device 模型里新增一个字段shadow来保存设备的影子,一个空的设备影子应该是:

  1. {
  2. "state":{},
  3. "metadata":{},
  4. "version":0
  5. }

我们按照这个来设置这个字段的默认值:

  1. //IotHub_Server/models/device.js
  2. const deviceSchema = new Schema({
  3. ...
  4. shadow:{
  5. type: String,
  6. default: JSON.stringify({
  7. "state":{},
  8. "metadata":{},
  9. "version":0
  10. })
  11. }
  12. })

下发设备影子相关的指令

IotHub 需要向设备发送两种设备影子相关的指令,一个是更新影子,这里使用指令名$update_shadow,另外一个是成功更新设备影子后,对设备的回复信息,这里使用指令名$shadow_reply。发送这两条指令使用 IotHub 指令下发的通道就可以了。

设备端发送影子相关消息

设备端会向 IotHub 发送三种影子相关的消息,IotHub Server 需要对这些消息进行回应:

  • 设备主动请求影子数据,使用设备数据请求的通道,resource 名为”$shadow”;
  • 设备更新完状态后向 IotHub 回复的消息,这里我们使用上传数据的通道,将 DataType 设为”$shadow_updated”;
  • 设备主动更新影子数据,这里我们使用上传数据的通道,将 DataType 设为”$shadow_reporeted”。

    服务端更新设备影子

    Server API

    IotHub 提供一个接口供业务系统修改设备的影子,接收一个 JSON 对象 {desired:{key1=value1, ...}, version=xx}作为参数,业务系统在调用时需要提供影子的版本,以避免业务系统用老版本的数据覆盖当前的。
    1. //IotHub_Server/routes/devices.js
    2. router.put("/:productName/:deviceName/shadow", function (req, res) {
    3. var productName = req.params.productName
    4. var deviceName = req.params.deviceName
    5. Device.findOne({"product_name": productName, "device_name": deviceName}, function (err, device) {
    6. if (err != null) {
    7. res.send(err)
    8. } else if (device != null) {
    9. if(device.updateShadowDesired(req.body.desired, req.body.version)){
    10. res.status(200).send("ok")
    11. }else{
    12. res.status(409).send("version out of date")
    13. }
    14. } else {
    15. res.status(404).send("device not found")
    16. }
    17. })
    18. })
    如果业务系统请求的 version 大于当前的影子 version,则更新影子的 desired 字段,以及相关的 metadata 字段,更新成功以后向设备下发指令”$update_shadow”:
    1. //IotHub_Server/models/device.js
    2. deviceSchema.methods.updateShadowDesired = function (desired, version) {
    3. var ts = Math.floor(Date.now() / 1000)
    4. var shadow = JSON.parse(this.shadow)
    5. if (version > shadow.version) {
    6. shadow.state.desired = shadow.state.desired || {}
    7. shadow.metadata.desired = shadow.metadata.desired || {}
    8. for (var key in desired) {
    9. shadow.state.desired[key] = desired[key]
    10. shadow.metadata.desired[key] = {timestamp: ts}
    11. }
    12. shadow.version = version
    13. shadow.timestamp = ts
    14. this.shadow = JSON.stringify(shadow)
    15. this.save()
    16. this.sendUpdateShadow()
    17. return true
    18. } else {
    19. return false
    20. }
    21. }
    22. deviceSchema.methods.sendUpdateShadow= function(){
    23. this.sendCommand({
    24. commandName: "$update_shadow",
    25. data: this.shadow,
    26. qos: 0
    27. })
    因为设备在连接时还会主动请求一次影子数据,所以这里使用 qos=0 就可以了。

    响应设备端影子消息

    影子数据请求

    在收到 resource 名为$shadow的数据请求后,IotHub 应该下发”$update_shadow”指令:
    1. //IotHub_Server/services/message_service.js
    2. static handleDataRequest({productName, deviceName, resource, payload, ts}) {
    3. if (resource.startsWith("$")) {
    4. ...
    5. } else if (resource == "$shadow_updated") {
    6. Device.findOne({product_name: productName, device_name: deviceName}, function (err, device) {
    7. if (device != null) {
    8. device.sendUpdateShadow()
    9. }
    10. })
    11. }
    12. }
    13. ...
    14. }

    状态更新回复

    在收到 DataType=”$shadow_updated” 的上传数据后,IotHub 应该按照数据的内容对设备影子进行更新:
    1. //IotHub_Server/service/message_service.js
    2. static handleUploadData({productName, deviceName, ts, payload, messageId, dataType} = {}) {
    3. if (dataType.startsWith("$")) {
    4. if (dataType == "$shadow") {
    5. Device.findOne({product_name: productName, device_name: deviceName}, function (err, device) {
    6. if (device != null) {
    7. device.updateShadow(JSON.parse(payload.toString()))
    8. }
    9. })
    10. }
    11. } else {
    12. ...
    13. }
    14. }
    更新时需要先检查回复的 version,同时如果 desired 中的字段值为 null 的话,需要在 reported 里面删除相应的字段,更新成功后需要回复设备:
    1. //IotHub_Server/models/device.js
    2. deviceSchema.methods.updateShadow = function (shadowUpdated) {
    3. var ts = Math.floor(Date.now() / 1000)
    4. var shadow = JSON.parse(this.shadow)
    5. if (shadow.version == shadowUpdated.version) {
    6. if (shadowUpdated.state.desired == null) {
    7. shadow.state.desired = shadow.state.desired || {}
    8. shadow.state.reported = shadow.state.reported || {}
    9. shadow.metadata.reported = shadow.metadata.reported || {}
    10. for (var key in shadow.state.desired) {
    11. if (shadow.state.desired[key] != null) {
    12. shadow.state.reported[key] = shadowUpdated.state.desired[key]
    13. shadow.metadata.reported[key] = {timestamp: ts}
    14. } else {
    15. delete(shadow.state.reported[key])
    16. delete(shadow.metadata.reported[key])
    17. }
    18. }
    19. shadow.timestamp = ts
    20. shadow.version = shadow.version + 1
    21. delete(shadow.state.desired)
    22. delete(shadow.metadata.desired)
    23. this.shadow = JSON.stringify(shadow)
    24. this.save()
    25. this.sendCommand({
    26. commandName: "$shadow_reply",
    27. data: JSON.stringify({status: "success", timestamp: ts, version: shadow.version}),
    28. qos: 0
    29. })
    30. }
    31. } else {
    32. this.sendUpdateShadow()
    33. }
    34. }

    设备主动更新影子

    在收到 DataType=”$shadow_reported” 的上传数据后,IotHub 应该按照数据的内容对设备影子进行更新:
    1. //IotHub_Server/services/message_service.js
    2. static handleUploadData({productName, deviceName, ts, payload, messageId, dataType} = {}) {
    3. if (dataType.startsWith("$")) {
    4. ...
    5. else if("$shadow_updated"){
    6. Device.findOne({product_name: productName, device_name: deviceName}, function (err, device) {
    7. if (device != null) {
    8. device.reportShadow(JSON.parse(payload.toString()))
    9. }
    10. })
    11. }
    12. }
    13. ...
    14. }
    在更新影子时也需要检查 version 和 null 字段:
    1. //IotHub_Server/models/device.js
    2. deviceSchema.methods.reportShadow = function (shadowReported) {
    3. var ts = Math.floor(Date.now() / 1000)
    4. var shadow = JSON.parse(this.shadow)
    5. if (shadow.version == shadowReported.version) {
    6. shadow.state.reported = shadow.state.reported || {}
    7. shadow.metadata.reported = shadow.metadata.reported || {}
    8. for (var key in shadowReported.state.reported) {
    9. if (shadowReported.state.reported[key] != null) {
    10. shadow.state.reported[key] = shadowReported.state.reported[key]
    11. shadow.metadata.reported[key] = {timestamp: ts}
    12. } else {
    13. delete(shadow.state.reported[key])
    14. delete(shadow.metadata.reported[key])
    15. }
    16. }
    17. shadow.timestamp = ts
    18. shadow.version = shadow.version + 1
    19. this.shadow = JSON.stringify(shadow)
    20. this.save()
    21. this.sendCommand({
    22. commandName: "$shadow_reply",
    23. data: JSON.stringify({status: "success", timestamp: ts, version: shadow.version}),
    24. qos: 0
    25. })
    26. } else {
    27. this.sendUpdateShadow()
    28. }
    29. }

    查询设备影子详情

    最后只需要在设备详情接口返回设备影子的数据就可以了:
    1. //IotHub_Server/models/device.js
    2. deviceSchema.methods.toJSONObject = function () {
    3. return {
    4. ...
    5. shadow: JSON.parse(this.shadow),
    6. }
    7. }
    这一节我们完成了 IotHub 设备影子的服务端实现,下一节我们来实现设备影子的设备端实现,并写一些代码来验证这个功能。