M5Stack Toughと土壌水分量センサーでプランター菜園をモニタリングするVol.3（AWS IoT編）

前回と前々回で電源とセンサーの接続が終わったので、いよいよIoT側の処理に入っていきます。

https://qiita.com/EuzoBass/items/fb4a8f379c2425ad942b

https://qiita.com/EuzoBass/items/b4bd99995d39b812ea56

## 今回の内容

センサーから取得したデータをAWSのサービスを使って蓄積し可視化していくため、下記の作業をしていくことになります。
- M5Stack用のプログラムの作成
- AWS IoTの設定
- 時系列データベース(Amazon Timestream)へのデータの蓄積
- 可視化 (Amazon Managed Grafana)の設定

## 全体構成
今回構築した際の全体構成概要はこちらです

![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/6a7406a8-35cf-a896-2d37-00b2cf0fd045.png)

M5Stack Toughからインターネットを経由して AWS IoT にパブリッシュします。後述しますが、その際タイムスタンプの値を修正する必要があるため、修正後に再度別なIoTトピックにパブリッシュを行います。その後、時系列データベースであるTimestreamに流し込んで、Grafana を利用して可視化するよう構成していきます。
その他、Grafanaにログインするための AWS SSO なども利用していますが、この図では割愛しています。

## M5Stack用のプログラムの作成
M5Stackで開発を行う場合、下記のようにいくつか方法があります。

| 言語 | ツール | 特徴（主観）|
| --- | --- | --- |
| UIFlow | UIFlow | GUI 上で処理が予め定義されたブロックを組み合わせて開発していく。プログラムを書けなくても開発できる反面、細かい制御をしようとすると出来なかったり、試行錯誤が必要だったりする |
| MicroPython | UIFlow | Python の組み込み向け言語。Pythonの完全互換ではないため、使えない記述やライブラリが存在する。UIFlowだけでは難しい処理を行う場合はお勧めだが、メモリの管理などハードに近い部分の制御は難しい |
| C++ | Arduino IDE | C++で開発するため、よりハードに近い処理ができ、MicroPythonやUIFlowで記述するよりも処理速度が速い。一方でC++を触ったことが無いと学習コストは高いかもしれない。 |

個人的には MicroPython が一番書きやすいのですが、今回は敢えてUIFlowで実装し、どこまでできるかの検証も兼ねてみることにしました。

