Arduino – ESP32 WiFiClientSecure ライブラリで、安定して https ( SSL )記事をGETする方法

ESP32 ( ESP-WROOM-32 )

スケッチの入力

では、Arduino IDE に以下のスケッチを入力してみてください。

SSL ( https )ページを定期的に安定して文字列を取得するには、微妙な細かいプログラムノウハウがありますので、解説をよく読んでみてください。

因みに、このプログラムは無保証です。このプログラムが原因で起きたいかなるトラブルも当方では一切責任を負いませんので予めご了承ください。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

#include <WiFiClientSecure.h>

const char* ssid = "xxxx"; //ご自分のルーターのSSIDに書き換えてください。
const char* password = "xxxx"; //ご自分のルーターのパスワードに書き換えてください

//Yahoo! Japan RSSニュース、ルート証明書
const char* yahoo_root_ca= \
  "-----BEGIN CERTIFICATE-----\n" \
  "MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ\n" \
  "RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD\n" \
  "VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX\n" \
  "DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y\n" \
  "ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy\n" \
  "VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr\n" \
  "mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr\n" \
  "IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK\n" \
  "mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu\n" \
  "XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy\n" \
  "dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye\n" \
  "jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1\n" \
  "BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\n" \
  "DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92\n" \
  "9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx\n" \
  "jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0\n" \
  "Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz\n" \
  "ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS\n" \
  "R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp\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; //起動時に記事をGETするために、多めの数値で初期化しておく
}
//*********************メインループ***************************
void loop() {
  if((millis() - WebGet_LastTime) > 180000){ //180秒(3分)毎に記事取得
    String str = https_Web_Get("news.yahoo.co.jp", "/rss/topics/top-picks.xml", '\n', "</rss>", "<title>", "</title>", "◆ ");
    Serial.println(str);
    Serial.flush();
    WebGet_LastTime = millis();
  }
}
//*****************Yahoo RSSニュースを取得する関数*************
String https_Web_Get(const char* host1, String target_page, char char_tag, String Final_tag, String Begin_tag, String End_tag, String Paragraph){

  String ret_str;
  
  WiFiClientSecure https_client;
  https_client.setCACert(yahoo_root_ca); //Yahooサイトのルート証明書をセットする
  
  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";
           str1 += "Host: " + String( host1 ) + "\r\n";
           str1 += "User-Agent: BuildFailureDetectorESP32\r\n";
           str1 += "Connection: close\r\n\r\n"; //closeを使うと、サーバーの応答後に切断される。最後に空行必要
           str1 += "\0";

    https_client.print(str1); //client.println にしないこと。最後に改行コードをプラスして送ってしまう為
    https_client.flush(); //client出力が終わるまで待つ
    Serial.print(str1);
    Serial.flush(); //シリアル出力が終わるまで待つ
    
  }else{
    Serial.println(F("------connection failed"));
  }

  if(https_client){
    String dummy_str;
    uint16_t from, to;
    Serial.println(F("-------WEB HTTPS Response Receive"));

    while(https_client.connected()){
      while(https_client.available()) {
        if(dummy_str.indexOf(Final_tag) == -1){          
          dummy_str = https_client.readStringUntil(char_tag);

          if(dummy_str.indexOf(Begin_tag) >= 0){
            from = dummy_str.indexOf(Begin_tag) + Begin_tag.length();
            to = dummy_str.indexOf(End_tag);
            ret_str += Paragraph;
            ret_str += dummy_str.substring(from,to);
            ret_str += "  ";
          }
        }else{
          while(https_client.available()){
            https_client.read(); //サーバーから送られてきた文字を1文字も余さず受信し切ることが大事
            //delay(1);
          }
          delay(10);
          https_client.stop(); //特に重要。コネクションが終わったら必ず stop() しておかないとヒープメモリを食い尽くしてしまう。
          delay(10);
          Serial.println(F("-------Client Stop"));

          break;
        }
        //delay(1);
      }
      //delay(1);
    }
  }
  
  ret_str += "\0";
  ret_str.replace("&amp;","&"); //XMLソースの場合、半角&が正しく表示されないので、全角に置き換える
  ret_str.replace("&#039;","\'"); //XMLソースの場合、半角アポストロフィーが正しく表示されないので置き換える
  ret_str.replace("&#39;","\'"); //XMLソースの場合、半角アポストロフィーが正しく表示されないので置き換える
  ret_str.replace("&apos;","\'"); //XMLソースの場合、半角アポストロフィーが正しく表示されないので置き換える
  ret_str.replace("&quot;","\""); //XMLソースの場合、ダブルクォーテーションが正しく表示されないので置き換える
  
  if(ret_str.length() < 20) ret_str = "※ニュース記事を取得できませんでした";
  
  if(https_client){
    delay(10);
    https_client.stop(); //特に重要。コネクションが終わったら必ず stop() しておかないとヒープメモリを食い尽くしてしまう。
    delay(10);
    Serial.println(F("-------Client Stop"));
  }
  Serial.flush(); //シリアル出力が終わるまで待つ

  return ret_str;
}

