AWS GreengrassのLocal Shadowを利用してLambdaの設定値の変更・永続化をする

Raspberry PiのGatewayにGreengrassを入れて使っていますが、機器からの値を取得する周期や、パラメータ等、Gateway側のLambdaから利用するパラメータを永続値として持っておきたいと思いました。

その上、その値はAWS IoT側を通して変更したい。

ということでLocal Shadowを利用して実現してみました。

概要図

今回やろうとしていることです。 Local Shadow とAWS IoTの Shadow の同期が一番やりたいことです。

f:id:yomon8:20200925223945p:plain

Lambdaコード

gg-shadow-init

test/settings/initialize というTOPICをトリガーとして1度だけ実行されます。

これにより、Local Shadowを初期化します。初期化しないで get_thing_shadow() を実行すると以下のような Shadow Not Found エラーとなりShadowを取得できませんでした。

※2020/10/23追記 Greengrass GroupにShadowが設定済みのDeviceを登録して「クラウドへのシャドウ同期」を設定し、デプロイすることで、この処理無しでもローカルShadowにデータが登録されることがわかりました。

exception: Request for shadow state returned error code 404 with message "Shadow Not Found"

初期化LambdaはMQTTでパラメータを渡してキックして一回だけ動かすことにします。 update_thing_shadow() で初期データを入れることでLocal Shadowができます。

import logging
import json

import greengrasssdk

THING_NAME = "my-ggc"

logger = logging.getLogger(__name__)
gg = greengrasssdk.client("iot-data")


def update(event):
    data = {"state": {"reported": event}}
    response = gg.update_thing_shadow(
        thingName=THING_NAME,
        payload=bytes(json.dumps(data), "utf-8"),
    )
    logger.info(f"Shadow Updated -> {response}")


def lambda_handler(event, context):
    logger.info("Start update")
    update(event)
    return

gg-shadow-publish-prams

Greengrassで良くあるように、Timerを使って再帰的にLoopを書いてます。

get_thing_shadow() でLocal Shadowから取得したパラメータをMQTTでAWS IoTにPublishします。これをAWS IoT側でSubscribeしてパラメータが変更されたことを確認します。

import datetime
import logging
import json
from threading import Timer

import greengrasssdk

THING_NAME = "my-ggc"
TARGET_TOPIC = "test/settings/get"

logger = logging.getLogger(__name__)
gg = greengrasssdk.client("iot-data")


def publish_params():
    response = gg.get_thing_shadow(
        thingName=THING_NAME,
    )

    logger.info(response)
    state = json.loads(response["payload"])["state"]["reported"]
    state['time'] = str(datetime.datetime.utcnow())

    gg.publish(
        topic=TARGET_TOPIC,
        queueFullPolicy="AllOrException",
        payload=bytes(json.dumps(state), "utf-8"),
    )
    logger.info(f"Published -> {state}")

    # loop
    Timer(10, publish_params).start()


logger.info("Start publish")
publish_params()


def lambda_handler(event, context):
    return

Greengrass Group設定

では、Greengrass Groupの設定に移ります。

Lambda

まずは上記の二つのLambdaをグループに設定します。

この際に gg-shadow-publish-prams は永続的にloopするので以下のように設定します。

サブスクリプション

以下の通り設定します。

src target topic usage
AWS IoT gg-shadow-init test/settings/initialize Local Shadowの初期化に一回だけ利用
gg-shadow-publish-prams AWS IoT test/settings/get AWS IoT側にShadowの内容をPublishして確認するため利用

以下のようになります。

Device

Local Shadowと同期するためのデバイスをグループに含めます。 1 Click Deployで my-ggc というデバイスを作成しておきます。証明書等は特にダウンロードしなくても大丈夫です。

デプロイ

Greengrass Groupをデプロイします。

動きを見てみる

ログ確認

まず、この時点でRaspberry Piに入ってログを確認してみます。

sudo tail -F /greengrass/ggc/var/log/user/ap-northeast-1/123456789012/gg-shadow-publish-params.log

上にも書きましたが、Local Shadowが無いので以下のようなエラーが出ています。

Request for shadow state returned error code 404 with message "Shadow Not Found"

Local Shadow確認(初期化前)

参考URLにもある通りLocal ShadowはsqliteのDBにて管理されているので確認してみます。

sudo sqlite3 /greengrass/ggc/var/state/shadow/shadow.db
SQLite version 3.27.2 2019-02-25 16:06:06
Enter ".help" for usage hints.
sqlite> select count(*) from doc;
0

0件ですね。上記の Shadow Not Found エラーも納得です。

Local Shadow初期化

AWS IoTのMQTTクライアントから test/settings/get をSubscribeしてみます。この時点では何も表示されないはずです。

その上で、test/settings/initialize に以下のデータをPublishしてみます。

{
  "param_str": "abc",
  "param_int": 123,
  "param_float": 1.23
}

初期化が成功すると以下のようにデータがtest/settings/get でSubscribeできるようになります。このデータは gg-shadow-publish-prams のLambdaからPublishされたものです。

f:id:yomon8:20200925223622p:plain

Local Shadow確認(初期化後)

今度はShadowのDBにデータが入っています。

sudo sqlite3 /greengrass/ggc/var/state/shadow/shadow.db
sqlite> select count(*) from doc;
1
sqlite> select * from doc;
my-ggc|my-ggc|{"ggFlv":{"reported":{"param_float":{"version":1},"param_int":{"version":1},"param_str":{"version":1}}},"isDeleted":false,"metadata":{"reported":{"param_float":{"timestamp":1601043622},"param_int":{"timestamp":1601043622},"param_str":{"timestamp":1601043622}}},"state":{"reported":{"param_float":1.23,"param_int":123,"param_str":"abc"}},"version":1}
sqlite>

Shadowを更新して設定を変更

Device my-ggc の設定を クラウドへのシャドウ同期 と変更します。ここでグループをデプロイします。

Shadowを確認すると、Local Shadowに設定した値が同期されていることがわかります。

Shadowの値を変更してみます。

MQTTクライアントに戻ってみるとShadowの変更前後で設定が変わっていることがわかります。

当然、sqliteで確認してみるとShadowDBの中身も変わっています。

sudo  sqlite3 /greengrass/ggc/var/state/shadow/shadow.db
sqlite> select * from doc;
my-ggc|my-ggc|{"ggFlv":{"reported":{"param_float":{"version":2},"param_int":{"version":2},"param_str":{"version":2}}},"isDeleted":false,"metadata":{"reported":{"param_float":{"timestamp":1601044265},"param_int":{"timestamp":1601044265},"param_str":{"timestamp":1601044265}}},"state":{"reported":{"param_float":9.87,"param_int":987,"param_str":"zyx"}},"version":2}

Shadowの操作はここに記載されています。例えばプロパティをnullにすれば、そのプロパティを消せるなどが書いてあります。

docs.aws.amazon.com

Shadowの正しい使い方

今回はわかりやすさを優先して、無視していましたが、Shadowには desiredreported などの項目が分かれており正しく使うにはShadowの考え方の理解が必要になります。

実は一番わかりやすい資料はAzureのDevice Twinという同様の機能の説明になりますので、こちらで記載しておきます。

f:id:yomon8:20210202102815p:plain

azure.microsoft.com

参考URL

dev.classmethod.jp

github.com