自分だけ開ける「スマート宝箱」を作って、大事なモノをセキュアに保管してみた

2021-01-05
日常で楽しむクラウドテクノロジー

飯田 起弘, 原田 裕平

こんにちは、アマゾンウェブサービスジャパンの飯田です。子供やパートナーと一緒に住んでいると、どうしても見られたくない、触られたくないもの、ありませんか ? わたしには有ります。

ただ、それを保存するのに金庫を買うのも若干仰々しさがあり、逆に怪しまれるリスクがあります。子供に触られたくないものなら、手の届かない高いところに置けばいいのでは、と思われる方もいるかもしれません。しかし、2 歳を過ぎた子供ならば椅子に乗ってアプローチする知恵を持ち合わせています。かといって、誰にもバレないと思われる場所に隠したら自分もその場所を忘れてしまったり。。
 
そんな、大事なモノをスマートに隠す箱、名付けて「スマート宝箱」を作ってみました。
色々やりたいことはありましたが、短期間でそんな課題を解決するために、最初にこんな MVP を定義しました。

  • 見た目が普通の箱 鍵穴なし
  • スマホから開けられる
  • スマホから状態がわかる
  • 電池駆動

そしてもう一つ、この開発は Arduino 等をあえて使わず、いつか誰かに商品化される日を夢見て、量産開発時にも利用可能な設計を目指しました。(とはいえやり残したことや課題も多いです。この記事のまとめにメモしております。)

なお、今回、アプリ側は私飯田が担当し、デバイス側を原田が担当し、並行開発しました。所要時間は 2 日間くらいでした。

今回実際に開発したのはこのようなものです。

Android のアプリでログインし、ボタンを押すと、クラウドを経由して宝箱が開きます。鍵穴もなく、閉じたときにはなんの変哲もない引き出しのように見えます。しかしこれは、自分しか開けられないスマートな宝箱です。

クリックすると拡大します

このクラウドレシピ (ハンズオン記事) を無料でお試しいただけます »

毎月提供されるクラウドレシピのアップデート情報とともに、クレジットコードを受け取ることができます。 


1. 設計の概要

1-1. ハードウェア

今回作ったものの、ハードウェア構成 (実体配線図) はこちらです。

クラウドと接続するエッジデバイスとして M5StickC を利用し、FreeRTOS を入れて動作させます。ロック機構として、5V 電源を投入すると解錠される電磁ロックを用意し、リレーを介して ON / OFF を制御します。リレーと M5StickC は Grove コネクタで接続していますが、シリアル通信をしているわけではなく、電源供給と信号制御をしているだけです。

1-2. ソフトウェア

今回作成したシステムのアーキテクチャはこちらになります。

Android アプリは AWS Amplify を利用し、Amazon Cognito による認証と GraphQL の API を実装しています。スマホからのデータ受け渡しには、GraphQL のマネージドサービスである AWS AppSync を利用しており、解錠コマンドの送信と、解錠完了通知の受信を行います。

スマホの解錠ボタンを押下すると、AppSync 経由で、Amazon DynamoDB に LockState = OPENING として保存されます。これが DynamoDB に書き込まれたことをトリガーとして、AWS Lambda を呼び出し、デバイス側の状態を管理する AWS IoT Core の機能であるAWS IoT Device Shadow サービス を利用してデバイスに解錠要求を出します。

デバイスはその変化を MQTT のメッセージとして受信し、マイコンに接続されたリレーを作動させ、解錠します。解錠が完了したら、Device Shadow を更新し、それをトリガーとして別の Lambda 経由で AppSync の API に解錠完了を通知します。モバイルアプリは、状態変更を AppSync の Subscribe によって検知し、UI 上に完了したことが表示されます。

Device Shadow は、デバイスの接続状態に関わらず、アプリなどでデバイスの状態を参照できるようにする機能ですが、こちらにデバイスの制御に必要な情報を集約します。また、DynamoDB では、デバイスとユーザーの紐付けやデバイスの状態など、アプリで使用するデータを、アプリが利用しやすい形で保存します。これによってデバイス側の開発とアプリ側の開発を疎結合にできるのではないかと考えました。

