M5StickでMQTTをAWS IoT Coreに送ってみた



# はじめに

以前までの記事でMQTTを使ったデータ送信を一通り抑えることができました。
今回は、前回同様M5 Stick C Plus（厳密にはCPlus2）と照度センサーを用いて取得したデータをAWSに送ってみます。
過去に、下記のハンズオン資料を使用して触ったことがあるのですが、自分のケースで実践したいのでやってみました。これらの構築手順と使用した際の感想を記載していこうと思います。

### 過去に触ったAWS IoT Coreの教育資料

https://catalog.us-east-1.prod.workshops.aws/workshops/b3e0b830-79b8-4c1d-8a4c-e10406600035/ja-JP


# AWS環境構築

## IAMの設定
少し手抜きではありますが、いつも使っているユーザを使用しました。
今回のユーザはIoT関係の許可を与えている必要があります。

## AWS IoT Core
MQTTの仕組みを使用したいIoT Coreを使用します。
モノの作成を行って接続するための証明書を発行し、ポリシーを設定して接続許可を作成します。
まずはAWSコンソールでIoT Coreを検索して、IoT Coreのページに移動し、ステップを進めてください。

### モノの登録
ここではモノを作成します。具体的には、接続するデバイスが使用するための証明書の発行を行います。

#### 登録画面への遷移
登録画面に移動するため、サイドバーの【1個のデバイスを接続】を選択するか、もしくはページ右上側の【デバイスを接続】を選択します。

![IoTCoreモノの登録画面への遷移.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671049/016cf078-a798-24b4-e436-1f417c96ebe2.png)

#### ステップ1
【ステップ1 デバイスを準備する】という画面に遷移しました。
ここではpingを飛ばして接続確認することができますが、M5ではpingを飛ばすというか、シェルがないのでできません。このまま先へ進みます。
(ここはデバイスの確認用なので問題はないと思います)

#### ステップ2
【ステップ2 デバイスを登録して保護する】に遷移しました。
まずはモノの作成に、作成したいものの名前を入力します。入力規則さえ守っていればどんな名前でも大丈夫です。ご自身が認識しやすい名前がベストと思います。
![モノの作成_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671049/1df386f8-4365-6cf8-97f2-996a82253d44.png)

モノのタイプは任意でつけましょう。
私は、使っているデバイスの【m5_stick_c_plus2】としました。デバイスの名前や用途ごとにつけたほうがいいように思えました。

#### ステップ3
【ステップ3 プラットフォームとSDKを選択】に遷移しました。
今回はSDKを使わないので、どれでも大丈夫ですが、自分の環境に近いものを選択しておきます。

![モノの作成_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671049/61a2a98a-a4ca-e16a-ca9a-40b966e36da7.png)

・・・バージョンの項目で、Linuxがanyとなっているのが心強いです。(Linux好き)


#### 【重要】ステップ４
【ステップ4 接続キットをダウンロード】に遷移しました。
ここはめちゃくちゃ重要です。
必ず【接続キット】をダウンロードしましょう。忘れると漏れなく不幸になります。

![モノの作成_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671049/a4acea52-f896-1d9f-1e01-b824a28ecbb0.png)

:::note alert
ここでダウンロードを忘れると情報を再度取得できないものがあります。
その場合、やり直しになるので必ずダウンロードしてください。
:::

#### ステップ5
【ステップ5 接続キットを実行】に遷移しました。
これらはシェルで実行するものなので先に進んで大丈夫です。【続行】を選択してモノの作成を終了します。


### ルート証明書
ルート証明書を取得します。
サイドバーの【セキュリティ】 ＞ 【認証期間】を選択します。
その後、CA証明書が現れますので、マルチアカウントであることを確認し、画面遷移してルート証明書をダウンロードします。

![AWS_ルート証明書.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671049/567c4aa0-b8bd-df9d-4d2d-ba4fbaa683c8.png)

証明書が現れなければあマルチアカウントで有効な証明書を作成してください。


### ポリシーの設定
まずは、クライアントIDとtopic名を決めてください。(任意の名前で大丈夫です)
次に、サイドバーの【セキュリティ】 ＞ 【ポリシー】を選択します。
遷移した画面に、ポリシー一覧が表示されるので、【[先程作成したモノの名前]-Policy】を選択してください。

ポリシーの内容が表示されます。画面下部に全てのバージョンという項目がありますので、【アクティブ】になっているものの横にチェックをつけて、【バージョンを編集】を押下します。

![AWS_ポリシーの編集.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671049/b617c639-3a54-2c9c-e679-7c89c0273c00.png)