はじめに UIFlow (https://flow.m5stack.com/) にアクセスします。M5Stack Tough を利用するので、*UIFlow1.0* を選択します。

![uiflow01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/a3007b37-38fa-bc85-fb51-68f8f3c876cc.jpeg)

**Api key** に M5Stack 起動時に表示されるAPI KEY を入力し、Deviceの左下にあるM5Stack Toughを選択して **OK** をクリックします。

![uiflow02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/44221e5f-be96-670b-790d-6bcc075d509f.jpeg)

表示された UIFlow の画面で処理のブロックを組み合わせていきます。

![uiflow03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/8b1c2e46-e4e1-9511-573b-3c82645cfdb3.jpeg)


最終的に出来上がったものがこちらです。以降で順番に解説していきます。

![uiflow.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/73c7a9f3-bbd0-8b3f-d690-487d23c9ce4a.jpeg)

## 全体の処理の流れ
今回の処理は下記の流れで行います

1. WiFi 接続と接続確認 
1. NTPとの時刻同期
1. AWS IoT の接続設定
1. センサーからの値取得
1. AWS IoT へのデータ送信

これらの中で、WiFiのステータス確認と時刻同期はAWS IoT との通信がエラーになった際にも影響を受けづらくするため、別スレッドで実行するようにしました。UIFlowでのマルチスレッド処理についてはまた別な機会に書こうと思います。

## AWS IoT の設定
起動時に WiFiやNTPの設定を行い、AWS IoT との接続に必要な設定を入れていきます。

| 設定項目 | 設定値 |
| ----- | ----- |
| Init things name | モノの名前 |
| host | AWS IoT の ATS エンドポイント |
| port | 8883 |
| keepalive | 1200 |
| keyFile | AWS IoT のモノに紐づけられたプライベートキーファイル |
| certFile | AWS IoT のモノに紐づけられたデバイス証明書 |

最後に **AWS start** のブロックを入れて初期化を行います。

![uiflow04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/4a22ab6d-5b22-61b7-af81-2044f5871de8.jpeg)

## センサーからのデータ取得

センサーの値は下記を参考に温度と湿度を取得します。

| Name | Register | R/W | Data Length (bytes) |
| --- | --- | --- | --- |
| GET_CAPACITANCE | 0x00 | R | 2 |
| SET_ADDRESS | 0x01 | W | 1 |
| GET_ADDRESS | 0x02 | R | 1 |
| MEASURE_LIGHT | 0x03 | W | 0 |
| GET_LIGHT | 0x04 | R | 2 |
| GET_TEMPERATURE | 0x05 | R | 2 |
| RESET | 0x06 | W | 0 |
| GET_VERSION | 0x07 | R | 1 |
| SLEEP | 0x08 | W | 0 |
| GET_BUSY | 0x09 | R | 1 |

https://github.com/Miceuz/i2c-moisture-sensor/blob/master/README.md

前回のブログでも少し触れましたが、今回のセンサーのインターフェースはM5Stack Toughの **Port A** に接続されています。UIFlow で **Hardware** - **I2C Master** の Init にI2Cに係わる設定がありますので、ここから選んでいきます。

![uiflow07.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/927967ae-dd59-ae47-94ab-b8c25cd30709.jpeg)

下記の絵のようにブロックを組み合わせて処理を作成していきます。

![uiflow05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/99b532a2-3c4c-e38a-57d5-b1e82a604647.jpeg)

このセンサーは下記にも記載があるように300000Hzで接続されているため、 **freq** には *300000* と入力します。
https://github.com/scopelemanuele/pyChirpLib/blob/master/example/find_chirp.py#L6

また、I2Cのアドレスは0x20ですので、 **Set slave addr** のところに *0x20* と記述します。

![uiflow06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/346681e6-92c4-125f-f5e0-3ef5f0c18df4.jpeg)

設定は *i2c0* というオブジェクトに格納されます。これで値を読み取る準備が出来ました。

次に実際のセンサーの値を読み取ります。湿度は上記の表のようにレジスタのアドレス *0x00* の値を取得します。このセンサーには土中の温度を取得する温度センサーも内蔵されているため、同様にアドレス *0x05* の温度の値も取得します。温度に関しては、1桁高くして整数値で扱うように設定されているため、小数で処理できるように floatの10で割ります。

https://github.com/scopelemanuele/pyChirpLib/blob/1f95dc4975d57d2a6431ed9cac0f4110d3599fd9/example/chirp.py#L79-L81

以上でセンサーの値を取得することが出来ました。

## AWS IoT への送信

最後にAWS IoT にデータを送信してあげます。

![uiflow08.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/70f796b0-ba14-89f5-e73d-5ad85f0ecff4.jpeg)

AWS IoT へは JSON 形式でデータを送ることになります。また、センサーの値を取得したときのタイムスタンプも必要になるため、**Get RTC time** で現在日時を取得して一緒に送信してあげることにします。下記のようになるように送ってみます。

```json
{
    "timestamp": 現在時刻,
    "moisture" : 400, (3桁整数)
    "temperature" : 30.0 (小数点以下1桁)
}
```

現在時刻を取得する際、M5Stack では NTP 配下にある **Get timestamp** や今回のように、Hardware - RTC 配下の **Get RTC time** のようにいくつか方法があります。Get timestamp を利用した場合、2000/01/01からの経過秒数になるようで、その後の時間の計算が難しくなってしまいます。余計な計算負荷を掛けたくなかったことと、正確な時間を送りたかったため、今回は1970/01/01からの経過時間で送ってくれるGet RTC timeを利用することにしました。

https://docs.m5stack.com/en/uiflow/advanced/ntp

さて、実際のAWS IoT 側での受信結果がこちらです。Get RTC time で送った結果、Timestampが配列になってしまっています。

![uiflow09.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/217ea7cc-8a5c-3244-e615-eab26f2e0130.jpeg)

AWS IoT ではトピックで受信したメッセージを後続のサービスにルーティングする際に作成する**ルール**にてSQL文で必要なメッセージのみ抽出したり、メッセージを修正したりすることが出来ます。今回は上で受信した配列形式の *timestamp* の値を経過秒数に変換するため、**time_to_epoch** 関数で時刻を経過時間に変換を行います。

https://docs.aws.amazon.com/iot/latest/developerguide/iot-sql-functions.html#iot-sql-function-time-to-epoch

基本形はこのように、2つの変数の前半が変換対象の文字列、後半は変換する際の変換元の文字列のフォーマット文字列を記述します。
```SQL
time_to_epoch(String, String)
```

たとえば、"2020-04-03 09:45:18 UTC+01:00" の1970/01/01からの経過時間を計算したい場合は、下記のように記述します。

```SQL
time_to_epoch("2020-04-03 09:45:18 UTC+01:00", "yyyy-MM-dd HH:mm:ss VV") = 1585903518000
```

同様にGet RTC timeで取得した配列の時刻も計算していきますが、0時～9時のように1文字になってしまう場合、都合よく0で埋める関数が無いため、CASE文で囲いながら1桁or2桁を判別して0で埋める処理を地道に作っていきます。出来上がったSQL文が下記です。これをIoTのルールのSQLステートメントに記述します。

```SQL
SELECT
    topic(2) AS deviceid,
    moisture,
    temperature,
    time_to_epoch(
        get(timestamp, 0) + "-" +
        CASE length(get(timestamp, 1))
            WHEN 1 THEN '0' + get(timestamp, 1)
            WHEN 2 THEN get(timestamp, 1) END
        + "-" +
        CASE length(get(timestamp, 2))
            WHEN 1 THEN '0' + get(timestamp, 2)
            WHEN 2 THEN get(timestamp, 2) END
        + " " +
        CASE length(get(timestamp, 4))
            WHEN 1 THEN '0' + get(timestamp, 4)
            WHEN 2 THEN get(timestamp, 4) END
        + ":" +
        CASE length(get(timestamp, 5))
            WHEN 1 THEN '0' + get(timestamp, 5)
            WHEN 2 THEN get(timestamp, 5) END
        + ":" +
        CASE length(get(timestamp, 6))
            WHEN 1 THEN '0' + get(timestamp, 6)
            WHEN 2 THEN get(timestamp, 6) END
        + " UTC+09:00",
        "yyyy-MM-dd HH:mm:ss VV")
    AS sampletime
FROM 'iotfarm/#'
```

次にルーティング先をルールアクションで定義していきます。ここでは、IoT トピックに再度パブリッシュするようにします。上記のSQL文を記述することでデータは成型されているので、そのままTimestreamに流してあげればよさそうに思えますが、Timestream に流す際に指定する timestamp の定義には256文字までしか入れられず、上のSQL文（time_to_epochで囲った範囲)が600文字以上あるためエラーになってしまいました。。。というわけで、新しく *iotfarm-m5t* というトピックに対してパブリッシュするように構成していきます。併せて、障害時に調査できるように、エラーアクションでCloudWatch logsにエラーログを出力するようにしておきます。

![iot_rule01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/855a72d0-6faa-a588-2478-a53abdc9a10d.jpeg)

これにより、新しいIoTトピックに対して下図のようなデータがパブリッシュされていくようになります。

![awsiot01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534832/11a50ca8-af13-daf1-948c-793db37af7a7.jpeg)

## ここまでのまとめ
- I2Cでデータを送る際はデータシートと突き合わせながらどのアドレスにどの値が設定されているか確認しながら進めます
- 細かな制御をしようとするとUIFlowのブロックだけでは難しくなることもありますが、今回の範囲であればブロックでも十分対応が出来ました
- M5Stack Toughからタイムスタンプを送る際はいくつか機能が用意されていますが、意図したデータがきちんと送られているかチェックし、必要に応じてAWS IoT の機能などもうまく使いながら修正を行っていきます

## 次回

AWS IoT のルールアクションにて Timestream に流し込むための設定を行っていきます。