先述の通り、デバイス側、クラウド側ともに、実際の製品開発をそれなりに考慮してみましたので、自分のスマート宝箱を作りたい方だけでなく、IoT 製品の開発を検討されている方もぜひ御覧ください。ここからは作り方の説明です。

ご注意

  • すべての手順を記載できておらず、ポイントのみの抜粋になっておりますが、予めご容赦ください。
  • AWS IoT に関する技術情報に関しては、AWS IoT 開発者ポータル も合わせてご覧ください。
  • 開発には MacOS を利用しています。他の環境の場合には手順が異なる可能性があります。

 


2. ハードウェア側の開発

2-1. 使用するハードウェアの選定

スマート宝箱を作るにあたり、最低限、収納箱、ロック機構、ロック機構を動かすためのマイコン、通信モジュール、電子部品が必要です。

まずは箱とロック機構から検討しましょう。本当にセキュリティの高い収納を作るためには、物理的に強固な箱と、密に連動したロック機構が必要です。しかし今回作るのははインテリアとしてさりげなく設置できるスマート宝箱。箱は、市販されている引き出し収納をベースにして、ロック機構を別途購入して取り付ける方針で進めます。

収納箱は近所の雑貨屋さんからこのようなものを購入しました。
加工性を考えて木製を選んでおります。

クリックすると拡大します

続いてマイコン、ロック機構、電子部品を検討していきます。

今回はクラウドと繋がるデバイスとして、M5StickC を用意しました。こちら、小型ながら内蔵バッテリーや LCD ディスプレイ、LED やボタンなどを備え、USB ケーブル一本で PC から書き込みができる優れものです。ESP32 というマイコンが搭載されており、Wi-Fi と Bluetooth を内蔵しているので、単体でクラウドと通信できます。

ロック機構としては、5V 駆動の電磁ロックを用意しました。電源を入れるとソレノイドが動き、ロックが解除されて金具が飛び出す仕組みになっています。今回はこちらを箱に取り付けて、クラウドから制御します。

ロック機構を動かすために、リレーモジュールも使用します。こちらは Grove モジュールとなっていて、M5StickC にコネクタを刺すだけで使えます。また、配線のために小型のブレッドボードも用意しました。

クリックすると拡大します

2-2. ファームウェアの実装

続いて M5StickC にクラウドからの指令を受けて、ロック機構を解錠する仕組みを実装します。

この部分のアーキテクチャはこちらです。

クリックすると拡大します

デバイスのプログラミングにあたっては、FreeRTOS を使います。FreeRTOS は、組み込みシステム用のリアルタイム OS で、FreeRTOS カーネルによって、複数のタスクのセットとしてアプリケーションを組むことができます。例えば、センサー情報をクラウドで収集したい時には、接続されたセンサーの情報を読み取るタスク、クラウドとメッセージのやり取りをするタスクをそれぞれ独立に書いておけば、スケジューラーが適切にそれぞれのタスクを動かしてくれます。また、AWS IoT との接続やメッセージング、OTA などを簡単に行うためのライブラリも用意されています。

M5StickC で FreeRTOS を使い始めるにあたっては、今回は こちら のワークショップを参考にします。

まずは AWS IoT Core のコンソールから、モノの登録をします。

クリックすると拡大します

続いて証明書を作成します。

クリックすると拡大します

証明書を作成したら、証明書とキーペアを忘れずにダウンロードします。
(AWS IoT のルート CA 証明書は後からでもダウンロードすることができます)

クリックすると拡大します

続いて、作成した証明書に紐づける IoT ポリシーを作成します。

クリックすると拡大します

ポリシードキュメントは以下のように設定します。

尚、今回は簡単にするために対象リソースを * としたポリシーとしていますが、プロダクションを考えた場合、アクセスできるリソースも制限する必要があります。今回のポリシードキュメントでは、どのトピックに対しても Pub/Sub ができてしまうので、複数デバイスがあった場合、他のデバイスの解錠を行えてしまう、などのリスクがあります。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect",
        "iot:Publish",
        "iot:Subscribe",
        "iot:Receive"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

