Ⅲ. ESP32非同期サーバーで計測情報のブラウザー表示を自動更新する

 前回は計測用デバイスをWebサーバーにして、計測値をクライアントのWebブラウザーに表示する実験を行いましたが、計測情報は表示されたままで更新されず、新たな計測値を表示させるにはブラウザーの[更新]ボタンをクリックしなければなりませんでした。今回はこのようなブラウザーの[更新]ボタン操作をしなくても、所定の間隔で計測した値を自動的に更新表示させる方法に取り組みます。
 これを実現するには、クライアントからのリクエストを待って応答するのではなく、サーバーの準備が整ったタイミングで呼び出し元に値を通知する「非同期処理」が必要になります。


 ※スケッチのダウンロードは右側の[Download]ボタンをクリックしてください。    


1.非同期Webサーバーのライブラリー

 非同期通信ができるWebサーバーを作成するには、Arduino IDE環境で提供されているESP32用の2つのライブラリーを使用します。まず、非同期Webサーバーライブラリーと非同期TCPライブラリーをインストールしてください。
 なお、このシリーズの『ESP32による近距離無線通信の実験③ MQTT通信』を行っている場合は、すでに非同期TCPライブラリーはインストール済みなので、非同期Webサーバーライブラリーだけをインストールしてください。

①非同期Webサーバーライブラリーのインストール
 ・ここをクリックしてESPAsyncWebServerライブラリーをダウンロードしてください。
 ・zipフォルダを解凍すると、ESPAsyncWebServer-masterフォルダが表示されます。
 ・フォルダー名をESPAsyncWebServerに変更して、Arduino IDEのインストールライブラリーフォルダーに
  移動します。
②非同期TCPライブラリーのインストール
 ・ここをクリックしてAsync TCPライブラリーをダウンロードしてください。
 ・zipフォルダを解凍すると、AsyncTCP-masterフォルダが表示されます。
 ・フォルダー名をAsyncTCPに変更して、Arduino IDEのインストールライブラリーフォルダーに移動します。
(参考1)
  Windows環境では、Arduino IDEインストールライブラリーフォルダーは次の位置にあります。
   C:\Users\User-Name\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.?\libraries
(参考2)
  Arduino IDEにインストールされているライブラリーの状況は、IDE画面の
    [スケッチ]-[ライブラリをインクルード]
  を開くと展開されるライブラリー一覧で確認することができます。


2.非同期サーバーの構造

 非同期WebサーバーではHTMLが重要な役割をします。厳密に言うと、JavaScriptで組み込むHTTP通信のための組み込みオブジェクト「XMLHttpRequest」が大きな働きをします。これはAjaxの基幹技術であり、ページを再表示することなくデータを送受信することができます。つまり、ウェブページの特定の部分だけを書き換えることができるわけです。
 画面のデザインは前回とほぼ同じ(画面タイトルとコメントが異なるだけ)なので、それをベースにして変更します。コード全体は後で見ることにして、以下にHTMLのボディー部とJavaScriptの部分を掲げます。ボディー部では温度と湿度を表示する64~65行のフレーズに注目してください。
 それぞれid属性で"temperature"と"humidity"を定義しています。これによって、これらの要素を固有の識別名で操作できるようになります。そして計測値表示部分は、両側を%で挟んだプレースホルダーを設定しておきます。

  <body>
    <h1>Asynchronous Server</h1>
    <p style='color:brown; font-weight: bold'>計測値は10秒ごとに自動更新されます!</p>
    <p><table>
      <tr><th>ELEMENT</th><th>VALUE</th></tr>
      <tr><td>Temperature</td><td><span id="temperature" class="value">%TEMPERATURE%</span>
      <tr><td>Humidity</td><td><span id="humidity" class="value">%HUMIDITY%</span>
      </td></tr>
    </table></p>
  </body>
  <script>
    var getTemperature = function () {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("temperature").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/temperature", true);
      xhr.send(null);
    }
    var getHumidity = function () {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("humidity").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/humidity", true);
      xhr.send(null);
    }

    setInterval(getTemperature, 10000);
    setInterval(getHumidity, 10000);
  </script>

 scriptタグに続けて、70行目から温度情報のハンドリング関数getTemperatureのコードを記述しています。
 まず71行目でXMLHttpRequestのオブジェクトを作成し、続く72~76行において、onreadystatechangeイベントでドキュメントの状態に変化があった場合に実行させるコールバック関数を設定します。この関数が呼ばれると、リクエストのreadyStateが操作完了を表す4(DONE)でなおかつステータスコードが200(OK)であれば、id属性に"temperature"をもつ要素の内容(innertHTML)を、サーバーから受け取ったテキストthis.responseTextで置き換えます。この処理でWebブラウザー画面の温度情報だけが最新の状態に更新・表示されます。
 77~78行で、引き続き最新の温度情報を受信できるようにopenメソッドで指示します。第1引数でリクエストメソッドが"GET"であることを指定し、第2引数ではリクエストを送信するURL(パス)を指定します。第3引数のtrueは非同期通信の指示です。そして、send(null)でサーバーへリクエストを送信します。
 80行目から同様の手順で、湿度情報ハンドリング関数getHumidityを記述しています。
 最後に91行目と92行目でタイマーの設定をしています。setIntervalの第1引数ではタイマー起動の都度実行させる関数を指定します。第2引数はタイマーの待ち時間をミリ秒で指定します。これによって10秒ごとに、先に記述した計測情報のハンドリング関数が実行されます。
 このように短いJavaScriptコードですが、Webサーバーの非同期通信にかかわるリクエストとレスポンス、タイマー制御のすべてを記述しています。


