OpenWatherMap for UNO R4 WiFi
OpenWatherMap for UNO R4 WiFi

はじめに

Bodmer さんの OpenWeather
Bodmer さんの OpenWeather

今回、個人的には初めての WiFi アプリを作ろうと思い立ち、お題を「Arduino で REST API」に選んでみました。

TFT_eSPI の作者 Bodmer さんの、カッコいいグラフィックデザインが特徴の OpenWeather に刺激を受け、これを UNO R4 WiFi で動かしたい!と思ったからです。

メモリの豊富な ESP32 で動作するソフトを UNO R4 WiFi 用にマイグレーションするにはそれなりの工夫が必要でしたので、その過程で得た知見を3回に分けて共有する予定です。

  1. RTC、NTP、HTTP、TLS、ルートCA認証の REST API 用サンプルコード(今回)
  2. ArduinoJson で大きな JSON データを少ない RAM で解析する方法
  3. アイコン画像のデータを UNO R4 の Flash 容量内に収める方法

1回目の今回は、TLS のセキュリティレベルを診断してくれる How’s My SSL?REST API エントリ を叩くサンプルコードを紹介します。

特に WiFi 関連や RTC、NTP 関連の情報は巷にも溢れているので、他のサイト様ではあまり紹介されていない、より実践的なコードを提示したいと思います 🤘

本稿で提示するプログラムの動作環境は以下の通りです。

バージョン UNO R4 WiFi ESP32
Arduino IDE 2.3.4 2.3.4
プラットフォーム Arduino UNO R4 Boards by Arduino 1.5.3 esp32 by Espressif Systems 3.3.8

ボードタイプ別に §1 〜 $4 のサンプルコードを順に1つのスケッチファイルコピペしていけば動作する構成にしています。ご利用の際は、出典のご確認も合わせてお願い致します 🙇‍♂️

§1. メインのスケッチ

まずは全体像です。以前紹介した DO_EVERY() マクロで2つのタスクを駆動します。1つは1秒毎に内臓 RTC の時刻を表示し、もう1つは2分毎に JSON データを取得します。コードは UNO R4 WiFi と ESP32 で共通です。

#include <Arduino.h>
#include <stdio.h>
#include <time.h>

// プロトタイプ宣言
void wifiInit(void);        // WiFiの初期化
void rtcInit(void);         // RTC/NTPの初期化
void httpInit(void);        // TLSの初期化

bool rtcSyncNTP(void);      // 一定周期毎にRTCを更新する関数
bool httpRequest(void);     // HTTPリクエストを送信する関数
bool readResponse(void);    // レスポンスボディを読み込む関数
uint32_t rtcGetTime(void);  // RTCからUNIX時刻を取得する関数

// 周期的にタスクを駆動するマクロ
#define DO_EVERY(period, lastTime)  static uint32_t lastTime = 0; for (uint32_t now = millis(); lastTime == 0 || now - lastTime >= period; lastTime = now)

void setup() {
  Serial.begin(115200);
  while (!Serial || millis() < 1000);

  wifiInit(); // WiFiを初期化
  rtcInit();  // RTC/NTPを初期化
  httpInit(); // TLSを初期化
}

void loop() {
  rtcSyncNTP();

  DO_EVERY(1000, lastTime1) {   // 1秒毎に実行
    struct tm tm;
    time_t time = rtcGetTime(); // RTCからUNIX時刻を取得
    localtime_r(&time, &tm);    // 現地時刻に変換
    printf("%02d:%02d:%02d\n", tm.tm_hour, tm.tm_min, tm.tm_sec);
  }

  DO_EVERY(120000, lastTime2) { // 2分毎に実行
    if (httpRequest()) {        // HTTPリクエストを送信
      readResponse();           // レスポンスを取得
    }
    Serial.println("Waiting for next session...");
  }
}

§2. WiFi の初期化

