M5Stack を AWS IoT Core に登録し超音波距離センサーの値を MQTT で送信し SNS に流してメールで受信する

arduinoiotaws

ESP32 ベースの M5Stack Basic v2.7 を買った。

チュートリアルに従い、 Arduino IDE をインストール、 Board Manager の設定 を行って M5Stack Boards をインストール、Tools から Board=M5Core と Port を選択し M5Unified ライブラリをインストールすると準備完了。M5GFX の Example の LongTextScroll のコードを Upload するとコンパイルが始まり実機に文字が流れ始めた。

超音波距離センサー HC-SR04 を繋げて値を表示してみる。 このセンサーには VCC(電源) と GND のほかに 2 つの端子があって、 Trig に 10μs のパルスを送ると超音波が出て物体に当たって返ってくるまでの間 Echo が HIGH になるのでそこから距離を計算できる。 GPIO35,36 は入力専用なので TRIG は繋げられない。

#include <M5Unified.h>

const int TRIG_PIN = 26;
const int ECHO_PIN = 36;

void setup() {
  M5.begin();

  M5.Display.setTextSize(2);

  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
}

void loop() {
  M5.update();

  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10); // You only need to supply a short 10uS pulse to the trigger input to start the ranging
  digitalWrite(TRIG_PIN, LOW);
  
  long duration = pulseIn(ECHO_PIN, HIGH);
  float distance = duration * 0.034 / 2; // Test distance = high level time×velocity of sound (340M/S) / 2
  
  M5.Display.fillScreen(BLACK);
  M5.Display.setCursor(10, 50);
  M5.Display.print("Distance: ");
  M5.Display.print(distance);
  M5.Display.println(" cm");
  
  delay(500);
}

AWS IoT Core のリソースを作っていく。まずは Security からポリシーを作成する。

$ THING_ID=M5Stack_01
$ AWS_ACCOUNT_ID=111122223333
$ AWS_REGION=ap-northeast-1
$ cat <<EOF
{
    "Version":"2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "iot:Connect",
            "Resource": "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:client/${THING_ID}"
        },
        {
            "Effect": "Allow",
            "Action": "iot:Publish",
            "Resource": [
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topic/\$aws/things/${THING_ID}/shadow/update",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topic/\$aws/things/${THING_ID}/shadow/delete",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topic/\$aws/things/${THING_ID}/shadow/get",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topic/sensor/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "iot:Receive",
            "Resource": [
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topic/\$aws/things/${THING_ID}/shadow/update/accepted",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topic/\$aws/things/${THING_ID}/shadow/delete/accepted",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topic/\$aws/things/${THING_ID}/shadow/get/accepted",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topic/\$aws/things/${THING_ID}/shadow/update/rejected",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topic/\$aws/things/${THING_ID}/shadow/delete/rejected"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "iot:Subscribe",
            "Resource": [
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topicfilter/\$aws/things/${THING_ID}/shadow/update/accepted",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topicfilter/\$aws/things/${THING_ID}/shadow/delete/accepted",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topicfilter/\$aws/things/${THING_ID}/shadow/get/accepted",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topicfilter/\$aws/things/${THING_ID}/shadow/update/rejected",
                "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:topicfilter/\$aws/things/${THING_ID}/shadow/delete/rejected"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iot:GetThingShadow",
                "iot:UpdateThingShadow",
                "iot:DeleteThingShadow"
            ],
            "Resource": "arn:aws:iot:${AWS_REGION}:${AWS_ACCOUNT_ID}:thing/${THING_ID}"
        }
    ]
}
EOF

Things を作成する。Auto-generate a new certificate を選びポリシーをデバイス証明書にアタッチして、デバイス証明書と公開/秘密鍵、ルートCA証明書をダウンロードする。鍵は作成時にしかダウンロードできない。

MQTT ブローカーのエンドポイントを取得する

MQTT ブローカー Mosquitto を立ち上げて paho-mqtt でメッセージを送り QoS ごとのパケットの内容を Wireshark で見る - sambaiz-net

$ aws iot describe-endpoint --endpoint-type iot:Data-ATS
{
    "endpointAddress": "xxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com"
}

ESP32 は Wi-Fi と Bluetooth に対応している。 証明書と秘密鍵を WiFiClientSecure に設定し 8883 番ポートで X.509 証明書による mTLS 認証を行って、MQTT クライアント PubSubClient でセンサーのデータを Publish する。