3.コードの解説

①ヘッダーファイルとデータ等の定義

・15行目 ESPAsyncWebServerライブラリー用のヘッダーファイルです。
・26~27行 環境に合った情報を設定してください!
・35行目 AsyncWebServerオブジェクトを、HTTPポートアドレスを指定してインスタンス化します。
・41行目 42行目以降に、HTMLのページ全体を記述しています。初期化処理のsend_P関数では文字配列型での
     引き渡しが必要なことから、char*型で定義します。
#include <Arduino.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <DHT.h>                    // DHTセンサー用

/* Function Prototype */
String getTemperature();
String getHumidity();
String processor(const String&);
void doInitialize();
void connectToWifi();

// ルーター接続情報
#define WIFI_SSID "your_ssid"
#define WIFI_PASSWORD "your_password"

/* 基本属性定義  */
#define SPI_SPEED   115200          // SPI通信速度
#define DHTTYPE     DHT11           // DHTセンサーの型式

// Webサーバーオブジェクト
#define HTTP_PORT 80
AsyncWebServer server(HTTP_PORT);

/* DHTセンサー*/
const int DHTPin = 14;                  // DHTセンサーの接続ピン
DHT   dht(DHTPin, DHTTYPE);             // DHTクラスの生成

/* HTMLページ */
const char* strHtml = R"rawliteral(
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      html { font-family: Helvetica; display: inline-block; margin: 0px auto;text-align: center;} 
      h1 {font-size:28px;}
      body {text-align: center;} 
      table { border-collapse: collapse; margin-left:auto; margin-right:auto;}
      th { padding: 12px; background-color: #0000cd; color: white; border: solid 2px #c0c0c0;}
      tr { border: solid 2px #c0c0c0; padding: 12px;}
      td { border: solid 2px #c0c0c0; padding: 12px;}
      .value { color:blue; font-weight: bold; padding: 1px;}
    </style>
  </head>
  <body>
    <h1>Asynchronous Server</h1>
    <p style='color:brown; font-weight: bold'>計測値は10秒ごとに自動更新されます!</p>
    <p><table>
      <tr><th>ELEMENT</th><th>VALUE</th></tr>
      <tr><td>Temperature</td><td><span id="temperature" class="value">%TEMPERATURE%</span>
      <tr><td>Humidity</td><td><span id="humidity" class="value">%HUMIDITY%</span>
      </td></tr>
    </table></p>
  </body>
  <script>
    var getTemperature = function () {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("temperature").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/temperature", true);
      xhr.send(null);
    }
    var getHumidity = function () {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("humidity").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/humidity", true);
      xhr.send(null);
    }

    setInterval(getTemperature, 10000);
    setInterval(getHumidity, 10000);
  </script>
</html>)rawliteral";


②定型のsetup()とloop()

 setup()の記述はシンプルですが内容はかなり込み入っています。非同期Webサーバーなので、loop()には何も記述する必要がありません。
void setup(){
    doInitialize();             // 初期化処理をして
    connectToWifi();            // Wi-Fiルーターに接続する

    // GETリクエストに対するハンドラーを登録して
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send_P(200, "text/html", strHtml, editPlaceHolder);
    });
    server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send_P(200, "text/plain", getTemperature().c_str());
    });
    server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send_P(200, "text/plain", getHumidity().c_str());
    });
    // サーバーを開始する
    server.begin();
}
 
