AWS IoT Enterprise Buttonの入力結果をLambda経由でDynamoDBに保存し、API GatewayからM5Flowを使ってM5Stack Core2に表示する

この記事は[メディカルハッカソン](https://moicen-forest.studio.site/2)で作成されました。

積みガジェットや普段使わない技術を触ってみる目的で色々組み合わせてIoT数取器を作ってみた。

# 構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/dc9b07ec-bf83-4ba1-7702-baa93e0b1059.png)

# AWS IoT Enterprise Button - Lambda - DynamoDB
## AWS IoT Enterprise Buttonの設定

iPhoneから初期設定を行います。AppleStoreからAWS IoT 1-Clickをインストールします。

https://apps.apple.com/jp/app/aws-iot-1-click/id1315204832

AWS ルートユーザでログインした後、リージョンの設定を行います。今回は東京リージョン。

<img width="300" alt="AWS IoT 1-Click" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/008a8e3a-2c31-e150-94ce-f713a88d0095.jpeg">

デバイスを登録します。
<img width="300" alt="button set up" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/045dd9d5-7028-2123-8edb-e6f468a5fa74.jpeg">

デバイスIDを登録でスキャンを選択し、AWS IoT Enterprise Buttonの外装に張られたバーコードを読み取ります。
<img width="300" alt="button scan" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/8e80d529-1f9f-1be5-c9d1-981c9ef26d30.jpeg">

インターネットに接続するためWi-Fiの設定もしておきます。2.4GHz帯を使います。
<img width="300" alt="button wi-fi" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/9f36db94-22f3-41e2-b6a6-358b0b290cef.jpeg">

ブラウザでAWS IoT 1-Clickに接続し、登録状況を確認します。デバイスが無効になっている場合、有効にしてください。アプリからでも可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/bb73b30d-f27c-01dc-dd83-eebba5f8d1c0.png)

また、動作確認のためボタンを押下（SINGLE、DOUBLE、LONG）してイベントログを確認します。LONGは1.5sec以上の押下で判定されるようです。アプリからでもブラウザからでも確認できます（画像はアプリ）。remainingLifeは電池の残量ですが、電池交換可能なデバイスだと意味が異なったりするようです。
<img width="300" alt="button event log" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/a54f4e78-1338-9891-cabc-e1e248ad2401.png">

## DynamoDBの設定
マネジメントコンソールから、テーブルの作成を選択し、パーティションキーとソートキーを入力します。テーブルの名前は適当に設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/8cde11e0-628b-00ac-448d-a6abb501f9f7.png)

テーブルの詳細がこんな感じになっていればOK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/e71ed25b-6250-f939-5734-ba8a9ac55ce6.png)


## Lambdaの設定
関数の作成で、一から作成を選び、ランタイムにPython3を選択します。適当な関数名を付けて、その他はデフォルトのまま作成完了します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/f69721fd-4b02-f64a-1b8b-531c0062c89f.png)

コードソースを以下のように編集してデプロイします。DynamoDBのテーブル名は先ほど作成したテーブル名を入力してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/b6f46550-673d-f284-14e8-9a0e5453a57c.png)


```python3:lambda
import json
import boto3
import datetime

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('your table') # DynamoDBテーブル名を入力

def lambda_handler(event, context):
    clickType = event['deviceEvent']['buttonClicked']['clickType']
    timestamp = event['deviceEvent']['buttonClicked']['reportedTime']
    
    # クリックタイプとタイムスタンプをDynamoDBに保存
    response = table.put_item(
        Item={
            'clickType': clickType,
            'timestamp': timestamp
        }
    )
    
    return {
        'statusCode': 200,
        'body': json.dumps('ClickType and Timestamp saved successfully!')
    }
```

関数に割り当てたIAMロールのポリシーにDynamoDBへの書き込み権限を付与します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/26eb7df6-7be8-8e8d-1e78-553eb3315bba.png)

## IAMロールのポリシー設定
Lambdaの実行ロールのリンクからIAMのロールを表示し、許可ポリシーを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/d881e889-3cdb-1cbc-fbdf-fee1afc1a43d.png)