ポリシーを作成したら、先ほど作成した証明書にアタッチします。

これでクラウド側のセットアップは完了です。

クリックすると拡大します

続いてデバイス側の作業を進めます。

ワークショップの手順に従い、lab のリポジトリを Clone します。
先ほどのクラウドセットアップの手順でダウンロードしたクライアント証明書と秘密鍵を aws_clientcredential_keys.h ヘッダファイルに記述します。(こちらのツールを使ってヘッダファイルを作成することもできます)

続いて、aws_clientcredential.h ファイルに AWS IoT へ接続するエンドポイント (AWS IoT コンソールの設定から確認できます)、先ほどの手順で作成したモノの名前、お手元の Wi-Fi の SSID、パスワード、セキュリティモードを設定します。

MQTT 接続するエンドポイントは AWS IoT コンソールの設定から以下のように確認できます。

クリックすると拡大します

aws_clientcredential.h は以下の部分を編集します。

...
#define clientcredentialMQTT_BROKER_ENDPOINT "ここにエンドポイントの URL をコピー"
...
#define clientcredentialIOT_THING_NAME       "ここにモノの名前を入力 (例 : PersonalBox-001))"
...
#define clientcredentialWIFI_SSID            "ここに Wi-Fi の SSID を入力"
...
#define clientcredentialWIFI_PASSWORD        "に Wi-Fi のパスワードを入力"
...
#define clientcredentialWIFI_SECURITY        eWiFiSecurityWPA2
...

ここまで設定が終了すれば、あとはコードをビルドして、作成されたバイナリファイルをデバイスに書き込んで準備完了です(手順は こちら)。

次に、クラウドから解錠の指令をデバイスに送り、デバイスはその指令に応じて解錠を行うロジックを実装します。

AWS IoT Core では、MQTT による双方向メッセージングを行うことができます。こちらを利用して解錠を行う場合は、1. クラウドからデバイスに向けて解錠コマンドを発行し、2. デバイス側はコマンドを受け取ったらロックの解錠を行い、3. クラウドに解錠が完了したメッセージを送る、といった実装が考えられます。しかし、AWS IoT Core にはこのような動作を簡単に実現する仕組みがあります、それが Device Shadow です。Device Shadow は、json ドキュメントとして状態を定義し、クラウドとデバイス上で状態を同期する仕組みです。

今回は lockState という状態を定義し、0 を施錠状態 (リレーが OFF である状態)、1 を解錠状態 (リレーが ON である状態) とします。(厳密には、今回のロック機構は物理的に金具を押し込むことで初めて施錠状態へと戻るので、電流を流していない状態とロックがかかっている状態は一致するとは限りません。しかし、そのような状態管理をするには、物理的に施錠状態を検知するためのセンサーが必要になります。こちらについては後述します。)

この Device Shadow に対して、今回は次のように解錠動作を実装します。

まず、施錠状態の Device Shadow は以下とします。

{
  "desired": {
    "lockState": 0
  },
  "reported": {
    "lockState": 0
  }
}

クラウド側のアプリケーションから、解錠指令を送るため、desired (目標状態) を 1 へ更新します。

すると、reported (現在の状態) との差分が検知され、"delta": {"lockState": 1} となります。こちらがデバイスに通知されます。

{
  "desired": {
    "lockState": 1
  },
  "reported": {
    "lockState": 0
  },
  "delta": {
    "lockState": 1
  }
}

デバイスはこれを受けて、リレーが繋がったピンを一定時間 High に変更し、 "reported": {"lockState": 1} と報告します。クラウド側はこちらを受けて状態が変更され、アプリケーション側は無事に解錠されたことを知ることができます。

{
  "desired": {
    "lockState": 1
  },
  "reported": {
    "lockState": 1
  },
}

今のデバイス側にはこの後再び施錠状態に戻ったことを知る術がありません。しかしこのままでは解錠したままということになってしまい、再び解錠することができません。本来であればセンサーによって引き出しが閉じられたことを検知して lockState を 0 (施錠状態) へと戻すべきですが、今回は解錠した後に時間経過で lockState を 0 に戻してやります。このとき、reported の変更だけではなく、desired も 0 へと戻してやる必要があります。あるいは "desired": {"lockState": null} として目標状態を消してしまってもよいですね。