ここからは UNO R4 WiFi 版と ESP32 版を分けたコードを示します。プロジェクトを GitHub などに上げる際、WiFi の SSID やパスワードは arduino_secrets.h に分離し、.gitignore で除外するように設定しましょう。

arduino_secrets.h
#define SECRET_SSID ""
#define SECRET_PASS ""

§2.1 UNO R4 WiFi 版

UNO R4 WiFi に不要な部分を除き、ほぼ公式の例題通りのコードです。

サンプルコード
#include <WiFiS3.h>
#include "arduino_secrets.h"

void wifiInit(void) {
  Serial.print("Connecting to WiFi network...");

  // WiFi のファームウェアバージョンを確認する
  String fv = WiFi.firmwareVersion();
  if (fv < WIFI_FIRMWARE_LATEST_VERSION) {
    Serial.println("Please upgrade the firmware.");
  }

  while (WiFi.status() != WL_CONNECTED) {
    WiFi.begin(SECRET_SSID, SECRET_PASS);
    delay(1000); // 接続の待ち時間
  }

  // print your board's IP address:
  Serial.println("done.");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP().toString());

  // print the received signal strength:
  Serial.print("signal strength (RSSI):");
  Serial.print(WiFi.RSSI());
  Serial.println(" dBm");
}

while() 内の「接続待ち時間」は 1 秒に縮めちゃってますが(オリジナルは 10 秒)、アクセスポイントとの関連もあるかと思うので、お使いの環境に合わせて調整してください。

また WiFi.status()WiFi.begin() ですが、UNO R4 WiFi ではこの順番に実行しないと接続までの時間が伸びちゃいます(理由は追跡していません、悪しからず…😅

出典

§2.2 ESP32 版

WiFi を司るチップが分かれていない分、UNO R4 WiFi 版に比べて少しだけシンプルです。

サンプルコード
#include <WiFi.h>
#include "arduino_secrets.h"

void wifiInit(void) {
  Serial.print("Connecting to WiFi network...");

  WiFi.mode(WIFI_STA); // ステーションモードに設定(デフォルト)
  WiFi.begin(SECRET_SSID, SECRET_PASS);

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

  // print your board's IP address:
  Serial.println("done.");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP().toString());

  // print the received signal strength:
  Serial.print("signal strength (RSSI):");
  Serial.print(WiFi.RSSI());
  Serial.println(" dBm");
}
出典

§3. RTC と NTP サーバーの設定

特に UNO R4 の RTC は精度が悪い ことで有名ですが、両ボードとも内蔵の RTC を使うことにします。

§3.1 UNO R4 WiFi 版

まずは追加のハードウェアなしに時計として使えるリソースを洗い出してみました。

  • RTC ライブラリ
    LOCO(低速オンチップオシレータ)または SOSC(サブクロック発振器)をクロックソースとする RA4M1 の RTC モジュールを I/F したクラスライブラリです。アラーム機能に加え、年月日を取得するカレンダー機能を備えています。
  • NTPClient ライブラリ
    NTP サーバーから取得した時刻と、HOCO(高速オンチップオシレータ)をクロックソースとする GPT32(汎用 PWM タイマ)で刻まれる millis() で時間を管理します。
  • WiFi.getTime()
    (オンボードの ESP32-S3-MINI が)WiFi ルータから得た時刻を取得するメソッドです。精度はルータの仕様/性能に依存し、また ESP32-S3 での扱われ方も不明です。このメソッドは Arduino 系ボード専用で、Espressif 系にはありません。
NTPClient ライブラリ
NTPClient ライブラリ

実際に実験してみると精度はどれも似たり寄ったりです。

個人的には NTPClient で定期的に NTP サーバーから時刻を取得すれば、RTC は不要だと思いますが、NTP で RTC を補正するのが一般的な使い方でしょう。

NTPClient は、ライブラリのインストールが必要です。

サンプルコード

