M5StickC with ENV HatでAWS IoTにPublishしCloudWatch Logs Insightsで可視化

はじめに

前回の続きとなります。
https://www.skyarch.net/blog/?p=18370

よくある話で、温度、湿度、気圧をAWS IoTに投げ可視化するのですが
最近高頻度でUpdateされ、便利になって大好きなCloudWatch Logs Insightsを使ってみます。

出来たもの

電源を入れると、Wifi接続 ⇢ AWS IoTへ接続 ⇢ センサ値をPublishと進み、AWS IoTのテスト画面にて受信できた事を確認できるという動画です。

  • 温度・湿度・気圧を画面内に表示
  • 温度・湿度・気圧をAWS IoTにPublish
  • Aボタンを押下すると (離した際に)LED状態が変化します。 (将来用に前回のソースコードそのまま)

可視化

CloudWatch Logs Insightsを利用し簡易的に可視化しています。
温度、湿度の1分毎平均値をプロット

目次

手順の概要

  1. AWS IoTで モノ/証明書/ポリシーを作成し適切な関連にAttach (AWS CLIにて実施)
  2. コード書く/転送
  3. AWS IoTで データ受信確認
  4. AWS IoTのルールでCloudWatch Logsへ書き出し
  5. CloudWatch Logs Insightsでクエリを書き可視化

M5StickCで利用するライブラリ等

他にMQTT通信できるライブラリは無いものかと
Aruduino IDEのライブラリマネージャーから探してみましたが
Betaだったり、あまり人気が無かったりという事で結局人気が一番ある下記ライブラリを利用しました。

制限事項として、PublishはQoS 0のみ、Subscribe QoS 0 or 1、メッセージサイズが標準ではとても小さい(128Byte)等色々あります。

PubSubClient
https://pubsubclient.knolleary.net/

Githubページ – 制限事項が色々と記載されています
https://github.com/knolleary/pubsubclient

参考にさせて頂いた記事

ESP32でAWS IoTに繋いでThing Shadowを弄る
https://blog.maripo.org/2017/07/esp32-aws-iot/

ESP32〜AWS IoTでMQTT通信して詰んだ話【ClientID】
https://qiita.com/rockguitar67/items/4f028500d520dbf14be1

【macOS】デバイスとAWS IoTとの接続設定を行うスクリプトをかいたった:証明書の設定からThing登録まで行う
https://dev.classmethod.jp/articles/aws-iot-connect-device-script/

実行環境

MacOS上で実施しました。

手順

1. AWS IoTで モノ/証明書/ポリシーを作成し適切な関連にAttach (AWS CLIにて実施)

注記

  • 下記コマンドを流して行くと、カレントディレクトリにAWS IoTにて作成された証明書/鍵ファイルが保存されます。
  • ポリシーは全許可のテスト用のもののため、ご利用は計画的に

モノを作成

DEVICE_NAME="M5Stick01"
aws iot create-thing --thing-name ${DEVICE_NAME}

ポリシーを作成 (テスト用全許可ポリシーのため要注意)

aws iot create-policy --policy-name ${DEVICE_NAME}-policy --policy-document \
'{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": "iot:*", "Resource": "*" }]}'

証明書と鍵を作成

aws iot create-keys-and-certificate --set-as-active \
--certificate-pem-outfile ${DEVICE_NAME}-certificate.pem.crt \
--private-key-outfile ${DEVICE_NAME}-private.pem.key \
--public-key-outfile ${DEVICE_NAME}-public.pem.key \
--query "certificateArn"

こちらの結果(ARN表記で出力される証明書)を下記コマンド(Policyと証明書を紐付け/モノと証明書を紐付け)にて利用する

Policyと証明書を紐付け

aws iot attach-principal-policy --policy-name ${DEVICE_NAME}-policy --principal "上記コマンド実行結果をペーストarn:aws:iot:..."

モノと証明書を紐付け

aws iot attach-thing-principal  --thing-name ${DEVICE_NAME} --principal "上記コマンド実行結果をペーストarn:aws:iot:..."

AWS IoT エンドポイントを取得

aws iot describe-endpoint --query "endpointAddress" --endpoint-type iot:Data-ATS

2. コード書く/転送

下記の [] 括弧部分をご自身の物に書き換える必要があります。

rootCA

AWS IoTの開発者ドキュメント内に記載のリンクから取得します。
https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/server-authentication.html#server-authentication-certs