{
  "desired": {
    "lockState": 0
  },
  "reported": {
    "lockState": 0
  },
}

これで、最初の状態へ戻ってきました。

このような動作を次のようなタスクとして実装します。

static void prvUnLockTask(void * pvParameters)
{
    // Used for the screen.
    char pUnLockStr[16] = {0};
    char * pThingName = (char *)pvParameters;

    ESP_LOGI(TAG, "prvUnLockTask: Starting the UnLock task for: %s", pThingName);

    for(;;)
    {        
        if(ulTaskNotifyTake(pdTRUE, portMAX_DELAY) >0){
            int status = EXIT_SUCCESS;

            if (shadowStateReported.lockState == 1){
                UNLOCK();
                vTaskDelay(pdMS_TO_TICKS(200));
                LOCK();
                status = snprintf(pUnLockStr, sizeof(pUnLockStr), "Lock: Open");
                shadowStateReported.lockState = 0;
                shadowStateDesired.lockState = 0;
            }else{
                LOCK();
                status = snprintf(pUnLockStr, sizeof(pUnLockStr), "Lock: Close");
            }

            if (status >= 0)
            {
                TFT_print(pUnLockStr, CENTER, 49);
            }
            
            /* Report Shadow. */
            status = _reportShadow(pThingName);
            if (status != EXIT_SUCCESS)
            {
                ESP_LOGE(TAG, "prvUnLockTask: Failed to report the shadow.");
            }

            /* Update Desired. */
            status = _desiredShadow(pThingName);
            if (status != EXIT_SUCCESS)
            {
                ESP_LOGE(TAG, "prvUnLockTask: Failed to report the shadow.");
            }

            vTaskDelay(pdMS_TO_TICKS(100));
        }
    }
}

こちらのコードは、ワークショップの lab2 コード をベースにタスク書き換える形式で実装しています。

LOCK()UNLOCK() 関数は別の箇所で define しており、内部では gpio_set_level() によって、リレーに接続されたピンの High/Low を切り替えています。こちらは M5StickC に組み込まれている ESP32 の場合の GPIO を制御する関数です。

_reportShadow()_desiredShadow() は Shadow のドキュメントを更新する関数で、あらかじめ文字列としてドキュメントを定義しておき、グローバル変数 shadowStateReported shadowStateDesired を参照して Shadow を更新します。_reportShadow() 関数はワークショップのコードをそのまま用い、_desiredShadow() は新しく定義します。アップデートするドキュメントを変更するだけで中身は殆ど同じです。

また、FreeRTOS の API を用いて、タスク通知がきた際に、このロジックが走る仕組みを実装しています。Shadow に差分が発生すると、 _shadowDeltaCallback() が呼び出され、その中で、xTaskNotifyGive が呼ばれます。これを受けて、ulTaskNotifyTake() 以降のコードが実行されます。

このような実装により、定期的に状態監視のロジックを走らせる必要がなくなり、shadow が更新され、コールバックが呼ばれた際にのみ動作するイベント駆動となります。このようにタスクを利用することで、センサーデータを定期的に取得する機能と Device Shadow を更新する機能をシンプルに連携することができます。

では、ここまでの挙動を、実機で確認しましょう。

クリックすると拡大します

実体配線図は次のようになります。

配線が終わったら、AWS IoT のコンソールから Device Shadow を変更してみます。

無事、金具が勢いよく飛び出しました。
Device Shadow も解錠状態へと更新されたのち、施錠に戻ります。

あとは、スマート宝箱として仕上げるだけですね。

2-3. 加工と組み立て

まず、木箱の引き出しに、ロック機構を取り付けるための加工を行います。

引き出しの手前側か、奥側に金具を固定してしまえば引き出しを固定することができます。今回は見た目をスマートにするために、奥側に取り付けてみます。

ノコギリで、引き出し奥側をくり抜きます。MDF 製なので加工は容易でした。