NTP の更新周期を短くして RTC の精度の悪さを補いたいところですが、更新頻度が高いとアクセス遮断される可能性があります。例えば NICT では 1時間平均で20回を超えないよう 注意喚起されています。

そこで複数の NTP サーバーを順に変えるコードにしてみました。1分の更新周期でも3つ以上のサーバーを使えば1サーバー当たり3分の更新周期となります。

#include <RTC.h>
#include <WiFiUdp.h>
#include <NTPClient.h>

#define TIMEZONE_OFFSET   (9 * 3600)        // UTCに対する時差(日本は UTC+09:00)
#define NTP_SYNC_INTERVAL (1 * 60 * 1000LU) // 1分毎に更新

// NTP サーバーのリスト
static int serverID = 0;
static const char *ntpServers[] = {
  "ntp.nict.jp",
  "ntp.jst.mfeed.ad.jp",
  "pool.ntp.org",
  "time.google.com",
};
#define NTP_N_SERVERS (sizeof(ntpServers) / sizeof(ntpServers[0]))

// UDP経由でNTPサーバーとパケットの送受信を行うためのインスタンス
static WiFiUDP Udp;
static NTPClient timeClient(Udp, ntpServers[0], TIMEZONE_OFFSET, NTP_SYNC_INTERVAL);

// Connect to an NTP server and get the time
void rtcInit(void) {
  Serial.print("Initializing RTC...");

  RTC.begin();
  timeClient.begin();

  Serial.println("done.");
}

// 一定周期毎にNTPサーバーに接続し、時刻を取得する(loop()中で呼び出す)
bool rtcSyncNTP(void) {
  if (timeClient.update()) {
    // 取得した時刻をRTCに設定する
    auto unixTime = timeClient.getEpochTime();
    RTCTime timeToSet = RTCTime(unixTime);
    RTC.setTime(timeToSet);
    Serial.println("The RTC was just set to: " + timeToSet.toString());

    // 次のサーバーを設定する
    serverID = (serverID + 1) % NTP_N_SERVERS;
    const char *server = ntpServers[serverID];
    Serial.println("Next NTP server: " + String(server));

    timeClient.end();
    timeClient.setPoolServerName(server);
    timeClient.begin();
    return true;
  }
  return false; // 更新周期に達していない、まはたタイムアウトで接続に失敗した場合
}

// 現在時刻を取得する
uint32_t rtcGetTime(void) {
  struct timeval tv;          // time_t tv_sec; suseconds_t tv_usec;
  gettimeofday(&tv, NULL);    // ArduinoCore-renesas では戻り値は常に 0
  return (uint32_t)tv.tv_sec; // time_t (8バイト) --> uint32_t (4バイト)
}
getEpochTime() メソッドについて

ソースコード を見ると、NTP サーバーのデータ受信時刻に millis() で観測した経過時間とタイムゾーンのオフセット値を足しているだけです。

また戻り値が符号無し32ビット整数なので、2038年問題ならぬ2106年問題 を内包していますが、まぁ、問題にはならないでしょう 😌

夏時間(Daylight Saving Time, DST)の自動設定について

NTPClient クラスの setTimeOffset() メソッド でタイムゾーンのオフセット値を変えられますが、そもそもシステムの系に「タイムゾーン」自体が実装されていないため、特定地域の夏時間を設定するには追加のライブラリが必要です。Bodmer さんの OpenWeather ライブラリ では以下が採用されています。

これらにより標準時間(STD)と夏時間(DST)の適用ルールが設定可能ですが、ルールが毎年変わる地域もあるなど、わりと面倒です。

今回は OpenWeatherMap の JSON データに埋め込まれたタイムゾーンのオフセット値を使うので、夏時間設定の掘り下げは致しません。悪しからず 🙄

出典

§3.2 ESP32 版