【解説】

●3-4行目:
ご自分のルーターの SSID と パスワードに書き換えてください。

●7-28行:
ここで上記で取得した Yahoo! Japan RSS ニュースサイトのルート証明書をテキストエディタで開いて、それをスケッチ上にコピペして、サンプルスケッチの書式に合わせて編集したものです。
これは、現時点ではグローバル変数領域に置いていますが、メモリを食うので、将来的には SDカードや SPIFFS に保存しておいた方が良いでしょう。

●37-50行:
ここで、Wi-Fiルーター(アクセスポイント)と接続します。

●53行:
初回起動直後に Web記事をGET するために、57行の数値より大きくしておきます。

●57-62行:
メインループで、180秒毎に Yahoo! Japan RSS ニュース記事を取得して、String 文字列に格納してシリアルモニターに表示させます。
ここの秒数は短くし過ぎないように注意してください
相手サーバーに負担をかけますし、高速で記事を連続 GET するとサイバー攻撃されていると判断されますので要注意です。

また、取得する文字列が長いので、60行目にあるように、シリアルモニターに全て出力されるまで待つ関数を使います。
Serial.flush() は昔の Arduino ではメモリをクリアする動作でしたが、今は出力が終わるまで待つという動作らしいです。

●69行目:
WiFiClientSecure ハンドルは、ここで宣言します。
ローカル関数内で宣言した方が、ローカル関数を出る時にメモリを解放してくれるので、節約につながります。

●70行目:
ここで、Yahoo! Japan RSS サイトのルート証明書をセットします。

●72行目:
ここで、Yahoo! Japan RSS サイトに SSL 通信でコネクションします。ここで、ルート証明書の照合を行っていると思われます。

●77-86行:
ここでは、Yahoo! Japan RSS サイトにニュース記事を取得するための GET リクエストを送信しています。
ここで注意することは、80行目で¥r¥nという形で空行1行を送信しているので、83行目の https_client.print を println にしないことです。
println にしてしまうと、さらに改行を送ってしまうので、レスポンスが正しく帰って来ない可能性があります。
以前、私は println にしていて、なぜか希に記事が取得できないことがありましたが、println を print に直したら、記事が100% 取得できるようになりました。
要するに、安定動作の為には、余分な文字は送らないことが大事ということです。
そして、84行目や86行目の flush は送信が終わるまで待つ関数です。
これは、効果があるのかどうかわかりませんが、気持ち的に良くなったような気がします。
注意してほしいのは、これはメモリをクリアする関数ではないということです。

●92-125行:
ここで、Yahoo! Japan RSSから送られてきたデータを受信して、必要な文字列を抽出します。
Yahooから送られてくる文字列は、ブラウザでWebページを開いた時の HTML ソースコードその物です。
そこから、ニュース記事の文字列だけを抽出します。
そして、上記でも述べたように、SSL で暗号化されて送られてくるので、それを WiFiClientSecure ライブラリで復号しているわけです。
http ページよりも多量の文字列を扱うために、メインループのスタックメモリを多く消費するので、前々回の記事で紹介したように、main.cpp のスタックサイズを増やす必要があります

詳しく調べていないのですが、おそらく、72行目の connect() 関数と、97行目の connected() 関数で公開鍵、秘密鍵のやり取りを行っていて、100行目の readStringUntil で暗号を復号しているものと思われます。

readStringUntil 関数で、改行コード(‘¥n’)まで、dummy_str に文字列を格納し、String文字列関連の関数である、indexOf 関数やsubstring関数で、<title>~</title>の間の文字列を抽出します。
ここで、注意していただきたいのは、改行コードまでの文字列取得ができるのは、対象のWebページの HTMLソースコードの各行が、比較的少ない文字数で作られていることに限ります
Webページの HTMLソースコードに、1行でも過度に長い文字列があれば、プログラムが HALT エラーを吐き出して動作停止してしまいます。
この、readStringUntil が使えるのは、1行が比較的少ない文字列で構成されている、Yahoo! Japan RSS サイトだからできるのです。

