JINS MEMEをSORACOM経由でAWS IoTに繋いでみる


※この記事は、ソラコムのSoftware Design 誌 巻頭特集記念リレーブログ5月6日分です

今回は、JINSさんが提供しているMEMEをソラコム経由でAWS IoTにあげてみたいと思います。

前提

MEMEからAWS IoTへは、AndroidデバイスにSORACOM Airを挿入し、使います。
AndroidにおけるSORACOMの設定についてはこちら

構成は以下の様のとおりです。

JINS MEME

話題のMEMEをお借りすることができた!
JINS MEMEを知らない方はこちらを参照してください。
軽くセンサーの紹介

センサー名
3点式眼電位センサー
3軸加速度センサー
3軸ジャイロ(角速度)センサー

IoTの中でドライブやオフィス・工場内での作業など人が行う活動をセンシングし、それを活かしたいというニーズは多くあります。姿勢、目の動きなどを採ることができ、かなり有益なセンサーデバイスです。
アプリも提供されてますが、自分で開発することができるようにSDKの提供も行っているのがなんともよく考えられてます。
開発するには、ディベロッパーサイトにてアカウントを作り、アプリ登録をする必要があります。

登録すると、アプリIDとアプリSecretが表示されますので、メモしておきます。

SDKのダウンロードとサンプルコードのImport

Android SDKは、こちら からダウンロードできます。解凍する、以下の様な形で展開されます。

お試しということで、サンプルコードに無理やりAWS IoTにデータをPublishするようにしてしまいましょう。
つまり、無理やり突っ込みますのでその辺りは目をつぶってくださいw

Android Stadioを起動し、"File" -> "New" -> "Import Project"でインポートします。

最低限追加したところだけ説明します。

  • appフォルダ内のbuild.gradleにAWS IoT用のSDKとGSONを使ったのでそちらを追加 (バージョンは、2016年5月3日時点の最新です)
dependencies {
   ** 省略 **
    compile 'com.amazonaws:aws-android-sdk-iot:2.2.15+'
    compile 'com.google.code.gson:gson:2.6.2+'
}
  • JINS MEMEアプリケーション認証に必要な情報を追加 先ほどディベロッパーサイトにて取得した値をこちらにセットします。
MainActivity.java
String appClientId = "ここ";
String appClientSecret = "ここ";
  • MEMEと接続し、データを取得しているとこららへんにAWS IoTへのPublish機能を無理やり入れる

AWS IoTへは、WebsocketsやMQTTを使ってデータ(メッセージ)をPublishすることができます。
MQTTを使う場合、TLS1.2を使った相互認証が必須です。Androidデバイス側証明書が必要ということになります。AWS IoTでは、AWS IoTが生成する公開鍵、秘密鍵、証明書をダウンロードして利用するパターン(CreateKeysAndCertificate)かCSRを利用して証明書を作成するパターン(CreateCertificateFromCsr)があります。今回は、前者を使います。というよりも、AWS Mobile SDKに、サンプルコードがあり、そこに、AWS IoTを使ったPubSubのサンプルコードがあるので、そちらを使います。
このサンプルコードでは、Cognitoで認証情報を取得し、CreateKeysAndCertificate APIをコールし、必要な鍵と証明書をダウンロードし、Keystoreに格納してくれます。その鍵を使ってAWS IoTにPublishすることができるようになるものです。
Cognitoの設定含め必要な設定は、サンプルコードのReadmeに細かく記述されてます。こちらを参照してください。
MEMEをBLEで接続できたあとのアクティビティがMemeDataActivityなので、その初期化処理時にKeystoreに鍵がなければ、AWS IoTから鍵を取得する処理をいれます。その後、MQTTにてConnectします。


