WioLTEでAWS IoTのThing Shadowを利用する

私はSORACOMのSIMカードをサービス開始当初から2年近く維持しているものの、なかなか活用できていない、ということで思い切って同社主催のもくもく会(勉強会)に参加してみることにしました。(テキストはこちら)

そこで知り合ったのがWioLTEというデバイス。SIMカードを挿すだけでセンサーデータのアップロードなど、クラウドと連携したIoTの仕組みを簡単に開発できる、優れものです。

デバイス側の電子工作の勉強が面倒で後回しにしている私にうってつけの機材です。

さて、もくもく会でも本デバイスの基本的な使い方は十分に理解できましたが、クラウドとの連携を理解するには時間不足。
IoT関連の勉強を始める良いきっかけと思い、スターターキットを購入してみました。

AWS IoTと接続したい

アーキテクチャ

デバイスとクラウドと連携する場合は、上図のようなAWS IoT等のメッセージブローカーと接続するアーキテクチャを採用することが通常かと思います。

またAWS IoTでは便利なデバイスシャドウ(Thing Shadow)の機能がありますので、これを活用しデバイス⇔クラウドの双方向の情報交換を実現し、WioLTEオンボードのLEDの発光色をクラウドから制御してみたいと思います。

今回は温度センサーの情報をLambdaで判定し、閾値を超えた場合はデバイスシャドウ経由でWioLTEのLEDを点灯させるところまでを目標にします。
(※ WioLTE側で温度を判定すればいいのでは?というツッコミは無しで・・・)

デバイスシャドウ(Thing Shadow)とは

モノのインターネット(IoT)において、安定した通信経路が利用できるとは限りません。
また、消費電力削減の観点から定期的にデバイスをオフラインにしていることもあるでしょう。

そうした事情からクラウド⇒デバイスにデータを送る際、特別な仕掛けないとデバイス側で受信できたのか?状態を把握し再送処理を実装する必要があり、システムによっては数千、数万のデバイスを管理する際のスケールの妨げとなります。

そこで便利なのがAWS IoTのデバイスシャドウで、AWS IoTにて各デバイスの状態(reported)と、管理側からの指示内容(desired)を保持することができます。AWS IoTにて状態と指示内容の差異(delta)を自動的に抽出してくれるので、デバイスは差異の情報から処理を定義すれば良く実装が簡単になるメリットがあります。

詳しくは以下のURLがわかりやすいかと思います。

クラウド接続設定

デバイスがAWS IoTに接続できるように設定を行います。

AWS IoTの設定

まず、AWS IoTの設定を行います。
初めてアクセスした場合はこのような画面が出ますので、「今すぐ始める」を押しましょう。

デバイスポリシーの作成

まず、安全性→ポリシーより、デバイスポリシーの作成を行います。
最初は アクションを iot:* リソースARNを * とします。
(一旦全ての接続を許可して、後から絞っていくのが良いかと思います)

モノの登録

続いて管理→モノより、AWS IoT上で管理するモノ(thing)の登録を行います。
単一のモノの登録を選択した後、モノの名前を入力します。

続いて証明書の発行を行います。
今回は1-click証明書を選択します。

次の画面で以下のファイルがダウンロードできますので、ダウンロードし大事に保管します。

  • デバイス証明書(XXXXXXXXXXX-certificate.pem.crt)
  • プライベートキー(XXXXXXXXXXX-private.pem.key)
  • パブリックキー(XXXXXXXXXXX-public.pem.key)※今回は利用しない
  • シマンテック社ルート証明書(VeriSign-Class 3-Public-Primary-Certification-Authority-G5.pem)

最後に先に作成したデバイスポリシーをこのモノに適用して完了です。

なお、後続の設定にAWS IoTの設定画面で表示されているエンドポイント名が必要となるためメモしておきます。

SORACOM BEAMの設定

続いてSORACOM BEAMの設定を行います。
任意のSIMグループを作成し、その中でSORACOM BEAMの設定にMQTTエントリポイントを追加します。

MQTTエントリポイントの設定を行います。
プロトコルはMQTTS(ポート8883)、ホスト名は先程AWS IoTの設定画面でメモしたエンドポイント名を入力します。

証明書認証をONにし、認証情報入力ウィンドウを開きます。

 認証情報にはAWS IoTからダウンロードした証明書を登録します。

  • 秘密鍵:AWS IoTから発行したプライベートキー
  • 証明書:同デバイス証明書
  • CA証明書:シマンテック社ルート証明書

最後に、利用するSIMをSIMグループに参加させて、一連の接続設定は完了です。