IstioのSidecarでmTLS認証を行いServiceAccountによるアクセス制御を行う - sambaiz-net

Topic は事前に登録しておく必要はないが、Policy で Publish を許可しておく必要がある。

#include <M5Unified.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>

// WiFi settings
const char* ssid = "xxxxxxxxx";
const char* password = "xxxxxxxxx";

const char* thing_id = "M5Stack_01";

// AWS IoT settings (need to update)
const char* mqtt_server = "xxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com";
const char* topic = "sensor/distance";

// Sensor pins
const int TRIG_PIN = 26;
const int ECHO_PIN = 35;

// Certificates
const char* root_ca = R"(
-----BEGIN CERTIFICATE-----
....
-----END CERTIFICATE-----
)";

const char* certificate = R"(
-----BEGIN CERTIFICATE-----
.....
-----END CERTIFICATE-----
)";

const char* private_key = R"(
-----BEGIN RSA PRIVATE KEY-----
.....
-----END RSA PRIVATE KEY-----
)";

WiFiClientSecure net;
PubSubClient client(net);

void connectWifi() {
  WiFi.begin(ssid, password);

  M5.Display.fillScreen(BLACK);
  M5.Display.setCursor(0, 0);
  M5.Display.println("WiFi connecting...");
  
  int wifiRetry = 0;
  while (WiFi.status() != WL_CONNECTED && wifiRetry < 50) {
    delay(500);
    M5.Display.print(".");
    wifiRetry++;
  }
  
  if (WiFi.status() != WL_CONNECTED) {
    M5.Display.fillScreen(RED);
    M5.Display.setCursor(0, 0);
    M5.Display.printf("WiFi Failed!\nCheck SSID/Password\n");
    while(1) { delay(1000); }
  }

  delay(3000);
}

void connectAWSIoT() {
  M5.Display.fillScreen(BLACK);
  M5.Display.setCursor(0, 0);
  M5.Display.println("AWS IoT connecting...");

  net.setCACert(root_ca);
  net.setCertificate(certificate);
  net.setPrivateKey(private_key);

  client.setServer(mqtt_server, 8883);
    
  int mqttRetry = 0;
  const int maxMqttRetry = 5;
  while (!client.connected() && mqttRetry < maxMqttRetry) {
    M5.Display.printf("Try %d/%d\n", mqttRetry + 1, maxMqttRetry);
    
    if (client.connect(thing_id)) {
      M5.Display.fillScreen(GREEN);
      M5.Display.setCursor(0, 0);
      M5.Display.printf("AWS IoT Connected!\nThing: %s\n", thing_id);
      delay(3000);
      return;
    }
    
    mqttRetry++;
    delay(500);
  }
  
  if (!client.connected()) {
    M5.Display.fillScreen(RED);
    M5.Display.setCursor(0, 0);
    M5.Display.printf("AWS IoT Failed!\nError: %d\n", client.state());
    while(1) { delay(1000); }
  }
}

float getDistance() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  
  long duration = pulseIn(ECHO_PIN, HIGH);
  return duration * 0.034 / 2;
}

void setup() {
  M5.begin();
  M5.Display.setTextSize(2);
  
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  
  connectWifi();
  connectAWSIoT();
}

void loop() {
  // Keep MQTT connection
  if (!client.connected()) {
    connectAWSIoT();
  }
  client.loop();
  
  float distance = getDistance();
  
  char payload[100];
  snprintf(payload, sizeof(payload), 
           "{\"distance\":%.2f,\"timestamp\":%lu}", 
           distance, millis());
  
  M5.Display.fillScreen(client.publish(topic, payload) ? BLACK : RED);
  M5.Display.setCursor(0, 0);
  
  if (client.state() == 0) {
    M5.Display.printf("Published!\nDistance: %.1f cm\n", distance);
  } else {
    M5.Display.printf("Publish Failed!\nState: %d\n", client.state());
  }
  
  delay(5000);
}

MQTT test client で Topic を Subscribe して値が届いているのを確認する。

Message routing のルールを作成する。ルールは Topic に対する SQL と送り先からなり、送り先には SNS, S3, Step Functions などさまざまなサービスが指定できる。Topic はシングルクォートで囲まないと反応しないが、囲まなかった場合でもエラーにならず作成できてしまうので注意。

SNS に Protocol=Email の Subscriptions を登録するとメールが届く。