ここでバージョンを編集できます。
クライアントIDが【client_test_id】、topic名が【test/aaa】とした場合の例を記載します。
なお、XXXXXXXXXXXXはAWSのアカウントIDです。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Publish",
        "iot:Receive",
        "iot:PublishRetain"
      ],
      "Resource": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:topic/test/aaa"
    },
    {
      "Effect": "Allow",
      "Action": "iot:Subscribe",
      "Resource": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:topicfilter/test/aaa"
    },
    {
      "Effect": "Allow",
      "Action": "iot:Connect",
      "Resource": "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:client/client_test_id”
    }
  ]
}
```

:::note info
繰り返しになりますが、クライアントIDとトピック名は任意のものをつけていただいて構いません。
ただし、それぞれの入力値は後ほど設定ファイルに記載するということをご認識ください。
トピック名は「AWS_CLIENT_ID」、トピック名は「PUB_TOPIC_NAME」という変数に入れます。
忘れた場合は、ポリシーを確認すればOKです。
:::


### エンドポイントの確認
サイドバーの設定を選択します。
画面中央にエンドポイントと記載があるので、そこのURLをコピーします。
セキュリティポリシーは要件に満たないものを選択するとメッセージが送れずに沼ります。
私は【IoTSecurityPolicy_TLS13_1_2_2022_10】を選択しました。
・・・はじめに【IoTSecurityPolicy_TLS13_1_3_2022_10】を選択し、5日くらい接続できず原因不明で苦戦していました。

![エンドポイントの確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671049/f5805515-50c3-5f3d-c259-b4a643a8f9a6.png)

:::note info
ここで取得したURL情報は、M5ソースコード内の設定ファイルに入力します。
「MQTT_SERVER」という変数内に情報を格納します。
:::



# M5 Stick設定

M5 Stickの設定について記載します。
（少し前にM5 Stick C plus2を購入しましたのでそちらを使いました）

## 暗号化キーの準備
「モノの登録」の際にAWSからダウンロードした下記の3つのファイルを使用します。
下記の3つのファイルを手元に準備します。
- （長いアルファベット)-certificate.pem.crt　(ルート証明書)
- （モノの名前）.cert.pem　(デバイス証明書)
- （モノの名前）.private.key　(暗号化キー)

例えばモノの名前が「iot20240214」だったら、デバイス証明書と暗号化キーの名前はそれぞれ下記のようになります。
- iot20240214.cert.pem　(デバイス証明書)
- iot20240214.private.key　(暗号化キー)


## 秘密情報用の設定ファイルを作成
証明書、暗号化キーと、ポリシーで決めたトピック名,クライアントID名、さらにエンドポイントを設定ファイルに記述します。
証明書、暗号化キーは一度ファイルを開き、中に記載されている文字列をコピーして貼り付けてください。

```c++
const char* WIFI_SSID = "WiFi等のSSID"
const char* WIFI_PASSWORD = "WiFi等のパスワード"
const char* MQTT_SERVER = "先程取得したエンドポイントのURL"
const char *AWS_CLIENT_ID = "ポリシーで決めたクライアントID"
const char *PUB_TOPIC_NAME = "ポリシーで決めたトピック名"

const char *AWS_IOT_ROOT_CA = R"(-----BEGIN CERTIFICATE-----
（ルート証明書ファイルの中身　← 「（長いアルファベット)-certificate.pem.crt」）
-----END CERTIFICATE-----
)";

const char *AWS_IOT_DEVICE_CERT = R"(-----BEGIN CERTIFICATE-----
（デバイス証明書ファイルの中身　← 「（モノの名前）.cert.pem」）
-----END CERTIFICATE-----
)";

const char *AWS_IOT_PRIVATE_KEY = R"(-----BEGIN RSA PRIVATE KEY-----
（プライベートキーファイルの中身　← 「（モノの名前）.private.key」）
-----END RSA PRIVATE KEY-----
)";
```

## センサー制御用のコード作成
設定ファイルを作成したので、メインコードを作成します。

```c++
#include <M5StickCPlus2.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <time.h>

// wifi
#define QOS 0
WiFiClientSecure httpsClient;
PubSubClient mqttClient(httpsClient);

// [Secret]
// WiFi config information.
extern const char *WIFI_SSID;
extern const char *WIFI_PASSWORD;
extern const char *MQTT_SERVER;
extern const int MQTT_PORT;

// secret_info.cpp (aws iot core)
extern const char *AWS_CLIENT_ID;
extern const char *PUB_TOPIC_NAME;
extern const char *AWS_IOT_ROOT_CA;
extern const char *AWS_IOT_DEVICE_CERT;
extern const char *AWS_IOT_PRIVATE_KEY;


// For fetch date time from ntp server.
const char *ntpServer = "ntp.nict.jp";
const long gmtOffset_sec     = 9 * 3600;
const int daylightOffset_sec = 0;


// For mqtt publisher.
unsigned long lastMsg = 0;
#define MSG_BUFFER_SIZE (200)
char msg[MSG_BUFFER_SIZE];
int value = 0;


uint16_t analogRead_value = 0;
uint16_t digitalRead_value = 0;

void setup_wifi() {
  M5.Lcd.print("Connecting to ");
  M5.Lcd.print(WIFI_SSID);

  WiFi.disconnect(true);
  delay(1000);

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    M5.Lcd.print(".");
  }

  M5.Lcd.print("\nWiFi connected");
}

void callback(char*, byte* , unsigned int);

void setup_awsiot() {
  const int port = 8883;
  httpsClient.setCACert(AWS_IOT_ROOT_CA);
  httpsClient.setCertificate(AWS_IOT_DEVICE_CERT);
  httpsClient.setPrivateKey(AWS_IOT_PRIVATE_KEY);
  mqttClient.setServer(MQTT_SERVER, port);
  mqttClient.setCallback(callback);
}