clientCert/clientPrivateKey

上記CLIコマンドを実行した結果作成される3ファイルのうち2ファイルを利用します。

  • [DEVICE_NAME]-certificate.pem.crt
  • [DEVICE_NAME]-private.pem.key

コード

500msec毎にデータをAWSIoTへ送信しております

/*
    note: need add library Adafruit_BMP280 from library manage
    Github: https://github.com/adafruit/Adafruit_BMP280_Library
*/

#include <M5StickC.h>
#include "DHT12.h"
#include <Wire.h>
#include "Adafruit_Sensor.h"
#include <Adafruit_BMP280.h>
#include <ArduinoJson.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>

// Wifi Settings
const char* ssid     = "[ご自身の環境のWifiSSID]";
const char* password = "[ご自身の環境のWifiPassowrd]";

// AWS IoT Settings
const char* endpoint = "[ご自身のAWSアカウントのIoTエンドポイント]"; // 東京リージョンなら右記のような形 xxxx-ats.iot.ap-northeast-1.amazonaws.com
const int   port     = 8883;
const char* pubTopic = "[適当なTopic名 - 記事中では envTopic としていました]";
const char* clientId = "[適当なデバイス名 - 記事中では M5Stick01 としていました]"; // 

const char* rootCA = \
"-----BEGIN CERTIFICATE-----\n" \
"...\n" \ // [ご自身で取得したRootCA]
"-----END CERTIFICATE-----\n";

const char* clientCert = \
"-----BEGIN CERTIFICATE-----\n" \
"...\n" \ // [上記CLIコマンドで生成したデバイス用証明書]
"-----END CERTIFICATE-----\n";

const char* clientPrivateKey = \
"-----BEGIN RSA PRIVATE KEY-----\n" \
"...\n" \ // [上記CLIコマンドで生成したデバイス用秘密鍵]
"-----END RSA PRIVATE KEY-----\n";

// Wifi/Network
WiFiClientSecure httpsClient;
PubSubClient mqttClient(httpsClient);
char buffer[128];

// LED_PIN
#define M5_STICK_PIN_LED 10
boolean ledState = true; // HIGH: LED Off/Low: LED On

DHT12 dht12; 
Adafruit_BMP280 bme;

void checkWiring() {
  while (!bme.begin(0x76)) {
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0, 2);
    M5.Lcd.println("Could not find a valid BMP280 sensor, check wiring!");
    delay(1000);
  }
}