クリックすると拡大します

続いて、電源供給のための、DC ジャック用の穴を用意します。

木工用ドリルを使います。

 

クリックすると拡大します

これで、電池が空っぽになって解錠できなくなる心配はなくなりました。

クリックすると拡大します

この引き出しには、動かせる仕切りが付いているので、このスペースにモバイルバッテリーごと埋め込みます。

クリックすると拡大します

最後に、蓋をするためのアクリル板をパキっと切断して、

クリックすると拡大します

仕切りのスペーズにはめます、ぴったり !

クリックすると拡大します

最後にステッカーを貼って、引き出しを箱に入れれば完成です。
これで、デバイス側の準備も整いました。

クリックすると拡大します


3. モバイルアプリの開発

続いてモバイルアプリの開発です。今回は以下のようなアーキテクチャとしています。Android アプリを開発しましたが、iOS でも同様の方法で実現できると思います。

クリックすると拡大します

3-1. Android / Amplify 開発環境をセットアップ

まずは、開発環境の構築です。homebrew や npm などはすでにインストール済みの場合は不要です。利用した Amplify のバージョンは、4.32.1 です。

# install homebrew, npm, amplify-cli
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install node
 npm install -g @aws-amplify/cli

こちらのチュートリアルの、Prerequisites、Create your application の項目に沿って作成します。
名前は、Personal Box としました。

クリックすると拡大します

チュートリアル上で、以下を追記するように出ていますが、これは不要でしたので削除しました。

apply *plugin*: *'com.amplifyframework.amplifytools'

3-2. Authentication

続いて認証部分です。ここを見ながら実装を進めます。

実装が出来たら認証部分だけ試します。

クリックすると拡大します

これで認証ができました。
LogCat をみると、たしかに認証出来ています。

クリックすると拡大します

次にこのトークンを使って、解錠コマンドを送ります。ここでは、AppSync を使って、DynamoDB に値を書き込みます。

3-3. AppSync(GraphQL)

引き続き Amplify のチュートリアルを見ながら実装を進めます。

*amplify init*
*amplify add api*
? Please select from one of the below mentioned services: GraphQL
? Provide API name: personalbox
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Do you want to configure advanced settings for the GraphQL API Yes, I want to make some additional changes.
? Configure additional auth types? Yes
? Choose the additional authorization types you want to configure for the API IAM
? Configure conflict detection? No
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? No

後々 Lambda から GraphQL の API を呼び出す際に、IAM による認証を使うため、additional auth types で IAM を追加しています。

プロジェクトルートディレクトリに、amplify フォルダが作られます。
schema.graphql を以下の様に変更します。

graphQL

type PrototypePersonalBox
@model
@auth(rules: [{ allow: private }, { allow: private, provider: iam }])
{
  boxid: String!
  state: String!
  id: ID!
}

ここで、boxid には AWS IoT 側のモノの名前、state として宝箱の状態、id に Cognito の username を入れようと思います。

* amplify push*
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name       | Operation | Provider plugin   |
| -------- | ------------------- | --------- | ----------------- |
| Auth     | personalbox155c4a4a | Create    | awscloudformation |
| Api      | personalbox         | Create    | awscloudformation |
? Are you sure you want to continue? Yes

GraphQL schema compiled successfully.

Edit your schema at /Users/tatsiida/AndroidStudioProjects/PersonalBox/amplify/backend/api/personalbox/schema.graphql or place .graphql files in a directory at /Users/tatsiida/AndroidStudioProjects/PersonalBox/amplify/backend/api/personalbox/schema
? Do you want to generate code for your newly created GraphQL API Yes
? Enter the file name pattern of graphql queries, mutations and subscriptions app/src/main/graphql/**/*.graphql
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
⠸ Updating resources in the cloud. This may take a few minutes...

UPDATE_IN_PROGRESS amplify-personalbox-dev-152441 AWS::CloudFormation::Stack Thu Nov 12 2020 15:45:50 GMT+0900 (Japan Standard Time) User Initiated
⠴ Updating resources in the cloud. This may take a few minutes...

これでバックエンドの設定が完了です。