地域別の標準時間と夏時間の適用ルール はとても複雑です。ESP32 では NTP と RTC を同時に設定してくれる configTzTime() が用意されていて、POSIX タイムゾーン形式の文字列 を指定すれば 良きに計らってくれる ので、とっても便利です。

サンプルコード
#include <esp_sntp.h>
#include <time.h>

#define TIMEZONE_STRING   "JST-9"           // UTCに対する時差(日本は UTC+09:00)
#define NTP_SYNC_INTERVAL (3 * 60 * 1000LU) // 3分毎に同期

// NTP サーバーのリスト
static const char *ntpServers[] = {
  "ntp.nict.jp",
  "ntp.jst.mfeed.ad.jp",
  "pool.ntp.org",
  "time.google.com",
};

// 更新完了フラグ、プロトタイプ宣言
static bool synchronized = false;
static void syncNTP_cb(struct timeval *tv);

void rtcInit(void) {
  // 更新周期と更新時のコールバック関数を登録
  sntp_set_sync_interval(NTP_SYNC_INTERVAL);
  sntp_set_time_sync_notification_cb(syncNTP_cb);

  // 環境変数とNTPサーバーを設定
  configTzTime(TIMEZONE_STRING, ntpServers[0], ntpServers[1], ntpServers[2]);

  Serial.print("Waiting for RTC to be synchronized with NTP server...");

  // RTCが更新されるまで待機
  struct tm timeInfo;
  if (!getLocalTime(&timeInfo)) {
    Serial.println("Synchronization timeout.");
  }
}

static void syncNTP_cb(struct timeval *tv) {
  if (sntp_get_sync_status() != SNTP_SYNC_STATUS_RESET) {
    struct tm timeInfo;
    localtime_r(&tv->tv_sec, &timeInfo);

    Serial.print("The RTC was just set to: ");
    Serial.println(&timeInfo, "%Y-%m-%d %H:%M:%S");
    synchronized = true;
  }
}

// UNO R4 WiFi版との互換性のための関数
bool rtcSyncNTP(void) {
  if (synchronized) {
    synchronized = false;
    return true;
  }
  return false;
}

// 現在時刻を取得する
uint32_t rtcGetTime(void) {
  return time(NULL);  // time_t (8バイト) --> uint32_t (4バイト)
}
rtcSyncNTP() と syncNTP_cb() について

UNO R4 WiFi とは異なり、NTP サーバーとの同期は loop() とは異なるコンテキストで実行されます。以下は printRunningTasks(Serial); の実行結果で、コア0のタスク tiT が関係している ようです(残念ながらソースは発見できずでした)。

Tasks: 11, Runtime: 600s, Period: 600002711us
Num	            Name	Load	Prio	 Free	Core	State
  8	        loopTask	  4%	   1	 6396	   1	Running
  5	           IDLE0	  5%	   0	  464	   0	Ready
  6	           IDLE1	  0%	   0	  568	   1	Ready
 10	             tiT	  0%	  18	 2828	   0	Blocked
  7	         Tmr Svc	  0%	   1	 3596	   *	Blocked
  2	            ipc1	  0%	  24	  480	   1	Suspended
 11	         sys_evt	  0%	  20	 2900	   0	Blocked
 12	  arduino_events	  0%	  19	 3152	   1	Blocked
  3	       esp_timer	  0%	  22	 7908	   0	Suspended
 13	            wifi	  1%	  23	 4292	   0	Blocked
  1	            ipc0	  0%	  24	  484	   0	Suspended
出典

§4. HTTP over SSL/TLS とルートCA証明書

いよいよ REST API の本体です。まずは UNO R4 WiFi と ESP32 で使えるライブラリです。

UNO R4 WiFi ESP32 機能
ArduinoHttpClient HTTPClient - 下記ライブラリのラッパーとして機能
- 応答のヘッダとボディを分けてくれる
WiFiSSLClient NetworkClientSecure HTTPS 用ライブラリ
WiFiClient NetworkClient HTTP 用ライブラリ