void loop(){
}

 setup()で行う主な処理は、GETリクエストに対するハンドラーの登録処理です。GETリクエストの発生は、Webブラウザーからの最初の接続リクエストと"/temperature"リクエスト、"/humidity"リクエストの3種類になります。それぞれの場合について、サーバーがHTTPリクエストをリスンするルートと、そのリクエストを受信した時に実行させる関数を指定する必要があります。
 これを指定するには、サーバーオブジェクトのonメソッドを使用します。
 104~106行は最初の接続リクエストに対する指定です。リスンするルートは第1引数で"/"を、第2引数ではHTTP_GETリクエストだけを受信するように指定します。第3引数は複雑な形をしていますが、これはC++のラムダ式と呼ばれるものです。引数として簡易的に関数オブジェクトを定義するための手法で、名前付き関数を宣言するよりも簡潔に記述できるために使用されます。一般的な書式は次のようになります。
    [capture list] (parameter list) {function expression}
 キャプチャーは使用しないため[]だけ。パラメータとしては、AsyncWebServerRequest型オブジェクトへのポインター*requestを受け取ります。これが104行目の機能です。
 続いて、受け取ったポインターを介してHTTPレスポンスを指定できるsend_Pメソッドを使って、関数本体を記述します。第1引数では応答するステータスコードとして200(OK)を設定。第2引数ではHTMLコンテンツ"text/html"を、第3引数ではHTML本体(Webページ全体)を渡します。通常は第4引数は省略できますが、引数にString型のポインターを持ちString型の返却値をもつAwsTemplateProcessor型コールバック関数を指定することができます。ここでは、別に定義された「String editPlaceHolder(const String&)」を指定しています。この関数の動作は⑤で説明します。
 107~109行は温度情報"/temperature"リクエストに対する指定です。リスンするルートが"/temperature"になっている他は先のケースと同じです。send_Pメソッドでは、第2引数がテキストのみのデータを指定する"text/plain"に変わり、第3引数ではString型で取得した温度計測値をc_str()で文字列型に変換しています。第4引数は不要なので省略しています。
 110行目から同様の手順で湿度情報の処理を記述し、114行のserver.begin()でサーバーを起動します。

③初期化処理

・シリアルポートを設定し、DHTセンサーを起動します。
void doInitialize() {
    Serial.begin(SPI_SPEED);
    dht.begin();                       // DHTセンサーを起動
}


④計測処理

 温度と湿度を非同期に処理するために、計測値をString型で返す2組の関数を定義します。
String getTemperature() {
    float t = dht.readTemperature();
    if (isnan(t)) {    
        Serial.println("Failed to get temperature!");
        return "--";
    }
    else {
        Serial.println(t);
        return String(t) + " ℃";
    }
}

String getHumidity() {
    float h = dht.readHumidity();
    if (isnan(h)) {
        Serial.println("Failed to get humidity!");
        return "--";
    }
    else {
        Serial.println(h);
        return String(h) + " %";
    }
}


⑤プレースホルダー処理

 最初の接続リクエスト時にコールバック関数として呼ばれ(先のsetup関数105行目で)、温度と湿度のプレースホルダーに計測値を設定(表示)します。
String editPlaceHolder(const String& var){
    if(var == "TEMPERATURE"){
        return getTemperature();
    }
    else if(var == "HUMIDITY"){
        return getHumidity();
    }
    return "??";
}


⑥コネクション確立用

・166行目 所定のSSIDとパスワードでWi-Fiのアクセスポイントに接続します。
・167行目 接続完了のWL_CONNECTEDが返却されるまで待ちます。
・175行目 割り当てられたIPアドレスをシリアルモニターに表示します。
/* Wi-Fiルーターに接続する */
void connectToWifi() {
    Serial.print("Connecting to Wi-Fi ");
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("");
    // モニターにローカル IPアドレスを表示する
    Serial.println("WiFi connected.");
    Serial.print("  *IP address: ");
    Serial.println(WiFi.localIP());
    server.begin();
}


4.動作検証

 シリアルモニターを立ち上げて実行を開始します。Wi-Fi接続が完了すると「割り当てられたIPアドレス」が表示されます(ここでは192.168.0.32)。
 パソコンまたはスマートフォンのWebブラウザーを起動して、アドレスバーに「割り当てられたIPアドレス」を入力すると下図(スマートフォンの場合は本章表題部の写真)のように計測値が表示されます。この値は10秒ごとに更新され、シリアルモニターには温度と湿度の送信内容が順次表示されます。


 スケッチ全体は、ページ先頭の[Download]ボタンでダウンロードしてください。
 これで、一定間隔で計測した値の表示を自動更新することが可能になりました。非同期通信はさまざまなケースに利用できそうですね。次回は、Webブラウザーから計測用デバイスのLEDを点灯/消灯させる実験を行います。お楽しみに!


 
Copyright (C) 2011-2024 Marchan, All rights reserved.