3-4. Android (Kotlin) の実装

非常にシンプルな UI を書きます。※エラーケース等は考慮していません。

MainActivity.kt

kotlin

package com.example.personalbox

// import は省略

class MainActivity : AppCompatActivity() {
    private var subscription: ApiOperation<*>? = null
    private val TAG = "App"
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        val thingname = "PersonalBox-001"

        Amplify.Auth.signInWithWebUI(
            this,
            { result ->
                {
                    Log.i(TAG, result.toString())
                    binding.username.text = Amplify.Auth.currentUser.username
                }
            },
            { error -> Log.e(TAG, error.toString()) }
        )

        binding.button.setOnClickListener {

            binding.username.text = Amplify.Auth.currentUser.username

            val command = PrototypePersonalBox.builder()
                .boxid(thingname)
                .state("OPENING")
                .id(Amplify.Auth.currentUser.username)
                .build()

            Amplify.API.mutate(
                ModelMutation.update(command),
                {
                    runOnUiThread {
                        binding.status.text = "OPENING"
                    }
                },
                { Log.e(TAG, "Create failed", it) }
            )

            subscription = Amplify.API.subscribe(
                ModelSubscription.onUpdate(PrototypePersonalBox::class.java),
                { Log.i(TAG, "Subscription established") },
                {
                    Log.i(TAG, "create subscription received: " + it)
                    runOnUiThread {
                        binding.status.text = it.data.state
                    }
                },
                { Log.e(TAG, "Subscription failed", it) },
                { Log.i(TAG, "Subscription completed") }
            )
        }
    }

    override fun onPause() {
        subscription?.cancel()
        super.onPause()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == AWSCognitoAuthPlugin.WEB_UI_SIGN_IN_ACTIVITY_CODE) {
            Amplify.Auth.handleWebUISignInResponse(data)
        }
    }
}
PersonalBoxApplication.kt

kotlin

package com.example.personalbox

// import は省略

class PersonalBoxApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        try {
            Amplify.addPlugin(AWSCognitoAuthPlugin())
            Amplify.addPlugin(AWSApiPlugin())
            Amplify.configure(applicationContext)
        } catch (error: AmplifyException) {
            Log.e("MyAmplifyApp", "Could not initialize Amplify", error)
        }
    }
}
activity_main.xml

シンプルです。特に変わったことはしていません。

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:id="@+id/username"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Open" />

    <TextView
        android:id="@+id/status"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center" />

</LinearLayout>

ここまでで、クライアントアプリの実装は完了です。

クリックすると拡大します


4. システムの統合

いよいよ、上記のモバイルアプリ開発部分と、デバイス開発部分を Lambda を 2 つ使って結合します。

4-1. DynamoDB の更新をトリガーに Device Shadow を更新

1つめは、DynamoDB の変更をトリガーとして、Device Shadow を更新させる部分です。personal-box-update-thingshadow というファンクション名で、Python3.8 で作成しました。Lambda にアタッチする IAM Role には、DynamoDB へのアクセスと AWS IoT へのアクセスを追加しています。(一旦フルアクセスを与えています。)

クリックすると拡大します

import json
import boto3
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

client = boto3.client('iot-data')

def lambda_handler(event, context):

    state = event['Records'][0]['dynamodb']['NewImage']['state']['S']
    thingName = event['Records'][0]['dynamodb']['NewImage']['boxid']['S']

    if(state != "OPENING"):
        logger.info("ignoring: " + state)
        return

    LOCK_STATE_OPEN = 1

    payload = json.dumps({'state': { 'desired': { 'lockState': LOCK_STATE_OPEN } }})

    logger.info("thingName:"+thingName+", payload:"+payload)

    response = client.update_thing_shadow(
        thingName = thingName, 
        payload =  payload
        )

DynamoDB 上では、state は UI 表示を意識して、OPENING (解錠リクエスト済みだが未解錠) / OPEN / CLOSE の 3 つとしています。

一方で、Device Shadow では、OPEN / CLOSE のみです。OPENING の状態は、Reported:CLOSE / Desired:OPEN によって表現されます。その部分を変換した上で、Device Shadow の更新をしています。