HTTP はヘッダーファイルとポート番号を変えるだけですし、今日日使わないので省略します。

サンプルコードには TLS による暗号化に加え、ルート CA 証明書(ここではサーバー証明書の検証のためにクライアントが保有する証明書を指します)を導入します。市販品の WiFi カメラ脆弱性 と違い、趣味レベルの Arduino アプリを狙うハッカーが居るとは思えませんが、意識が大事ということで… 😅

(あくまで可能性のおハナシですが… Arduino_JSON に含まれる cJson の脆弱性 が狙われ、中間者攻撃 で踏み台にされるシナリオも!? 😜)。

§4.1 ラッパークラスを使用しないバージョン

readResponse() は、次回で JSON データの解析処理に置き換え予定ですが、今回はシリアルモニターに出力するだけです。

howsmyssl_root_ca.hwww.howsmyssl.com のルート CA 証明書です。

howsmyssl_root_ca.h
// ISRG Root X1 (Downloaded by Safari, Firefox)
// openssl x509 -inform der -in ISRG\ Root\ X1.cer -out ISRG\ Root\ X1.pem
// NotBefore: Jun  4 11:04:38 2015 GMT; NotAfter: Jun  4 11:04:38 2035 GMT
R"literal(
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)literal";

サンプルコードは、ヘッダーファイルと変数を差し替えるだけで、残りは UNO R4 WiFi 版と ESP32 版とで同じコードが動きます(サーバー証明書の検証を行わない場合を除く)。

UNO R4 WiFi、ESP32 共通のサンプルコード
#if defined(ARDUINO_UNOR4_WIFI)
  #include <WiFiSSLClient.h>
  WiFiSSLClient client;
#elif defined(ESP32)
  #include <NetworkClientSecure.h>
  NetworkClientSecure client;
#endif

#define PORT  443 // HTTPS用TCPポート

static constexpr char host[] = "www.howsmyssl.com"; // サーバーのホスト/ドメイン
static constexpr char path[] = "/a/check";          // サーバーのパス

// www.howsmyssl.com のルートCA証明書
static constexpr char root_ca[] = 
#include "howsmyssl_root_ca.h"
;

void httpInit(void) {
  // ルートCA証明書の設定
  // サーバー証明書の検証を行わない場合は次の通り
  // UNO R4 WiFi: root_ca を nullptr にする、またはコメントアウト
  // ESP32: setInsecure() に差し替え
  client.setCACert(root_ca);
}

bool httpRequest(void) {
  Serial.print("\nConnecting to " + String(host) + ":" + String(PORT) + "...");

  // サーバーに接続
  if (!client.connect(host, PORT)) {
    Serial.println("Connection failed!");
    client.stop();
    return false;
  }

  Serial.println("connected.\nSend HTTP request...");

  // HTTP リクエストを送信(行末に "\r\n" が付加される)
  client.println(String("GET ") + path + " HTTP/1.1");
  client.println(String("Host: ") + host);
  client.println("Connection: close");
  client.println();

  // レスポンスヘッダを受信
  bool detected = false;
  while (client.connected()) {
    String header = client.readStringUntil('\n');
    Serial.println(header);
    if (header == "\r") { // 空の "\r\n" を受信した?
      Serial.println("End of headers");
      detected = true;
      break;
    }
  }

  // ヘッダが受信出来なかった?
  if (!detected || !client.available()) {
    client.stop();
    Serial.println("Invalid response.");
    return false;
  }

  return true; // レスポンスボディの受信へ
}

// HTTP レスポンスボディを受信する
bool readResponse(void) {
  while (client.available()) {
    char c = client.read();
    Serial.write(c);
  }

  client.stop();
  Serial.println("\ndone.");
  return true;
}

JSON データの末尾に含まれる診断結果からは、対応している暗号化スイート(暗号技術の詰め合わせ)こそ違いますが、両ボードとも TLS 1.2 に対応していることが分かります。