デバイスシャドウ通信確認

デバイスシャドウの値によってLEDの発光色を変える機能の実装を行います。

WioLTE側ソースコード

WioLTE側のソースコードはスケッチ例のMQTTクライアントを参考に記載し、以下のようになりました。(20年ぶりにC言語を試行錯誤書いているので、稚拙な点はご容赦ください)

MQTTクライアントであるPubSubClientと、JSONパーサーであるArduinoJsonのライブラリを追加で利用しています。

下記コードはGitHubにも掲載しました。https://github.com/kizawa2020/iot_sample/blob/master/mqtt-client-shadowLED.ino

#include <WioLTEforArduino.h>
#include <WioLTEClient.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <stdio.h>

#define APN               "soracom.io"
#define USERNAME          "sora"
#define PASSWORD          "sora"

#define MQTT_SERVER_HOST  "beam.soracom.io"
#define MQTT_SERVER_PORT  (1883)

#define ID                "(デバイス名)"
#define OUT_TOPIC         "$aws/things/(デバイス名)/shadow/update"
#define IN_TOPIC          "$aws/things/(デバイス名)/shadow/update/delta"

WioLTE Wio;
WioLTEClient WioClient(&Wio);
PubSubClient MqttClient;

void callback(char* topic, byte* payload, unsigned int length) {
  // Receive payload contents
  char subsc[length];
  for (int i = 0; i < length; i++) subsc[i]=(char)payload[i];
  subsc[length]='\0';
  SerialUSB.println("### Subscribe");
  SerialUSB.println(subsc);

  // JSON parse
  StaticJsonBuffer<200> jsonBuffer;
  JsonObject& root = jsonBuffer.parseObject(subsc);

  if (!root.success()) {
    SerialUSB.println("parseObject() failed");
  }
  else{
    // Change LED color
    const char* ledcolor = root["state"]["LED"];

    int r = 0;
    int g = 0;
    int b = 0;

    if      (strcmp(ledcolor,"red") == 0)   { r = 255; }
    else if (strcmp(ledcolor,"green") == 0) { g = 255; }
    else if (strcmp(ledcolor,"blue") == 0)  { b = 255; }
    else if (strcmp(ledcolor,"yellow") == 0){ r = 255; g = 255; }
    else if (strcmp(ledcolor,"purple") == 0){ r = 255; b = 255; }
    else if (strcmp(ledcolor,"lblue") == 0) { g = 255; b = 255; }
    else if (strcmp(ledcolor,"white") == 0) { r = 255; g = 255; b = 255; }
    else if (strcmp(ledcolor,"off") == 0)   {          }
    else {
      SerialUSB.println("Incollect color ID");
      return;
    }
    SerialUSB.print("Change LED color to: "); 
    SerialUSB.println(ledcolor);
    Wio.LedSetRGB(r,g,b);

    // RePublish LED Status
    char data[100];
    sprintf(data,"{\"state\": {\"reported\" : {\"LED\" : \"%s\"}}}",ledcolor);
    MqttClient.publish(OUT_TOPIC, data);
    SerialUSB.println("### LED Status Sent."); 
  }
}

void setup() {
  delay(200);
  SerialUSB.println("");
  SerialUSB.println("--- START ---------------------------------------------------");
  
  SerialUSB.println("### I/O Initialize.");
  Wio.Init();
  
  SerialUSB.println("### Power supply ON.");
  Wio.PowerSupplyLTE(true);
  delay(500);

  SerialUSB.println("### Turn on or reset.");
  if (!Wio.TurnOnOrReset()) {
    SerialUSB.println("### ERROR! ###");
    return;
  }

  SerialUSB.println("### Connecting to \""APN"\".");
  if (!Wio.Activate(APN, USERNAME, PASSWORD)) {
    SerialUSB.println("### ERROR! ###");
    return;
  }

  SerialUSB.println("### Connecting to MQTT server \""MQTT_SERVER_HOST"\"");
  MqttClient.setServer(MQTT_SERVER_HOST, MQTT_SERVER_PORT);
  MqttClient.setCallback(callback);
  MqttClient.setClient(WioClient);
  if (!MqttClient.connect(ID)) {
    SerialUSB.println("### ERROR! ###");
    return;
  }
  int qos=0;
  MqttClient.subscribe(IN_TOPIC,qos);
  SerialUSB.println("### Setup completed.");

  // Send Initialize LED Status
  char *data = "{\"state\": {\"reported\" : {\"LED\" : \"off\"}}}";
  MqttClient.publish(OUT_TOPIC, data);
  SerialUSB.println("### LED Status Sent.");  
}