void connect_awsiot(){
  while (!mqttClient.connected()) {

    M5.Lcd.setTextColor(BLUE);
    M5.Lcd.setCursor(10, 2);
    M5.Lcd.fillScreen(YELLOW);
    M5.Lcd.print("Attempting MQTT connection...\n");
    M5.Lcd.setTextSize(1);
    M5.Lcd.print(AWS_CLIENT_ID);
    M5.Lcd.print("\n");
    M5.Lcd.print(MQTT_SERVER);
    M5.Lcd.print("\n");
    M5.Lcd.setTextSize(2);

    if (mqttClient.connect(AWS_CLIENT_ID)) {
      M5.Lcd.print("Connected.");
    }
    else {
      M5.Lcd.print("Failed\n   rc=");
      M5.Lcd.print(mqttClient.state());
      M5.Lcd.print("\n Try again in 5 seconds");
      delay(5000);
    }
  }
}

void mqtt_callback(char* topic, byte* payload, unsigned int length) {
    Serial.print("Recieved. topic=");
    Serial.println(topic);

    char sub_message[length];
    for (int i = 0; i < length; i++) {
      sub_message[i] = (char)payload[i];
    }
    Serial.println(sub_message);
}


String createJson() {
  String timestampStr = fetchTimestamp();
  // char timestamp[20];
  // timestampStr.toCharArray(timestamp, 20);
  String json = "{";
    json += "\"status\": \"success\",";
    json += "\"payload\": {";
    json +=   "\"timestamp\": \"";
    json +=   timestampStr;
    json +=   "\", ";
    json +=   "\"lightVal\": ";
    json +=   analogRead_value;
    json +=   "}";
    json += "}";
  return json;
}

String fetchTimestamp() {
  struct tm timeInfo;
  getLocalTime(&timeInfo);

  char timestamp[20];
  sprintf(timestamp, "%04d/%02d/%02d %02d:%02d:%02d",
    timeInfo.tm_year + 1900,
    timeInfo.tm_mon + 1,
    timeInfo.tm_mday,
    timeInfo.tm_hour,
    timeInfo.tm_min,
    timeInfo.tm_sec);

  return timestamp;
}


void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setTextColor(BLUE);
  M5.Lcd.setCursor(10, 2);
  M5.Lcd.fillScreen(YELLOW);
  pinMode(32, INPUT);
  pinMode(36, INPUT_PULLUP);

  setup_wifi();
  setup_awsiot();


  M5.Lcd.printf("UNIT LIGHT \n");
  // For fetch current time.
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
}


void callback(char* topic, byte* payload, unsigned int length) {
}


void loop() {

  if (!mqttClient.connected()) {
    connect_awsiot();
  }

  mqttClient.loop();

  unsigned long now = millis();
  if (now - lastMsg > 10000) {
    analogRead_value = analogRead(33);
    digitalRead_value = digitalRead(32);
    lastMsg = now;
    ++value;
    String json = createJson();
    char jsonStr[200];
    json.toCharArray(jsonStr,200);
    snprintf(msg, MSG_BUFFER_SIZE, jsonStr);

    if (analogRead_value <= 2600) {
      M5.Lcd.fillScreen(BLUE);
    }
    else {
      M5.Lcd.fillScreen(YELLOW);
    }
    M5.Lcd.setTextSize(2);
    M5.Lcd.setCursor(10, 2);
    M5.Lcd.printf("UNIT LIGHT \n");
    M5.Lcd.setCursor(10, 30);
    M5.Lcd.print("status: ");
    M5.Lcd.setTextColor(BLACK);
    M5.Lcd.setCursor(105, 30);
    M5.Lcd.printf("%d (%d)", digitalRead_value, analogRead_value);
    M5.Lcd.setCursor(10, 50);
    M5.Lcd.setTextSize(1);
    M5.Lcd.println(msg);

    mqttClient.publish(PUB_TOPIC_NAME, msg);
  }
}
```


# 送信テスト
サイドバー内の【MQTTテストクライアント】で確認できます。

まずはMQTTテストクライアントを立ち上げます。
MQTTテストクライアントでは、トピック毎にメッセージを確認できます。先程入力したトピックを入力してみましょう。
(図では、"test/home"というトピック名になっています)

![MQTTの接続確認1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671049/959d9f75-0fec-0d53-7b79-43642e485975.png)


トピックを指定すると、指定した直後からメッセージをブラウザ内に表示することができます。


![MQTTの接続確認2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671049/dfff748a-7ad6-c5ca-864a-2abf8ae65688.png)

上記の通り、メッセージが表示されたので、無事トピックにメッセージが送信できていることが分かりました。

今回はここまでにします。
次回は（少しボリュームは少ないですが）、メッセージを取得するまでを実施したいと思います。


# Reference
今回の記事作成にあたり下記の記事を参考にさせていただきました。
- https://netmark.jp/2021/05/2021-05-09-12-03.html
- https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/iot-gs.html