実装ができたら Lambda のトリガーに DynamoDB を追加します。

クリックすると拡大します

これで変更通知を受け付けるようになりました。

ここまでで、スマホアプリ上での解錠操作から、宝箱を開けることができるようになりました。

さらに今度は、宝箱が解錠コマンドを受理したことをモバイルアプリに通知する部分 (いわゆる ACK) を追加していきます。

4-2. Device Shadow の変更を検知し、AppSync API 経由でスマホに通知

もう一つの Lambda では、デバイス側の解錠が完了した (reported が OPEN になった) 場合に AWS IoT のルールエンジンを使って Lambda を起動し、AppSync の API に対して Mutation を行います。

こちらの実装は、Node.js を利用しており、Cloud9 IDE を使って開発しました。Cloud9 の Build-in 機能を使って Lambda のテストやデプロイを簡単に行えます。(ここでは割愛します。詳しくは こちら をご覧ください。)

Device Shadow で、ユーザーに関する情報を保持しないため、DynamoDB を参照し、boxid (モノの名前) から userid を逆引きしています。その userid をキーとして、AppSync 経由で解錠状態を更新します。(直接 DynamoDB を更新してしまうと、モバイルアプリ側の Subscripton に通知が到達しません。)

const aws = require('aws-sdk');
const appsync = require('aws-appsync');
const gql = require('graphql-tag');
require('cross-fetch/polyfill');

const graphqlClient = new appsync.AWSAppSyncClient({
  url: 'https://xxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql',
  region: 'ap-northeast-1',
  auth: {
    type: 'AWS_IAM',
    credentials: aws.config.credentials
  },
  disableOffline: true
});

const docClient = new aws.DynamoDB.DocumentClient();

exports.handler = async (event) => {

    const params = {
      TableName: 'PrototypePersonalBox-xxxxxx-dev',
      IndexName: 'byBoxId',
      KeyConditionExpression: 'boxid = :boxid',
      ExpressionAttributeValues: {
        ':boxid': event.thingName,
      }
    };

    let ret = await docClient.query(params).promise();
    const userid = ret.Items[0].id;
    
    const mutation = gql`mutation MyMutation($prototypePersonalBox: UpdatePrototypePersonalBoxInput!) {
     updatePrototypePersonalBox(input:$prototypePersonalBox) {
        state
        boxid
        id
      }
    }`;
    
    ret = await graphqlClient.mutate({
      mutation: mutation,
      variables: {
        prototypePersonalBox : {
        boxid : event.thingName,
        id : userid,
        state : "OPEN"
        }
      }
    });
    console.log(ret);
};

関数ができたら、IoT Core のルールエンジンを使用して、Device Shadow の変更を検知してこの Lambda を呼び出すように設定します。

クリックすると拡大します

ルールクエリステートメントは、以下のように設定しています。

SELECT topic(3) AS thingName FROM 
'$aws/things/+/shadow/update/documents' 
WHERE current.state.reported.lockState = 1 
AND current.state.desired.lockState = 1

このルールクエリステートメントでは、トピック名の中からモノの名前 (PersonalBox-001) を抜き出し、thingName として Action に伝える Json に付加しています。WHERE 句を用いて、解錠リクエスト中かつ解錠状態の場合のみにこのアクションが実行されるようにしています。呼び出されるアクションには、先程作成した Lambda を指定します。

実際に Device Shadow に届く Json は以下のようなものです。

{
    "thingName": "PersonalBox-001"
}

※ Device Shadow のメッセージが届かない場合は、AWS IoT の設定画面において、「イベントベースのメッセージ」を受け付ける設定をオンにして頂く必要があるかもしれません。


5. まとめ

以上で、Android アプリの操作をスマート宝箱が受け付けて解錠し、解錠が完了したらスマホ完了通知が届くという一連の流れを実現することが出来ました。実際の動作は以下の動画のとおりです。これで自分だけの大事な小物をしっかり守ることができそうです。

やり残したこと (製品開発に向けて)