void loop() {
  MqttClient.loop();
}

解説

デバイスシャドウを用いてLED発光色のステータスを管理する仕組みになります。

#define ID "(デバイス名)" 
#define OUT_TOPIC "$aws/things/(デバイス名)/shadow/update"
#define IN_TOPIC "$aws/things/(デバイス名)/shadow/update/delta"

デバイス側からMQTTによってシャドウの値を更新する際は、トピック名 $aws/things/(デバイス名)/shadow/update にJSONデータをパブリッシュすることで実現できます。

デバイス側からは”reported”、クラウド(管理)側からは”desired”を登録することで、差異があれば”delta”が登録されているので、この”delta”をデバイス側で受信した際にデバイスの動作を指定することになります。

初期設定

デバイス起動時には setup() 関数が実行されますが、殆どサンプルコード同様です。
最後にLEDの初期状態(消灯状態)をAWS IoTにパブリッシュしています。

// Send Initialize LED Status
char *data = "{\"state\": {\"reported\" : {\"LED\" : \"off\"}}}";
MqttClient.publish(OUT_TOPIC, data);
SerialUSB.println("### LED Status Sent.");

ループ動作

Arduinoは setup() での初期設定が完了後は loop() 関数が繰り返し動作し続けますが、本プログラムでは MqttClient.loop() を呼んでポーリングを行っているのみとなります。

SubScribe登録している $aws/things/(デバイス名)/shadow/update/delta にて受信があった際には callback 関数が呼ばれ実行されます。
JSONデータをパースし指示の発行色データを入手、それに応じた色に発光しています。

最後に、変更した色情報をAWS IoTにパブリッシュして完了です。
(この時点で差分は無くなりdeltaは削除される)

動作確認

デバイスが起動すると、”LED”:”off”初期状態がデバイスシャドウに登録されます。
(AWS IoTコンソールのモノ→シャドウにあるシャドウドキュメントで確認できる)

このシャドウドキュメントに”desired” “LED”:”red”を登録すると・・・

Subscribeデータを受信して、

色が変わりました!

Lambdaによる温度の判定

Lambdaによって温度を判定しデバイスシャドウを書き換える部分の実装を行います。

WioLTE側ソースコード

上記のコードに、温度センサーのデータ取得、Topicへの送信のコードを付加します。

GitHubに掲載しましたので掲載は割愛します。
https://github.com/kizawa2020/iot_sample/blob/master/mqtt-shadowLED-sensor.ino

Lambdaの実装

IAMロールの作成

ロールを使用するサービスとしてLambda、
アタッチするポリシーに AWSIoTDataAccess を指定してロールを作成します。

Lambda関数の新規作成

今回は言語にPython3.6を指定、ロールには上記で作成したIAMロールを指定します。

トリガーにはAWS IoTを指定します。
Lambda設定側からAWS IoTのRule設定ができますので、今回は無条件でセンサーデータを送信するよう、SELECT * FROM ‘sensordata/#’ と指定しました。

Lambdaソースコード

以下のようになりました。意外と簡単ですね。
https://github.com/kizawa2020/iot_sample/blob/master/WioLTE_TempCheck.py

import boto3
import json
iot = boto3.client('iot-data')
thingName = '(デバイス名)'
temp_threshold = 30

def lambda_handler(event, context):
    # eventを分解
    temp = event['temp']
    humi = event['humi']

    if temp > temp_threshold:
        ledcolor_desired = "red"
    else:
        ledcolor_desired = "off"
        
    shadow_stream = iot.get_thing_shadow(thingName = thingName)
    shadow_string = json.loads(shadow_stream['payload'].read().decode('utf-8'))
    ledcolor_status = shadow_string['state']['desired']['LED']

    if ledcolor_desired != ledcolor_status:
        # IoT シャドウを更新
        payload = {"state": {"desired": {"LED": ledcolor_desired }}}
        response = iot.update_thing_shadow(
            thingName = thingName,
            payload = json.dumps(payload)
        )

以上の実装により、温度センサーのデータをLambdaで判定してデバイスシャドウ経由でLEDを点灯することができました。

感想

IoTの開発においてはセンサーデータの可視化など、デバイス⇒クラウドのデータの流れが一般的であり注目されますが、クラウド側からデバイスを制御できると色々可能性が広がるな、と感じました。

なお今回は間に合いませんでしたが、クラウド側からのデバイスシャドウ更新ははlambda等のプログラムから実施することが望ましいと思います。

WioLTEは私に良いきっかけを与えてくれました。
引き続きIoTの学習を進めたいと思っています。