void init() {
***省略***
        credentialsProvider = new CognitoCachingCredentialsProvider(
                getApplicationContext(),
                COGNITO_POOL_ID,
                MY_REGION
        );

        Region region = Region.getRegion(MY_REGION);

        mqttManager = new AWSIotMqttManager(clientId, region, CUSTOMER_SPECIFIC_ENDPOINT_PREFIX);

        mqttManager.setKeepAlive(10);

        mIotAndroidClient = new AWSIotClient(credentialsProvider);
        mIotAndroidClient.setRegion(region);

        keystorePath = getFilesDir().getPath();
        keystoreName = KEYSTORE_NAME;
        keystorePassword = KEYSTORE_PASSWORD;
        certificateId = CERTIFICATE_ID;

        try {
            if (AWSIotKeystoreHelper.isKeystorePresent(keystorePath, keystoreName)) {
                if (AWSIotKeystoreHelper.keystoreContainsAlias(certificateId, keystorePath,
                        keystoreName, keystorePassword)) {
                    Log.i(LOG_TAG, "Certificate " + certificateId
                            + " found in keystore - using for MQTT.");
                    clientKeyStore = AWSIotKeystoreHelper.getIotKeystore(certificateId,
                            keystorePath, keystoreName, keystorePassword);
                } else {
                    Log.i(LOG_TAG, "Key/cert " + certificateId + " not found in keystore.");
                }
            } else {
                Log.i(LOG_TAG, "Keystore " + keystorePath + "/" + keystoreName + " not found.");
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, "An error occurred retrieving cert/key from keystore.", e);
        }

        if (clientKeyStore == null) {
            Log.i(LOG_TAG, "Cert/key was not found in keystore - creating new key and certificate.");

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        CreateKeysAndCertificateRequest createKeysAndCertificateRequest =
                                new CreateKeysAndCertificateRequest();
                        createKeysAndCertificateRequest.setSetAsActive(true);
                        final CreateKeysAndCertificateResult createKeysAndCertificateResult;
                        createKeysAndCertificateResult =
                                mIotAndroidClient.createKeysAndCertificate(createKeysAndCertificateRequest);
                        Log.i(LOG_TAG,
                                "Cert ID: " +
                                        createKeysAndCertificateResult.getCertificateId() +
                                        " created.");

                        AWSIotKeystoreHelper.saveCertificateAndPrivateKey(certificateId,
                                createKeysAndCertificateResult.getCertificatePem(),
                                createKeysAndCertificateResult.getKeyPair().getPrivateKey(),
                                keystorePath, keystoreName, keystorePassword);

                        clientKeyStore = AWSIotKeystoreHelper.getIotKeystore(certificateId,
                                keystorePath, keystoreName, keystorePassword);

                        AttachPrincipalPolicyRequest policyAttachRequest =
                                new AttachPrincipalPolicyRequest();
                        policyAttachRequest.setPolicyName(AWS_IOT_POLICY_NAME);
                        policyAttachRequest.setPrincipal(createKeysAndCertificateResult
                                .getCertificateArn());
                        mIotAndroidClient.attachPrincipalPolicy(policyAttachRequest);

                    } catch (Exception e) {
                        Log.e(LOG_TAG,
                                "Exception occurred when generating new private key and certificate.",
                                e);
                    }
                }
            }).start();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mqttManager.setAutoReconnect(false);
                    mqttManager.connect(clientKeyStore, new AWSIotMqttClientStatusCallback() {
                        @Override
                        public void onStatusChanged(final AWSIotMqttClientStatus status,
                                                    final Throwable throwable) {
                            Log.d(LOG_TAG, "Status = " + String.valueOf(status));

                        }
                    });
                } catch (final Exception e) {
                    Log.e(LOG_TAG, "Connection error.", e);
                }
            }
        }).start();

MemeRealtimeListenerにてMemeのデータ受信処理をしているので、そこでデータが来たところをで無理やりPublishしちゃいましょう。