DynamoDBの書き込み権限を付与します。最小権限にするため、もっと絞ってもよさそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/2684bf4e-5de0-fc9d-1e94-0ea7a5ae18c3.png)

## AWS IoT 1-Clickの設定

プロジェクトを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/52f4bd91-2f9d-aee4-0ce8-e6112ee4ab89.png)

デバイステンプレートのアクションでLambda関数を選択して、プロジェクトを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/df9800d8-ddd8-ce91-911d-c64b9225ab15.png)

作成したプロジェクトにデバイスを割り当てます。プロジェクトの詳細からプレイスメントを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/ed500896-b479-dab6-3b03-1ab8ceaf82d9.png)

AWS IoT Enterprise Buttonを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/38877306-8875-bee8-0801-8595f8d3e3e5.png)

## 動作確認
AWS IoT Enterprise Buttonを押下し、DynamoDBに値が保存されていることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/c7843fca-4b28-c0bc-dacd-d3887877d853.png)

AWS IoT 1-Clickのプロジェクトからデバイスのアクティビティを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/51b951d6-125e-bfa3-1f21-9b77f87b3d25.png)

# DynamoDB - Lambda - API Gateway - M5Stack Core2

## Lambda作成
同様に関数を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/092594bc-5087-c541-8223-4285f8de0cca.png)

コードを設定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/7147f27b-0bd7-0e5a-aa5a-512400621763.png)

```python3:lambda
import json
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('your table')

def lambda_handler(event, context):
    response = table.scan()
    items = response['Items']
    
    return {
        'statusCode': 200,
        'body': json.dumps(items)
    }
```

## IAMロールのポリシー設定
Lambdaで使用中のロールの許可ポリシーに、DynamoDBの読み取り権限を付与します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/1706a754-83df-a88a-8e6e-0092d5f637e4.png)

# API Gateway設定
マネジメントコンソールからAPI Gatewayを開き、HTTP APIを構築します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/2561dc50-04f1-c3fd-58ad-65fdc4a1e451.png)

バックエンドに先ほど作成したLambdaを選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/6a83f0f4-000e-ef0d-99c4-ec0d4832fe1d.png)

ルートを設定します。読み取りだけなのでGETだけでOK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/c7ab76d3-4bd0-4500-0ef8-0f6a1fa0afbd.png)

ステージに対応したURLに設定したパスを付けて、ブラウザで読み込んでみましょう。DynamoDBのデータが表示されれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/3e57489c-8707-fb2b-c84a-01e1e4ad0f81.png)

## M5Stack Core2 IoT設定
M5BurnerでM5Stack Core2の最新UIFlowをダウンロードします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/f73fed36-d664-2f47-8a97-ef787d9344a3.png)

使用するWi-Fi情報を設定し、M5Stack Core2 IoTとM5Burnerを起動中のPCをUDBケーブルで接続し書き込みます。正常に書き込み完了し、起動すると「チーン！」と音がします。結構うるさい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/2cbcbe9f-f83c-eff4-1b2b-a5c253836509.png)

Wi-Fiの設定をします。SSIDとIPが表示されます。
<img width="300" alt="m5 set up" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/34745f9e-e580-221b-f56f-1e106e28256a.jpeg">

スマホ等でネットワークに接続してブラウザにIPアドレスを入力すると、設定画面が表示されます。自宅のWi-Fi情報を入力してConfigureします。
<img width="300" alt="m5 wifi" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/87362468-8e8b-8698-56e4-95bf5d76db49.jpeg">

設定が完了するとM5Stackが再起動します。Wi-Fi接続に成功するとAPI KEYが表示されます。
<img width="300" alt="m5 reboot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/32e70432-f84d-000f-85f9-6058072253f8.jpeg">

## UIFlow設定
[M5Flowのページ](https://flow.m5stack.com/)から設定を行います。
M5Stackの画面に表示されたAPI KEYを入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/440b8a86-d916-6172-4ef4-b6bcafdddee3.png)

画面のレイアウトを決めてラベルを設定します。今回は数取器に今日のカウント機能と合計を表示できるようにしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/fd1d8098-63da-c01c-2369-985e3518799d.png)