{
  "tls_version": "TLS 1.2",
  "rating": "Probably Okay"
}
ルート CA 証明書の取得方法
openssl コマンドを使う方法(推奨)

環境に合わせた openssl のインストール方法は、各自調べてください。コンソールで下記コマンドを打ち込み、保存したファイルをテキストエディタで開きます。

  • Linux、MacOS
openssl s_client -connect target-server.com:443 -showcerts < /dev/null > cert_chain.txt
  • Windows
openssl s_client -connect target-server.com:443 -showcerts < nul > cert_chain.txt
openweathermap.org の例
openweathermap.org の例

上から順に -----BEGIN CERTIFICATE----------END CERTIFICATE----- で囲まれ Base64 でエンコーディングされたブロックがチェーンされているので、一番最後のルートCA証明書のブロックをコピーして使います。また 有効期限もちゃんと管理しましょう!

ブラウザからダウンロードする方法

証明書を取得するサイトを開き、URL の先頭についた「設定」や「鍵」、「セキュリティ」のアイコンから証明書をダウンロードすることができます。

Safari の例
Safari の例

Safari の場合は、証明書のアイコンをドラッグ&ドロップでデスクトップなどに保存したのち、次のコマンドで DER(Distinguished Encoding Rules)形式の .cer ファイルをプレーンなテキストの PEM 形式に変換します。

openssl x509 -inform der -in certificate.cer -out certificate.pem

また Google Chrome でダウンロードした証明書では認証されない場合があったので要注意です。Google では独自のルート認証局(Google Trust Services)を運用していて、それが原因かと思われます。

Base64 でエンコード済みのデータから有効期限を抽出する方法

-----BEGIN CERTIFICATE----------END CERTIFICATE----- のブロックから有効期限を抽出するには、次のコマンドで標準入力から同ブロックを貼り付けます。

openssl x509 -noout -dates
-----BEGIN CERTIFICATE-----
(ここに Base64 データを貼り付け)
-----END CERTIFICATE-----
(最後にリターンキーまたは Ctrl+D で入力終了)

あるいは、ヒアドキュメントを使った方が分かりやすいでしょうか。

openssl x509 -text -noout << 'EOF'
-----BEGIN CERTIFICATE-----
(ここにBase64データ)
-----END CERTIFICATE-----
EOF
出典
  • WiFiWebClientSSL
    Arduino 系ボード用の例題です。証明書の類は使われていません。
  • WiFiClientSecure
    ESP32 用の例題ですが、埋め込まれた証明書が有効期限(2027年3月12日)内であるにもかかわらず、なぜか接続に失敗します。また証明書を使わない WiFiClientInsecure(不安全な WiFi クライアント)もありますが、趣味レベルを超える場合は 注意喚起のコメント を一読すべきでしょう。

§4.2 ラッパークラスを使用するバージョン

こちらは UNO R4 WiFi 版と ESP32 版とで、クラスライブラリの実装が結構異なります。

ArduinoHttpClient (UNO R4 Wifi) 版

ArduinoHttpClient ライブラリ
ArduinoHttpClient ライブラリ

IDE のライブラリマネージャーから ArduinoHttpClient をインストールし、§4.1 ラッパークラスを使用しないバージョン を置き換えます。

ライブラリマネージャのアイコンには「EXPERIMENTAL」の表記が残っていて、放置されている感がありますが、Arduino 謹製のボードに対応しているため、Arduino Cloud との連携など、ソコソコの需要があるのだと思います。

サンプルコード
#include <WiFiSSLClient.h>
WiFiSSLClient client;

#define PORT  443 // HTTPS用TCPポート

static constexpr char host[] = "www.howsmyssl.com"; // サーバーのホスト/ドメイン
static constexpr char path[] = "/a/check";          // サーバーのパス

#include <HttpClient.h>
HttpClient http(client, host, PORT);