さて、個人で使う分にはこれで十分かもしれませんが、もしこれをベースに製品開発を考える場合には、以下のようなことを考える必要がありそうです。(他にも色々ありそうですが、現時点で特に気になっていることを書いておきます。)

  • 引き出しの開閉状態の検知
    • 現在の構成では、引き出しの開閉状態を取得することができません。そのため、解錠動作をさせたはずなのに引き出しが閉まったままになってしまう状態のエラーハンドリングや、開きっぱなし状態に対する警告などを行うことができません。こちらは、マイクロスイッチ、リードスイッチ、フォトリフレクタや距離センサなどを収納箱に取り付け、マイコンで読み取ることで実現できそうですね。

  • オフラインなどでデバイスにアクセス出来ない場合の振る舞い
    • スマホ側の操作で解錠リクエストをしたときにデバイス側がオフラインの場合、Device Shadow の Desired を更新しても、Reported の値が更新されない状態になります。その後しばらくしてオンラインになったタイミングでデバイスが Desired の値を確認してそのまま解錠してしまうと、意図しないタイミングで宝箱が開いてしまうかもしれません。それを避けるには、アプリ側でタイムアウト値 (例えば 5 秒など) をもうけて、その間に Reported の値が変わらないようであれば Desired の値を null にして、解錠失敗画面を表示する、などの対処が必要になるかもしれません。デバイス側も、Device Shadow の Timestamp を確認し、一定期間経過した Desired の値は無視するなどの対策を入れておくと良さそうです。

  • デバイス登録部分が未実装
    • 今回ユーザーと宝箱の紐付けは決め打ちでハードコーディングされています。デバイスから何らかの形 (BLE, QR コードなど) でデバイス固有の ID を取得して、初期登録する必要があります。また、デバイスには、IoT Core との接続に使用する証明書や秘密鍵を入れる必要があります。詳細は、AWS IoT Deep Dive #1 の記事を御覧ください。また、大量なデバイスの管理やプロビジョニング扱う、AWS IoT Device Management ハンズオン  も参考になるかもしれません。

  • デバイスとユーザーの関連が 1 対 1
    • ユースケースによっては、1 対多 (1 ユーザーが複数デバイス保持)、多対多 (複数ユーザーがデバイスをシェア) になるとおもいます。それに応じて DynamoDB の設計などを検討する必要がありそうです。

  • セキュリティ対策
    • AWS IoT のポリシーや IAM Role などは適切に設定を行い、必要十分な権限を付与するようにします。
    • デバイスへの不正アクセスなどを早期に検知し対策を打つためには、AWS IoT Device Defender  が利用可能かもしれません。実際に試す際にはハンズオン もあります。

  • 電池駆動にしたい場合
    • 今回 Wi-Fi の常時接続のため、消費電力がそれなりにかかります。電池で長期駆動したい場合などは、スマホとボックスの通信を BLE にすると良いかもしれません。その場合には、FreeRTOS の BLE ライブラリ を使うことで開発をショートカットできる可能性があります。

これらの課題については、また機会があればまとめていきたいと思います。
また、文中にも記載しましたが、AWS IoT 開発者ポータル では、IoT に関する様々な事例やハンズオンなどの技術情報を紹介しています。ぜひ合わせてご覧ください !


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

飯田起弘
アマゾン ウェブ サービス ジャパン合同会社
ソリューションアーキテクト

電機メーカーでソフトウェアエンジニアとして IoT 関連の新規事業の立ち上げを経験の後、AWS にてプロトタイピングソリューションアーキテクトとして、IoT 関連案件の PoC、本番導入などの支援に携わる。

原田裕平
アマゾン ウェブ サービス ジャパン合同会社
ソリューションアーキテクト

2020 年にアマゾン ウェブ サービス ジャパン株式会社に入社。
身近な生活や仕事の課題を、一人でも多くの方が自分の手で技術を駆使して切り開いていける世の中を目指して、お客様の技術支援をしております。
趣味は DIY、アウトドアスポーツ、格闘技です。

AWS のベストプラクティスを毎月無料でお試しいただけます

さらに最新記事・デベロッパー向けイベントを検索

下記の項目で絞り込む
1

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する