final MemeRealtimeListener memeRealtimeListener = new MemeRealtimeListener() {
        @Override
        public void memeRealtimeCallback(final MemeRealtimeData memeRealtimeData) {

        ***省略***
        dataItemAdapter.updateMemeData(memeRealtimeData,mqttManager); // MQTTManagerを引数に追加

画面描画用にデータをArrayListにいれている部分をそのままGSONを使ってJSONにしようとしたのですがあとで扱いにくいフォーマットだったので、Mapにして、MapをGSONにくわせて、Publishしてます。

MemeDataItemAdapter.java
// MQTT Managerを追加
public void updateMemeData(MemeRealtimeData d, AWSIotMqttManager mqttManager) {

***省略***
      final String topic = "適当なトピック";
      String json = new Gson().toJson(map); // ArrayListの処理部分にMapも追加

        try {
            mqttManager.publishString(json, topic, AWSIotMqttQos.QOS0);
        } catch (Exception e) {
            Log.e(LOG_TAG, "Publish error.", e);
        }
    }

という感じです。
実際には、データを減らしてバッファリングして送信した方がいいです。
すみません。手抜きです。トピック名も適当にあとのAWS IoTのルール設定で使います。

Elasticsearchの準備

Amazon Elasticsearch Service(以下、ES)のセッティングは、こちらを参考にしてください。ただし、今回は、AWS IoTから直接ESに接続できるようになったのが違いです。便利ですね。
今回は、各Androidデバイス側のサンプルコードをなるべく活かすため、データ型をマッピングテンプレートを使って調整します。ちなみに、マッピングテンプレート内のtimestampについては、AWS IoTのルールがセットしたものになります。こちらは、後ほどのAWS IoTのルールエンジンにて設定します。


curl -XPUT https://***.us-east-1.es.amazonaws.com/meme -d '
{
  "mappings": {
    "sensor": {
      "properties": {
                "Acc X": {
                    "type": "double"
                },
                "Acc Y": {
                    "type": "double"
                },
                "Acc Z": {
                    "type": "double"
                },
                "Blink Speed": {
                    "type": "double"
                },
                "Blink Streangth": {
                    "type": "double"
                },
                "Eye Move Down": {
                    "type": "double"
                },
                "Eye Move Left": {
                    "type": "double"
                },
                "Eye Move Right": {
                    "type": "double"
                },
                "Eye Move Up": {
                    "type": "double"
                },
                "Fit Status": {
                    "type": "string"
                },
                "Noise Status": {
                    "type": "boolean"
                },
                "Pitch": {
                    "type": "double"
                },
                "Power Left": {
                    "type": "double"
                },
                "Roll": {
                    "type": "double"
                },
                "Walking": {
                    "type": "boolean"
                },
                "Yaw": {
                    "type": "double"
                },
                "timestamp": {
                    "type": "date"
                }
      }
    }
  }
}'

AWS IoTの準備

準備といっても、証明書は、サンプルコードで生成されますし、ポリシもサンプルコード内の手順に記載されているので、ルールの設定のみです。

Attributeにtimestamp() as timestampを入れることで、元々にはなかったタイムスタンプを追加してくれます。とっても便利だと思います。
その他は、ESに必要な設定をいれれば問題無いです。トピックもサンプルコード内でいれたトピック名と同じものをいれてください。

kibanaで見るだけ

ここまでくれば、あとは、kibanaで見るだけです!

興味深いパラメータがあるので、今後、色々試せそうです。上記は、ジャイロとまばたきのスピードなどの平均をプロットしてます。

最後に

今回は、(今回も?)kibanaで可視化するのみになってしまいましたが、データがクラウド上がることで、様々な展開ができます。例えば、寝ていることの検知とか。
冒頭に書きましたとおり、人の行動の可視化・活用はIoTの中でも注目のエリアかと思います。
その中でメガネを通して自然な形で状況がとれるのは活用できそうです。

また、車や現場作業などWifiなどが使えない環境において効果が発揮するときにSORACOMは欠かせません。
単純にモバイル通信という役割だけでなく業務でAndroid端末を使うとき、使用するときだけActivateして業務終了時にDeactivateするなんてことも簡単です。JINS MEMEとの組み合わせだと、メガネの状態によってActivate/Deactivateをコントロールするなんてもこともできるので応用範囲は広いです。

AWS IoTのES対応はクイックに可視化するなら、楽だなと改めて思いました。あと、Mobile SDKを使ってAndroidからMQTT接続、Publishも簡単でした。

免責

こちらは個人の意見で、所属する企業や団体は関係ありません。