デバイスシャドウ/デバイスツインを活用した実装のベストプラクティス (2018年末)

デバイスシャドウ、デバイスツインは、**デバイスの状態に関する情報 (メタデータ、構成、状態など) を格納する** ことを目的としています。
詳しくは AWS IoT Core の [Device Shadow](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/iot-device-shadows.html) や Microsoft Azure の [Device Twin](https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-devguide-device-twins) を読んでください。
（以下、AWS IoT Core を対象にし、名称はシャドウで統一します）

ここではシャドウの実装に関するベストプラクティスを紹介します。

**2019年1月2日追記:** [Wio LTE を対象とした実装を公開しました](https://qiita.com/ma2shita/items/baf95281cbde56e8dc16)

## 実装すべき内容3点

実装すべきは以下3点です。便宜上 `reporter` `updater` `restore` とします。[^1]

* `Reporter`
    * デバイス自身の状態を取得し、取得できた内容を `/update` へ送信する
* `updater`
    * `/update/delta` から得られた内容を基にデバイス自身の状態を更新する
* `restore`
    * `/get` に問い合わせをして、返ってきた内容を基にデバイス自身の状態を更新する

### Reporter

デバイス自身の状態を取得し、取得できた内容を `/update` へ送信します。
Reporter の実装は必須です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/22754/f5f29a57-6d7d-4188-b796-6096a99fab6f.png)

送信時の JSON ですが、例えば Digital I/O の D38 番の状態を 0 もしくは 1 で表現するなら、以下の通りです。(`d38`とか、値は自由に設定できます)

```json
{
  "state": {
    "reported": {
      "d38": 1
    },
    "desired": null
  }
}
```

詳しくは AWS IoT Core であれば [シャドウの更新](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/using-device-shadows.html#update-device-shadow) を見てください。

Reporter の実行は任意ですが、 **デバイスへの更新が発生した時には必ず実行してください**
そうしないと、デバイスとシャドウが同期しなくなり、シャドウの中身が役に立たなくなります。

例えば後述する Updater や Restore を実行した結果、デバイスの状態が変化したら無論 Reporter を実行する必要がありますが、実世界上からの干渉によってデバイスの状態が変化したときも Reporter を実行してください。(例えば、実世界でボタンが押された、等)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/22754/ddc11c9b-bcd0-ecb1-bf2b-7a30b0128461.png)

#### desired を「空」にする

`delta` は `state` が更新された毎に算出され、必要があれば出力されます。
これは `desired` を送った際にはあたりまえの動作となりますが、見落としがちなのが `reported` を送った時にも `delta` が計算されるということです。
結論から言えば、 `reported` 送付時に `desired` を「空」（実装レベルで言えば `null`) にする必要があります。

さもないと、以下のような問題が発生します。(後日記載します)

### Updater

`/update/delta` から得られた内容を基にデバイス自身の状態を更新します。
Updater の実装は任意です。AWS IoT Core からの状態更新要求を受け取らないのであれば実装しなくても問題ありません。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/22754/ebc2571f-90bb-feac-bc0c-5505ba2a3a12.png)

Reporter のところでも解説しましたが、 Device に反映したら Reporter を起動するようにします。

### Restore

`/get` に問い合わせをして、返ってきた内容を基にデバイス自身の状態を更新します。
Restore の実装は任意です。ここで解説する挙動が不要であれば実装しなくても問題ありません。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/22754/6cc80339-042b-3422-e536-bfa841027ee8.png)

AWS IoT Core の場合 `/get` に空文字 (例えば `{}`) を Publish すると `/get/accpeted` に以下のような JSON が返ります。

```json:/get/accepted
{
  "state": {
    "desired":{"d38":1},
    "reported":{"d38":0},
    "delta":{"d38":1}
  },
  "metadata":{...}
}
```

この中で重要なのは `reported` と `delta` です。

`state.reported` には後述する `reporter` が送信した "状態" が入っています。この内容を参照しながらデバイスを構成することで「復元」ができます。

`state.delta` には、オフライン中に発生した要求で、かつデバイスが適用すべき要求が入っています。この内容を参照しながらデバイスを構成することで「オフラインへの対応」ができます。

それぞれ実行するか否かは任意です。
たとえば状態復元しないのであれば reported は無視すれば良いわけです。

また「定期実行」と書いてありますが、定期的に sync することもお勧めです。

### Updater との実装共有について

「デバイスの状態を更新する」という関数/メソッドを作る際、`/get/accpeted` と `/update/delta` では JSON の階層が違うため、ちょっと工夫が必要です。

```json:/get/accpeted
{"state":{"delta":{"d38":1}...}
```

```json:/update/delta
{"state":{"d38":1}}
```

以上のようにトピックに応じて `state` の下の階層を掘り出してから使ってください。

## 全部を実装した場合のシーケンス

![image.png](https://qiita-image-store.s3.amazonaws.com/0/22754/a74ab011-f3ec-a4fe-2e4b-12ccbfff6828.png)

## その他に実装すべきこと

### 接続を維持する方法

シャドウはステートフル・プロトコル、すなわちセッションを維持する必要があるため、セッション切断時における再接続も実装が必要です。

再接続の必要性判定＆処理は Publish 直前に入れるのが良いでしょう。疑似コードで解説します。

```ruby
def publish(topic, payload) {
  if (!MQTT.is_connected) {
    MQTT.connect()
  }
  MQTT.publish(topic, payload)
}
```

ここで解説した `Restore` は `/get` へのpublishが含まれているため、`Restore` を定期的に実行することで自動的に再接続判定＆処理がなされるという事になります。

### Device ID

デバイスの ID をデバイスに埋め込むと、生産や交換が大変です。これを
(記載中: TODO)

## Appendix

シーケンス図は PlantUML で記述しました。(レンダリングには [PlantUML previewer](http://sujoyu.github.io/plantuml-previewer/) を使いました)

```:Reporter
participant Device
participant "AWS IoT Core"

Device -> Device: Device の状態を取得
Device -> "AWS IoT Core": `{state: {reported: ...}} ` を `/update` に Publish
```

```:Updater
participant Device
participant "AWS IoT Core"

group 起動時
  Device --> "AWS IoT Core": Subscribe to `/update/delta`
end

group `/update/delta` 着信時のコールバック
  Device <- "AWS IoT Core": `{state: {...}}` が返ってくる
  Device -> Device: `state` の内容を Device に反映
  Device -> "AWS IoT Core": **Reporter**
end
```

```:Restore
participant Device
participant "AWS IoT Core"

group 起動時
  Device --> "AWS IoT Core": Subscribe to `/get/accepted`
end

group 起動時 もしくは 定期実行
  Device -> "AWS IoT Core": `/get` に Publush
  Device <- "AWS IoT Core": `{state: {...}}` が返ってくる
  Device -> Device: `state.reported` の内容を Device に反映
  note right: 最後にデバイス自身が報告した状態を復元する
  Device -> Device: `state.delta` の内容を Device に反映
  note right: オフライン中に発生した要求を反映するため
  Device -> "AWS IoT Core": **Reporter**
end
```

## あとがき

新年からこんなことやってます。

[^1]: センスがないのは許してください