void reconnect() {
  while (!mqttClient.connected()) {
    if (mqttClient.connect(clientId)) {
      M5.Lcd.setCursor(0, 40, 2);
      M5.Lcd.println("AWSIoTConnected");
    } else {
      M5.Lcd.setCursor(0, 40, 2);
      M5.Lcd.printf("AWSIoTConnect Failed. state=%d", mqttClient.state());
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void setup() {  
  // put your setup code here, to run once:
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  // I2C
  Wire.begin(0,26);
  // GPIOSetup
  pinMode(M5_STICK_PIN_LED, OUTPUT);
  digitalWrite(M5_STICK_PIN_LED, ledState);
  // check ENVHat
  checkWiring();
  // WifiSetup
  //// Avoid Connect Error (JIC)
  WiFi.disconnect(true);
  delay(1000);
  //// Connect
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  M5.Lcd.setCursor(0, 40, 2);
  M5.Lcd.printf("Wifi Connected");
  // Configure MQTT Client
  httpsClient.setCACert(rootCA);
  httpsClient.setCertificate(clientCert);
  httpsClient.setPrivateKey(clientPrivateKey);
  mqttClient.setServer(endpoint, port);
}

void loop() {
  // update button state
  M5.update();

  // Button Pressed
  if (M5.BtnA.wasReleased()) {
    ledState = !ledState;
    digitalWrite(M5_STICK_PIN_LED, ledState);
  }

  // check ENVHat
  checkWiring();
  
  // display Temp/Humidity/Pressure
  float tmp = dht12.readTemperature();
  float hum = dht12.readHumidity();
  M5.Lcd.setCursor(0, 0, 2);
  M5.Lcd.printf("Temp: %2.1f Humi: %2.0f%%", tmp, hum);
  
  float pressure = bme.readPressure();
  M5.Lcd.setCursor(0, 20, 2);
  M5.Lcd.printf("pressure: %2.1f", pressure);

  // create JSON
  DynamicJsonDocument doc(1024);
  JsonObject data = doc.createNestedObject("env");
  data["temperture"] = tmp;
  data["humidity"] = hum;
  data["pressure"] = pressure;
  serializeJson(doc, buffer, sizeof(buffer));
  // Debug JSON in SerialConsole
  Serial.println(buffer);

  // reconnect
  if (!mqttClient.connected()) {
    reconnect();
  }
  
  // The client only supports publishing at QoS 0
  mqttClient.publish(pubTopic, buffer);

  // wait
  delay(500);
}

3. AWS IoTで データ受信確認

AWS IoTサービスの左メニュー、テストを選択後
トピックのサブスクリプションに、ソースコード中の pubTopic と合わせたTopic名を入力し [トピックへのサブスクライブ] を押下します。
デバイスが正常に通信できれば、500msec 毎に送信されたデータが流れるように表示されます。

4. AWS IoTのルールでCloudWatch Logsへ書き出し

IoTルールでCloudWatch Logsへの吐き出しを作成します。

Topic名が適切に指定されていればロギングされる事を確認できるはずです。

5. CloudWatch Logs Insightsでクエリを書き可視化

CloudWatch Insights を開きクエリとして下記を入力します。

fields @timestamp, env.humidity, env.temperture
| stats avg(`env.temperture`), avg(`env.humidity`) by bin(1m)

上記クエリを実行すると、下記結果を得ることができます。
可視化した結果をダッシュボードに追加できるため色々と表現出来そうです。

ログ集約された結果

可視化した結果

はまった点

一通りサービス理解をしていたつもりでしたが、当初デバイスからプログラムを実行した結果下記エラーが出てハマりました…
Wifiには接続されるが、mqttClient.state() = -2 を返す状態

[E][ssl_client.cpp:33] _handle_error(): [start_ssl_client():199]: (-9984) X509 - Certificate verification failed, e.g. CRL, CA or signature check failed
[E][WiFiClientSecure.cpp:132] connect(): start_ssl_client: -9984

証明書エラーだなと、色々見直したが問題なかったのでモノ・証明書の作り直し、証明書をシリアルコンソールに表示し、一語一句違わないか等を見ましたが問題無し
ふとAWS IoTの証明書ドキュメントを読むと、”エンドポイントの種類” にて、新しい証明書の場合エンドポイントが異なるよという事でした (トホホ…)

上記CLIコマンドで、describe-endpoint する際に –endpoint-type iot:Data-ATS というオプション付きでないと新しいエンドポイントが返却されないようです。
(AWSIoTの設定画面でエンドポイントを確認すると、-ats 付きですね)

AWS IoT Core は、iot:Data と iot:Data-ATS の 2 つの異なるデータエンドポイントタイプをサポートしています。iot:Data エンドポイントは、VerVeriSign Class 3 Public Primary G5 Root CA 証明書によって署名された証明書を提供します。iot:Data-ATS エンドポイントは、Amazon Trust Services CA によって署名されたサーバー証明書を提供します。

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/server-authentication.html#server-authentication-certs

Debug実施方法

Debugレベルを上げる

ツール ⇢ Core Debug Level を上げる

Debugログを確認する

ツール ⇢ シリアルモニタ で確認する (右下の通信速度を合わせないと文字化けするので注意)

まとめ

次回はPub/Subに対応させ、Shadowと戯れてみようと思います。

実施してみました
https://www.skyarch.net/blog/?p=18513

投稿者プロフィール

takashi
開発会社での ASP型WEBサービス企画 / 開発 / サーバ運用 を経て
2010年よりスカイアーチネットワークスに在籍しております

機械化/効率化/システム構築を軸に人に喜んで頂ける物作りが大好きです。
個人ブログではRaspberryPiを利用したシステムやロボット作成も
実施しております。

スカイアーチネットワークスで一緒に働きましょう!

ABOUTこの記事をかいた人

開発会社での ASP型WEBサービス企画 / 開発 / サーバ運用 を経て 2010年よりスカイアーチネットワークスに在籍しております 機械化/効率化/システム構築を軸に人に喜んで頂ける物作りが大好きです。 個人ブログではRaspberryPiを利用したシステムやロボット作成も 実施しております。 スカイアーチネットワークスで一緒に働きましょう!