例えば、Google ニュースページや、英語のYahoo.com ページの HTMLソースコードを見てもらえれば分かるのですが、1行がもの凄く長い文字列がいくつかありますので、このプログラムは動作しません。
こういうページから記事を取得するにはプログラムを工夫する必要があります。
これについてはいつか記事にしたいと思います。

そして、最後の</rss>タグが見つかったら、110-113行で送信されてきた残りのデータを1文字も余すことなく受信し切ります
この受信を1文字でも漏らすと、あとで受信エラーになるので要注意です。
受信した文字は捨てます。

while文中でコメントアウトしてある delay(1) は、ESP32 のウォッチドッグタイマ(マイコンを監視するプログラム)が動作する余地を与えてやるため必要と思っていました。
しかし、シリアルモニターにエラーメッセージが出ないので、入れなくても良いです。
エラーが出ないということは、https_client.available() などの関数内で delayや vTaskDelay があり、ウォッチドッグタイマが起動する余地があると思われるからです。
ここでは、ESP8266 で活躍した yield() は無意味です。
Arduino – ESP32 の場合は yield() は効果が無く、delay(1) か vTaskDelay(1) だけのようです。

114行や116行では delay(10) としていますが、もっと短くても良いと思います。
ですが、今のところこれで安定動作しています。

115行目は特に重要です。
Yahoo サイトから送信されてきたデータを全て1文字も余さず受信し切ってコネクション終了したら必ず client.stop() してください
これは、GitHub の Arduino – ESP32 の issue でも述べられていますが、相手サーバーとコネクションが終わったら、client.stop() しないと、ESP32 のヒープメモリを食い尽くしてしまって、プログラムがダウンしますので要注意です。

●127-132行:
抽出した文字列の中には &amp や &#39 などという、HTML のエスケープシーケンス文字が正しく変換されていない場合があります。
その場合、String関連関数の replace を使って変換します。
ダブルクォーテーションやアポストロフィーなどの文字は、前に半角の¥文字を置いてください。

●134行:
取得した文字列があまりに少ない場合は記事を取得できていないので、その旨のメッセージを出します。

●136-141行:
ここでは、Yahooサイトとの通信が途中で途切れていたり、Yahoo側から切断された場合は、ここで client.stop() しておきます。

●142行:
シリアルモニターが時々変な表示になるので、あくまでおまじないのflush をしておきますが、あまり効果無いかも知れません。

以上、ソースコードの解説でした。

まとめると、Webページの記事取得で安定動作に大事なのは、余分な文字は送らないことと、サーバーから送られてきた文字は1文字も余すことなく受信し切ることです。
そして、所々の delay() 関数の配置と、client.stop を必ず設けることです。

そして、もう一つ大事なのは、SSLページ取得はルート証明書や公開鍵、秘密鍵の多量の文字列を扱う為、メモリを節約したプログラムを組むことです。
WiFiClientSecure ハンドルや、String 関数をローカル関数内で初期化するようにすれば、ローカル関数を出たらメモリを自動で解放してくれるので有効です。
ルート証明書は SD カードなどに保存しておく方が良いですね。

コンパイル書き込み実行

では、スケッチをコンパイルする前に、下図の様に Debug Level をVerbose ( 詳細 )に選択してからコンパイルしてください。

その後、シリアルモニターは 115200 bps で起動するとこんな感じになります。
Arduino IDE 1.8.2 以降では、このように Webから取得した文字列をそのまま シリアルモニターに日本語漢字出力ができるようになっています。

このように途中に、handle_errorメッセージがあります。

SSL – The peer notified us that the connection is going to be closed

これは、「相手サーバーからコネクション切断という通知が来た」という意味です。
つまり、サーバー 側が全てのデータを送信し切ってサーバー側から切断したことになります。
その後、こちらで client.stop() をかけて切断したので、このメッセージは特に問題有りません。

ウォッチドッグタイマが動いていないというメッセージも出ていないので、delay(1) は不要だと思います。

ある程度長い時間受信して、特に問題無ければ成功です。
これで、おそらく1000回以上連続受信も問題無くできると思います。

まとめ

以上、私なりの Arduino – ESP32 の WiFiClientSecure ライブラリを使った、Webページ記事の安定した取得方法でした。
うまく動作しましたでしょうか?

