ESP32(M5Atom-U)からHTTPS通信でGetとPostを実行する

# はじめに
- 初学者の備忘録
- 殴り書き

# やりたいこと
- ESP32(M5Atom-U)からhttps通信でgetとpostメソッドを実行したい

# 準備したこと
- AWS Lambda+Dynamodb+CloudFrontで超簡潔なHTTPS APIを作成
- Arduino IDEのインストール
- M5Atom-Uへコードを書き込める環境

# 環境
- M5Atom-U(ESP32 PICO)
- Arduino IDE 2.3.2
    - Boards Manager: [M5Stackライブラリ](https://static-cdn.m5stack.com/resource/arduino/package_m5stack_index.json)の`M5Stack/M5Atom`を使用

# 早速実装していく
### 1. ルート証明書を取得する
まず前提として、httpsはSSL/TLSを使用、プロトコルを暗号化して送信することで、盗聴や改ざんを防ぎます。
そのためにサーバは身分を認証することが必須であり、サーバは証明書を発行します。それをSSL/TLS証明書といい、webを作ったことがある人は取得したことがあるのではないでしょうか。

さて、この証明書、しっかりしたものか検証が必要ですよね？
その役割を行うのがブラウザ等にインストールされているルート証明書です。

早速見に行ってみましょう
Firefoxから今回使用するAPIのエンドポイントに飛びます

鍵マーク -> 安全な接続 -> 詳細を表示
![スクリーンショット 2024-07-24 4.32.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3504047/3aaf570a-edd5-2c11-d7df-7699e41a5152.png)![スクリーンショット 2024-07-24 4.37.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3504047/e29b6c86-ab9d-88ef-dd8a-208ffbe43fe4.png)
![スクリーンショット 2024-07-24 4.36.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3504047/bf2908ae-2e16-7751-0168-0d0b8ab9755e.png)

セキュリティ -> 証明書を表示
![スクリーンショット 2024-07-24 4.39.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3504047/638edfca-fb49-79de-b18a-0daf13078349.png)

証明書が3個ほど表示されますが、使える証明書は一つです
よく分からない方は全ての証明書をダウンロードし、使えるのをコードで実際に試して見つけるのがおすすめです
私はAmazon RSA 2048 M03の証明書でいけました
![スクリーンショット 2024-07-24 4.42.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3504047/53674088-31d0-4f2f-9ccc-3f112d9f876f.png)

ダウンロードした`.pem`ファイルを適当にエディタなどで開いてコピペします
`---BEGIN CERTIFICATE---`から`---END CERTIFICATE---`まで全てコピペします
![スクリーンショット 2024-07-24 4.48.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3504047/b97eb0e0-7131-392a-6227-3722ff423e44.png)

### 2. GETを実装してみる
以下のようなコードを用意しました
参考サイトとほぼ一緒です
```c:api_get.ino
#include <WiFi.h>
#include <WiFiClientSecure.h>

//wifiのSSIDとパスワードを入力
const char* ssid       = "";
const char* password   = "";

//ルート証明書
const char* root_ca = \
"-----BEGIN CERTIFICATE-----\n" \

"ここに証明書を貼り付ける\n" \

"-----END CERTIFICATE-----\n";

uint32_t WebGet_LastTime = 0;

//**************************セットアップ関数**************************
void setup() {
  Serial.begin(115200);
  delay(100);

  Serial.println();
  Serial.print("Attempting to connect to SSID: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

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

  Serial.println();
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.println(WiFi.localIP());
  delay(2000);

  WebGet_LastTime = 200000; //多めの数値で初期化しておく
}

//**************************メインの関数**************************
String https_Get(const char* host1, String target_page) {

  String ret_str;
  WiFiClientSecure https_client;
  https_client.setCACert(root_ca); // ルート証明書をセット

  if (https_client.connect(host1, 443)) {
    Serial.print(host1); Serial.print(F("-------------"));
    Serial.println(F("connected"));
    Serial.println(F("-------WEB HTTPS GET Request Send"));

    String str1 = String("GET https://") + String(host1) + target_page + " HTTP/1.1\r\n" +
                  "Host: " + String(host1) + "\r\n" +
                  "User-Agent: BuildFailureDetectorESP32\r\n" +
                  "Connection: close\r\n\r\n";

    https_client.print(str1);
    https_client.flush();
    Serial.print(str1);
    Serial.flush();

    while (https_client.connected() || https_client.available()) {
      if (https_client.available()) {
        String line = https_client.readStringUntil('\n');
        line.trim(); // 改行、空白を削除
        if (line.length() > 0) {
          ret_str += line + "\n"; // レスポンスを蓄積
        }
      }
    }

  } else {
    Serial.println(F("------connection failed"));
    if (WiFi.status() != WL_CONNECTED) {
      Serial.println("WiFi is not connected");
    } else {
      Serial.println("SSL/TLS handshake failed");
    }
  }

  if (https_client) {
    https_client.stop();
    Serial.println(F("-------Client Stop"));
  }
  Serial.flush();

  return ret_str;
}

//**************************メインループ**************************
void loop() {
  if((millis() - WebGet_LastTime) > 180000){ //180秒(3分)毎に取得
    String str = https_Get("test.com", "/api");
    Serial.println(str);
    Serial.flush();
    WebGet_LastTime = millis();
  }
}

```

Serial Monitorから確認してみると、しっかりgetできていますね！
`SSL/TLS handshake failed`とエラーが出る際はルート証明書を変えて再実行しましょう
![スクリーンショット 2024-07-24 4.59.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3504047/0d34cd45-3960-66d1-b5a8-e16e4ed2b3aa.png)

### 3. POSTを実装してみる
続いて、POSTを実装してみます
ほとんどgetのコードと変わりません

```c:api_post.ino
#include <WiFi.h>
#include <WiFiClientSecure.h>

// WiFiのSSIDとパスワードを入力
const char* ssid = "";
const char* password = "";

// ルート証明書
const char* root_ca = \
"-----BEGIN CERTIFICATE-----\n" \

"ここに証明書を貼り付ける\n" \

"-----END CERTIFICATE-----\n";

uint32_t WebGet_LastTime = 0;

//**************************セットアップ関数**************************
void setup() {
  Serial.begin(115200);
  delay(100);

  Serial.println();
  Serial.print("Attempting to connect to SSID: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

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

  Serial.println();
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.println(WiFi.localIP());
  delay(2000);

  WebGet_LastTime = 200000; // 多めの数値で初期化しておく
}

//**************************メインの関数**************************
String https_Post(const char* host1, String target_page) {
  WiFiClientSecure https_client;
  https_client.setCACert(root_ca);

  if (https_client.connect(host1, 443)) {
    Serial.println(F("Connected to server"));
    String postData = "{\"Id\": \"000\"}";
    String request = "POST " + String(target_page) + " HTTP/1.1\r\n" +
                     "Host: " + String(host1) + "\r\n" +
                     "Content-Type: application/json\r\n" +
                     "Connection: close\r\n" +
                     "Content-Length: " + String(postData.length()) + "\r\n\r\n" +
                     postData;

    https_client.print(request);
    Serial.println("Request sent");
    while (https_client.connected() || https_client.available()) {
      String line = https_client.readStringUntil('\n');
      if (line == "\r") {
        Serial.println("Headers received, body starts:");
      }
      if (line.length() > 0) {
        Serial.println(line);
      }
    }
    https_client.stop();
  } else {
    Serial.println(F("Connection failed"));
  }
  return String();
}

//**************************メインループ**************************
void loop() {
  if ((millis() - WebGet_LastTime) > 21600000) {  // 21600000ms = 6 hours
    https_POST("test.com", "/api");
    WebGet_LastTime = millis();
    }
}

```

POSTもいい感じにできました
![スクリーンショット 2024-07-24 5.10.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3504047/712d98aa-9ec5-8359-3e66-87a3358dbdd9.png)

お疲れ様でした！

# +α解説
### setup関数
setup関数とは、起動時に一度実行される関数
今回は
- Serialの通信の開始
- `WiFi.h`ライブラリでWiFiのセットアップ
- 接続の待機と確立

を行なっています
pirnt関数と要らないものを抜くと、以下のように簡潔にまとまります
公式の[arduinoドキュメント](https://www.arduino.cc/reference/en/libraries/wifi/)にメソッドが全て一覧で載っているので、詳しく知りたい方はそちらからどうぞ

```c
# incloude <WiFi.h>

void setup() {
  // 通信の開始
  Serial.begin(115200);

  // WiFiのssidとpasswordをセット
  WiFi.begin(ssid, password);

  // 待機と確立
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println("WiFiを接続しました");
}
```

### メインで使用する関数
この記事では、`https_Get`と`https_Post`を用意しています
基本送信する文章内にメソッドも記述してしまうので、GetとPostではコード上ほとんど変わりません

最小構成は以下に示す通りで、
1. `WiFiClientSecure.h`ライブラリをセットアップ
1. `https_client.connect`で接続を確認
1. `https_client.print`で送信
1. `https_client.flush`で全てのデータが返ってくるのを待つ
1. `https_client.stop`で処理を閉じる

が一連の流れとなります。
`flush`の下で送られてきた文章を処理して、戻り値で返すという処理を組むことが可能です

ちなみに`stop`は忘れるとメモリを食い散らかして大変なことになるので注意
[こちら](https://garretlab.web.fc2.com/arduino/esp32/examples/WiFiClientSecure/WiFiClientInsecure.html)を参考にしています

```c:get
# incloude <WiFiClientSecure.h>

// Get
String https_Get(const char* host1, String target_page) {

  WiFiClientSecure https_client; // 初期化
  https_client.setCACert(root_ca); // ルート証明書をセット

  // 接続を確認して処理スタート
  if (https_client.connect(host1, 443)) {

    String str1 = String("GET https://") + String(host1) + target_page + " HTTP/1.1\r\n" +
                  "Host: " + String(host1) + "\r\n" +
                  "User-Agent: BuildFailureDetectorESP32\r\n" +
                  "Connection: close\r\n\r\n";

    https_client.print(str1); // 送信
    https_client.flush(); // 処理が終わるまで待つ
  }
  https_client.stop(); // 接続を閉じる

  retrun;
}
```

```c:post
# incloude <WiFiClientSecure.h>

// Post
String https_POST(const char* host1, String target_page) {
  WiFiClientSecure https_client; // 初期化
  https_client.setCACert(root_ca); // ルート証明書をセット

  // 接続を確認して処理スタート
  if (https_client.connect(host1, 443)) {
  
    String postData = "{\"Id\": \"000\"}";
    String request = "POST " + String(target_page) + " HTTP/1.1\r\n" +
                     "Host: " + String(host1) + "\r\n" +
                     "Content-Type: application/json\r\n" +
                     "Connection: close\r\n" +
                     "Content-Length: " + String(postData.length()) + "\r\n\r\n" +
                     postData;

    https_client.print(request); // 送信
    https_client.flush(); // 処理が終わるまで待つ
  }
  https_client.stop(); // 接続を閉じる
  
  return;
}
```

### loop関数
loop関数とは、電源が切れるまでループし続ける関数です
関数実行までのタイマーと関数を呼び出しています
解説するまでもないので、今回は省略します

読んでいただき、ありがとうございました。


# 参考文献
https://qiita.com/takudooon/items/55ba92edac3e1ec9f602

https://www.mgo-tec.com/blog-entry-arduino-esp32-ssl-stable-root-ca.html

https://novicengineering.com/%E3%80%90arduino%E3%80%91setup%E9%96%A2%E6%95%B0%E3%80%81loop%E9%96%A2%E6%95%B0/

https://www.arduino.cc/reference/en/libraries/wifi/

https://garretlab.web.fc2.com/arduino/esp32/examples/WiFiClientSecure/WiFiClientInsecure.html