// www.howsmyssl.com のルートCA証明書
static const char *root_ca = 
#include "howsmyssl_root_ca.h"
;

void httpInit(void) {
  // ルートCA証明書をセット
  client.setCACert(root_ca); // スキップするには nullptr を渡すかコメントアウトする
}

bool httpRequest(void) {
  Serial.println("Send HTTP request to " + String(host) + ":" + String(PORT) + "...");

  // HTTP リクエストを送信
  int status = http.get(path);

  // https://github.com/arduino-libraries/ArduinoHttpClient/blob/master/src/HttpClient.h#L12-L24
  Serial.println("GET status: " + String(status));

  if (status == HTTP_SUCCESS) {
    // HTTP ステータスコードを取得する
    status = http.responseStatusCode();
    Serial.println("HTTP status: " + String(status));

    if (200 <= status && status < 300) {
      // レスポンスヘッダを受信
      if (http.skipResponseHeaders() == HTTP_SUCCESS) {
        return true; // レスポンスボディの受信へ
      }
    }
  }

  http.stop();
  Serial.println("Invalid response.");
  return false;
}

// HTTP レスポンスボディを受信する
bool readResponse(void) {
  while (client.available()) {
    char c = client.read();
    Serial.write(c);
  }

  http.stop();
  Serial.println("\ndone.");
  return true;
}
出典
  • SimpleGet
    シンプル過ぎる例題です。そもそも examples には TLS を使った例題がありません。

HTTPClient (ESP32) 版

レスポンスのヘッダセクションを気にする必要がなく、UNO R4 WiFi 版よりもシンプルです。

サンプルコード
#include <NetworkClientSecure.h>
NetworkClientSecure client;

#define PORT  443 // HTTPS用TCPポート

static constexpr char host[] = "www.howsmyssl.com"; // サーバーのホスト/ドメイン
static constexpr char path[] = "/a/check";          // サーバーのパス

#include <HTTPClient.h>
HTTPClient http;

// www.howsmyssl.com のルートCA証明書
static const char *root_ca = 
#include "howsmyssl_root_ca.h"
;

void httpInit(void) {
  // ルートCA証明書をセット
  client.setCACert(root_ca); // スキップするには setInsecure() に差し替える
}

bool httpRequest(void) {
  Serial.println("Send HTTP request to " + String(host) + ":" + String(PORT) + "...");

  // HTTP リクエストを送信
  String scheme = "https://";
  http.begin(client, scheme + host + path);
  int status = http.GET();

  // https://github.com/espressif/arduino-esp32/blob/master/libraries/HTTPClient/src/HTTPClient.h#L46-L123
  Serial.printf("HTTP status: %d %s\n", status, http.errorToString(status).c_str());

  if (200 <= status && status < 300) {
    return true; // レスポンスボディの受信へ
  }

  http.end();
  Serial.println("Invalid response.");
  return false;
}

// HTTP レスポンスボディを受信する
bool readResponse(void) {
  while (client.available()) {
    char c = client.read();
    Serial.write(c);
  }

  http.end();
  Serial.println("\ndone.");
  return true;
}
出典
  • BasicHttpsClient
    NetworkClientSecure を使った例題です。ヘッダのセクションからステータスコードや特定のヘッダを抽出する機能があるので、アプリによっては便利かもです。

おわりに

今回は、まずメモリの豊富な ESP32 で基本動作を確認し、その後 UNO R4 の RAM と Flash に収まるようマイグレーションしてきました。本記事でも欲張って両プラットフォームのコードを紹介したので分かり難かったと思いますが、どうぞ、ご勘弁を…。

今回提示したコードには Serial.print()String を多用していますが、RAM の厳しい UNO R4 では避ける方が無難です。

という事で、次回は RAM の、第3回は Flash の省メモリ対策を紹介したいと思います。

まだ途中ですが、アプリは GitHub にも上げた ので、興味があれば覗いてみてください。