SSL通信の認証局についてはまだ勉強中なので、もし、誤っていることを言っていたらコメント投稿欄等でご連絡いただけると助かります。

mbedtls ライブラリが不具合については、早くGitHub の Arduino core for ESP32 が修正されてくれればいいんですけどね。

2017/8/2に修正されました。libmbedtls.a ファイルと main.cpp のスタックサイズも修正されました。

でも、これでブラウザの SSL 通信のしくみが少しは分かったのではないでしょうか。

ESP-WROOM-32 ( ESP32 ) が日本で発売された頃は、WiFiClientSecure ライブラリは不具合だらけで使い物になりませんでしたが、この記事で紹介した方法を使うことによって、かなり安定的に SSL ( https ) Webページをマイコンで取得することができるようになってきました。
しかも、前回記事で紹介したように、このマイコンはデュアルコア(マルチタスク)が可能なので、電子工作家としては夢のようなお話です。
ディスプレイを動かしながら裏でウェブ記事を GET することが可能なのです。

ということで、次回こそは、実際のデュアルコア電光掲示板を紹介したいと思います。

では、また・・・。

コメント

  1. ヨッシー より:

    いつも大変お世話になっております。
    ここの記事を参考に、ESP32でyahooニュースを表示させていたのですが、今年一月下旬より表示されなくなりました。Yahoo RSSニュースを取得する関数「https_Web_Get」でエラーとなっています。色々調べてルート証明書を変更したりしたのですが、未だに解決できません。
    なにかアドバイスいただけないでしょうか?
    よろしくお願いいたします。

    • mgo-tec mgo-tec より:

      ヨッシーさん

      記事をご覧いただき、そして試していただきありがとうございます。
      そういえば、Twitterでもツイートしましたが、2023年1月下旬でYahoo! Japan RSSサイトのルート証明書が更新されました。
      私自身のM5Stack版Yahooニュースもゲットできなくなったので、証明書公開鍵を新しくダウンロードして、コンパイルし直したら問題無く表示されました。

      この記事にあるサンプルスケッチも新しいルート証明書でコンパイルしてみましたが、私のESP32開発ボードでは問題無く表示されました。
      ちなみに環境は以下です。

      Arduino IDE 1.8.19
      Arduino core for the ESP32 ver 2.0.6

      これでもゲットできない場合は、もしかしたら証明書がedge01.yahoo証明書をダウンロードしてしまった可能性が考えられます。
      私の場合は一度間違えて、それをダウンロードしてゲットできなかったことがあります。
      証明書をテキストエディタで開くと、21行よりも倍以上の長いテキストだったので、おかしいなと思いました。
      つまり、ルート証明書ではなく、エッジ証明書だったのです。

      今回を機に、この記事のルート証明書取得方法を一新しました。
      これで試してみて下さい。

      • ヨッシー より:

        アドバイスありがとうございます。
        私も、最初は、長いルート証明書で駄目だったようです。短いものに替えて動くようになりました。

        9個のトピックスを読んでいるのですが、以前は、9個取れていたのですが、現在接続に成功するのは2~3個で不安定です。

        あとネットで色々調べていたら、下記のコマンドでルート証明書をスキップ出来ることが分かりました。

        WiFiClientSecure https_client;
        // https_client.setCACert(yahoo_root_ca); //Yahooサイトのルート証明書をセットする
        https_client.setInsecure();//skip verification

        以上 参考までに。

        • mgo-tec mgo-tec より:

          ヨッシーさん

          とりあえず動いて良かったです。

          setInsecureに関しては存じております。
          ただ、Yahoo RSSサイト程度でしたらルートCAをセットしなくても良いと思いますが、私個人としてはブラウザと同じように、信頼できるサイトをESP32に登録しているという認識で使っていった方が良いと思っています。SSL通信の理解も深まるような気がしています。
          また、以前はトピックス9個取得できていて今は2~3個しか取得できていない場合は、おそらくESP32のメモリ容量オーバーしたプログラミングだと予想されます。
          私も以前同じ経験がありました。ルートCAは文字列が多いので、メモリを消費するんですよね。
          スタックメモリとヒープメモリを管理して、オーバーしないようにソースコードを改良したら、ルートCAセットしても問題無くトピックスを取得できるようになりました。
          そういえば、Arduino core ESP32も、バージョンアップするごとにメモリが多く消費されるようになってきましたしね。
          もしかしたら将来的にYahooさんもCA無しではRSSをゲットできなくなる可能性もあるかも知れませんね。

タイトルとURLをコピーしました