## Lambda応答をUIFlowで使いやすいように修正
作成済みのLambdaのコードを修正します。
ClickTypeがSINGLEなら+1、DOUBLEなら-1のようにカウントダウン機能も付けます。
本日分だけを返却してほしいのでフィルタ処理を書きました。
また、M5Flowのラベルにそのまま表示させたいので応答を0埋め4桁の文字列にしました。

```python3:today_count
import json
import boto3
from datetime import datetime, timezone

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('your table')

def is_today(timestamp_str):
    timestamp = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
    today = datetime.now(timezone.utc).date()
    return timestamp.date() == today

def lambda_handler(event, context):
    response = table.scan()
    items = response['Items']
    
    today_data = [item for item in items if is_today(item['timestamp'])]
    
    single_count = sum(item['clickType'] == 'SINGLE' for item in today_data)
    double_count = sum(item['clickType'] == 'DOUBLE' for item in today_data)
    result = single_count - double_count
    result_str = f"{result:04d}"
    
    return {
        'statusCode': 200,
        'body': result_str
    }
```

API Gatewayのエンドポイントにアクセスし想定通りの応答が返ってくることを確認します。
また、IoT Button押下のSINGLE/DOUBLEによって増減することを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/7a7614ba-9778-b590-231e-aab5154c47b8.png)

M5FlowでHTTP Requestの結果をラベルに表示させるブロックを組みRunで実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/e1049766-ad15-f86c-8037-84b4cc9b4454.png)

上手く表示されていればOK。心なしか変更前のラベル値が薄っすら見えます。なぜ。
<img width="300" alt="m5 today count" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/8df9f8e0-0353-864c-34bc-4f8ba764f95b.jpeg">

合計値を返すエンドポイントも作成しましょう。LambdaとAPI Gatewayをそれぞれ用意する必要があります。Lambdaの関数コピー機能は無さそうなので地道にやります。ロールを使い増せばIAM変更は省略できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/68999247-6a80-5717-1f6a-975ae42a6014.png)

```python3:total_count
import json
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('your table')

def lambda_handler(event, context):
    response = table.scan()
    items = response['Items']
    
    single_count = sum(item['clickType'] == 'SINGLE' for item in items)
    double_count = sum(item['clickType'] == 'DOUBLE' for item in items)
    result = single_count - double_count
    result_str = f"{result:04d}"
    
    return {
        'statusCode': 200,
        'body': result_str
    }
```

API Gatewayの作成も完了したらM5Flowに組み込みましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/86aafdbe-27b0-d0b5-9453-ab02fe2db403.png)

## カウント値を定期的に更新する
本当は更新あったときだけM5Stackから確認orデータ更新をM5Stackに通知するのが理想でMQTT等が必要ですが、今回は1秒に1回確認しに行くことで実装します。

API Gatewayの料金が3億リクエスト/1.29USDで、Lambdaが100万リクエスト/0.2USD。1秒に1回、2つのエンドポイントに確認しても18万リクエストなのでしばらくは無料枠で賄えそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/0879a017-e307-2c76-ac51-26eebaf220fd.png)

とりあえず完成！
<img width="300" alt="product" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/8dc06228-34c1-5e1f-1a48-6e31d210136f.jpeg">


# 拡張性

今回はNode-REDとMQTTを使ってみたかったのですが時間切れでした。
またenebulerについてもクラウド実行環境なら便利に使えそうなので次回試してみたいです。

https://blog.enebular.com/enebular/how-to-use-enebular-cloud-exec-environment/

## enebular設定(未完)
最初、enebularを使ってAPI GatewayとM5Stackを連携させようとした時の資料。
(ChatGPT4がクラウド実行環境の存在を教えてくれなかった)

供養として載せておきます。

enebularにサインインし、プロジェクトを新規作成し、フローを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/a427e065-a379-0d98-0fe7-195bb8a3445a.png)

フローの編集画面でhttp requestとhttp responseのノードを作成し接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/2c101057-836e-c0d9-d69f-cc1d50075c5f.png)

http requestのURLに、先ほど作成したエンドポイントを設定します。TLS接続も有効化。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628523/7f529dc7-b0c1-40b0-20e4-d488099a9f52.png)

設定を終えたら保存してデプロイします。