1-1.  非同期サーバーをクライアントから制御する①

 非同期サーバーについては、「Ⅲ. ESP32非同期サーバーで計測情報のブラウザー表示を自動更新する」において、サーバーで計測した温湿度を10秒毎にクライアント画面に反映させる実験をしました。一見するとサーバーからクライアントへの一方通行の通信なので、逆方向への通信はどうすればよいのかと疑問をもっている人がいらっしゃるかも知れません。
 例えば、特定のクライアントのブラウザーから計測間隔を変更したいとか、計測値の異常を検出する閾値を設定しておいてその値を変更したいといった、クライアントからサーバーへの指示が必要な場合です。
 今回はこれを試すために、簡単なスケッチでクライアントから非同期サーバーを制御してみましょう。

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



1.テーマと事前準備

①テーマ

 サーバー上のカレント日時をブラウザに表示させることにします。ほぼ1秒毎に表示を自動更新します。クライアント側には日時表示画面にボタンを設置して、それをクリックするたびにサーバーに設置したLEDを点灯/消灯させます。
 サーバーからの日時の送信と、クライアントのボタン操作によるサーバー制御が間断なくできることを確認して、双方向通信ができることを検証するのが目的です。


②ハードの準備

 ブレッドボードにESP32開発ボードをセットして、写真のようにLEDを配線します。LEDのアノード(+)をESP32のGPIO25に接続し、カソード(-)側は300Ω程度(ここでは330Ωを使用)の抵抗器を介してGNDに接続します。

③クライアント画面のデザイン

 いつものように、まず画面のデザインをしておきましょう。テキストエディターで、ざっくりとHTMLを書いて画面イメージを仕上げます。

 細かい説明は省略しますが、6行目から始まる<style>と</style>の間で、表示文字の字体や色、表示する領域の様式、ボタンの形状などの表示スタイルを指定しています。先頭が「.」で始まるのはクラス名で、以降でスタイルを参照する際に使用します。
 25,26行目はそれぞれLEDの点灯状態と日時を表示するコードですが、表示内容の部分はスケッチを作成する時に変更します。29行目はボタンで、onClick='btnClicked(this)'は、スケッチ作成時にクリックした時に実行させるJavaScript関数の記述を予定しています。以上はいずれも<style>で定義したクラス名でスタイルを指定しています。
<!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;}
      .btn_on {width:100px; height:43px; padding;1px 1px; vertical-align: middle; text-decoration:none; font-size:16px; background-color:
        #668ad8; color: #FFF; border-bottom: solid 4px #627295; border-radius: 4px;}
      .btn_on:active { -webkit-transform: translateY(0px); transform: translateY(0px);
        border-bottom: none;}
    </style>
  </head>
  <body>
    <h1>Asynchronous Server</h1>
    <p><table>
      <tr><th>ELEMENT</th><th width="100">VALUE</th></tr>
      <tr><td>LED status</td><td><span id="message" class="value">OFF</span></td></tr>
      <tr><td>Current time</td><td><span id="curtime" class="value">yyyy/mm/dd<br>hh:nn:ss</span></td></tr>
    </table></p>
	  <br>
      <button class='btn_on' onclick='btnClicked(this)' id='btn'>CHANGE</button>
  </body>
</html>


2.非同期サーバー用ライブラリー

 非同期Webサーバーを作成するには、ESP32用の次の2つのライブラリーをインストールする必要があります。
 ①非同期Webサーバーライブラリー

 ・ここをクリックしてESPAsyncWebServerライブラリーをダウンロードしてください。
 ・zipフォルダを解凍すると、ESPAsyncWebServer-masterフォルダが表示されます。
 ・フォルダー名をESPAsyncWebServerに変更して、Arduino IDEのインストールライブラリーフォルダーに
  移動します。
②非同期TCPライブラリーのインストール
 ・ここをクリックしてAsync TCPライブラリーをダウンロードしてください。
 ・zipフォルダを解凍すると、AsyncTCP-masterフォルダが表示されます。
 ・フォルダー名をAsyncTCPに変更して、Arduino IDEのインストールライブラリーフォルダーに移動します。
(参考)
 Windows環境では、Arduino IDEインストールライブラリーフォルダーは次の位置にあります。
  C:\Users\%UserName%\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.?\libraries


3.非同期通信のHTMLの構造

 非同期通信ではHTMLの記述が重要な役割を担います。クライアント上のブラウザーに表示する画面の様式はもちろん、サーバーとの通信や受信データの表示処理などをここに記述します。サーバーとの通信には、JavaScriptの組み込みオブジェクト「XMLHttpRequest」を使用しますが、これによってページ全体を再表示することなくデータを送受信するという、Ajaxの基幹技術を活用することが可能になります。
 サーバーからのデータ受信についてはすでに「Ⅲ. ESP32非同期サーバーで計測情報のブラウザー表示を自動更新する」で詳述していますが、ここで再度HTML作成の要点を簡単に解説し、クライアントからの送信方法などを確認しておきましょう。以下の①から③は、掲載のコードの順序に沿って解説します。

①CSSで表示パーツのスタイルを定義する

 51行目のstyleタグで、画面全体の表示位置や表示文字サイズを定義し、クラス名valueでは受信データの表示様式、btn_onではボタンのデザインやアクティブ時の状態を定義しています。


②入出力で使用するパーツを配置する

 66行目のbodyタグではパーツの配置を定義して、各パーツをハンドリングするための名称とプレースフォルダーを指定します。id=に続くのが名称で、このようにすることで日時の表示領域はcurtime、LEDの状態表示領域はmessageという名前で扱うことができます。%~~%は値を表示するためのプレースフォルダーで、JavaScriptで記述する関数によってこの部分に受信データが設定されます。


③JavaScriptでハンドリング関数を記述する

 76行目のscriptタグでは、データ受信時のデータ編集関数として、getCurTime(77行目)とgetMessage(87行目)の2つの関数を記述しています。それぞれ日時データとLEDの点灯状態を編集しますが、処理内容はほぼ同じなのでgetCurTimeに絞って説明します。
 まず78行目でXMLHttpRequestのインスタンスを作成しています。このオブジェクトを使うことで、対象領域以外を書き換えることなくデータを表示することが可能になります。続く79行目では、onreadystatechangeイベントでドキュメントの状態に変化があった場合に実行するコールバック関数を設定します。関数内では正常に受信していれば、idがcurtimeの要素にサーバーからの受信データthis.responseTextを設定します。これによって受信データが表示されます。
 次のopenメソッドでは、第1引数でリクエストメソッドが"GET"であることを指定し、第2引数ではリクエストを送信するURL(パス)を、第3引数ではtrueで非同期通信であることを指定します。そしてsend(null)でサーバーへリクエストを送信します。
 102,103行のJavaScriptメソッドsetIntervalは、第一引数に実行する関数を、、第二引数に与えられた間隔(ミリ秒)指定するとその間隔で実行します。つまり関数getCurTimeとgetMessageは、それぞれ500ミリ秒と200ミリ秒ごとに実行されて、サーバーの対象URLにリクエストを送信します。


④サーバーへの送信処理について

 サーバーへの送信処理は、74行目のボタンをクリックすることで起動します。これによって97行目のJavaScriptの関数btnClickedがキックされて、サーバーのパス'/update'にリクエストを送信します。
 このように、サーバーへのデーター送信は受信と同様にGETメソッドでリクエストを送信するだけです。
/* 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;}
      .btn_on {width:100px; height:43px; padding;1px 1px; vertical-align: middle; text-decoration:none; font-size:16px; background-color:
        #668ad8; color: #FFF; border-bottom: solid 4px #627295; border-radius: 4px;}
      .btn_on:active { -webkit-transform: translateY(0px); transform: translateY(0px);
        border-bottom: none;}
    </style>
  </head>
  <body>
    <h1>Asynchronous Server</h1>
    <p><table>
      <tr><th>ELEMENT</th><th width="100">VALUE</th></tr>
      <tr><td>LED status</td><td><span id="message" class="value">%MESSAGE%</span></td></tr>
      <tr><td>Current time</td><td><span id="curtime" class="value">%CURTIME%</span></td></tr>
    </table></p>
	  <br>
      <button class='btn_on' onclick='btnClicked(this)' id='btn'>CHANGE</button>
  </body>
  <script>
    var getCurTime = function () {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("curtime").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/curtime", true);
      xhr.send(null);
    }
    var getMessage = function () {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("message").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/message", true);
      xhr.send(null);
    }
    function btnClicked(element) {
      var xhr = new XMLHttpRequest();
      xhr.open("GET", "/update", true);
      xhr.send();
    }
    setInterval(getCurTime, 500);
    setInterval(getMessage, 200);
  </script>
</html>)rawliteral";

 以上から分かるように、非同期通信ではサーバーにリクエストをした後は、クライアント側(ブラウザ上)のJavaScriptが直接サーバーとのやりとりを行い、その間はブラウザーは開放されます。そしてサーバーからのレスポンスが返ってきたら、サーバーから取得した情報をブラウザーの対象位置に表示します。
 クライアント側からの通信は表示を伴わないので、サーバーの特定URLへ向けて単純に"GET"リクエストを発行するだけです。


4.サーバー側処理プログラムの構造

 非同期サーバー側のスケッチは少し変わった構造になります。特に必要がない限り void loop()の反復処理では何もしません。処理の主体は、クライアントからのリクエストに対して向け先のURLを解釈して、それに対応した処理を行い、結果をリクエストオブジェクトのsend_Pメソッドを使ってクライアントに応答することです。
 そして、これらの処理はすべて初期処理 void setup()において、"GET"リクエストに対するハンドラーを登録する方法で行います。
 以上の大まかな枠組みを記憶に留めて、コードの細部を眺めてみましょう。

①コードの冒頭部

 14行目でESP32の非同期Webサーバーライブラリーを使用するためにヘッダーファイルをインクルードしています。
 27行目はNTPサーバーから時刻情報を取得するために、対象サーバー名とUTC(Coordinated Universal Time:協定世界時)との時間差(秒数で)を指定しています。
 38行目では非同期WebサーバーAsyncWebServerオブジェクトを、HTTPポートアドレスを指定してインスタンス化しています。
 41行目はコメントの通り、LEDを接続したGPIO25ピンの指定、42行目はLEDの点灯/消灯状態(CST_ONまたはCST_OFF)を保持するグローバル変数です。
#include <Arduino.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>

/* Function Prototype */
void doInitialize();
void connectToWifi();
String editPlaceHolder(const String&);
String getMessage();
String getCurTime();

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

// NTPサーバー時刻設定用
const char* ntpServer = "pool.ntp.org";
const int   gmtOffset_sec = 9 * 3600;

/* 基本属性定義  */
#define SPI_SPEED   115200
#define CST_ON      "ON"
#define CST_OFF     "OFF"

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

/* LEDピン */
const int ledPin = 25;              // LED出力接続ピン
String ledState = "";               // 出力ピンの状態


②定型処理部

 111,112行では、スケッチ内に記述している初期化処理関数とWi-Fiルーター接続関数を呼んでいます。
 113行目は、NTPサーバーからの時刻取得メソッドconfigTimeで、"pool.ntp.org"サーバーからgmtOffset_secだけ時間差がある現在時刻を取得しています。
 116行目以降で"GET"リクエストに対するハンドラーを登録しています。記述は簡潔ですが、これらの説明はかなり込み入ってくるので、詳細は「Ⅲ. ESP32非同期サーバーで計測情報のブラウザー表示を自動更新する」の「3.コード解説 ②定型のsetup()とloop()」を参照してください。
 大まかな内容を述べると、AsyncWebServerオブジェクトのonメソッドを使って、サーバーがリクエストをリスンするルート(ブラウザーから指定されたURL)と、そのリクエストを受信した時に実行させる関数を指定しています。
 116行目は最初の接続リクエストに対する指定で、119,122,125行目は個別に指定されたルートに対応したものです。最初の接続に対しては、別に定義された「String editPlaceHolder(const String&)」を伴って、HTML全体をsend_Pメソッドで"text/html"としてクライアントに通知する「コールバック関数」を定義しています。
 続く個別のルートについては、それぞれに対応した処理関数をコールバック関数とし、send_Pメソッドで"text/plain"としてクライアントに通知するよう設定しています。
 混線しそうな説明になってしまいましたが、要は、ここで行っているのはクライアントからそれぞれのルートへのリクエストがあった時に実行する関数を、コールバック関数として指定しているということです。
 また、クライアントのボタンがクリックされた場合は、125行目で同様の設定をする前にその時点のLEDの状態を反転させるようにしています。
 これらの設定を行った後、138行目のserver.begin()でサーバーを起動します。
void setup(){
    doInitialize();             // 初期化処理をして
    connectToWifi();            // Wi-Fiルーターに接続する
    configTime(gmtOffset_sec, 0, ntpServer);  // まず時刻を合わせる

    // GETリクエストに対するハンドラーを登録して
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send_P(200, "text/html", strHtml, editPlaceHolder);
    });
    server.on("/curtime", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send_P(200, "text/plain", getCurTime().c_str());
    });
    server.on("/message", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send_P(200, "text/plain", getMessage().c_str());
    });
    server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){
        if (ledState == CST_ON) {
            ledState = CST_OFF;
            digitalWrite(ledPin, LOW);
        } else {
            ledState = CST_ON;
            digitalWrite(ledPin, HIGH);
        }
        Serial.print("LED changed : ");
        Serial.println(ledState);
        request->send_P(200, "text/plain", getMessage().c_str());
    });
    // サーバーを開始する
    server.begin();
}
 
void loop(){
}


③GETリクエストに対応したハンドラー

 String editPlaceHolder()は、最初の接続リクエスト時にコールバック関数として呼ばれ、日時表示領域とLEDの状態表示領域に該当データを返却します。
 String getCurTime()とString getMessage()は、それぞれ現在の日時を編集した文字列、LEDの点灯状態を返却します。
 これらはコールバック関数として動作することに注意してください
/*****************************************************************************
 *                          'GET' Request Handlers                           *
 *****************************************************************************/
/* プレースホルダー処理 */
String editPlaceHolder(const String& var) {
    if (var == "MESSAGE") {
        return getMessage();
    }
    else if (var == "CURTIME") {
        return getCurTime();
    }
}

/* 現在の日時を取得する */
String getCurTime() {
    struct tm timeinfo;
    char buf[64];

    if(getLocalTime(&timeinfo)) {
        sprintf(buf,"%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);
        Serial.println(buf);
        return buf;
    }
}

/* LEDの状態を通知する */
String getMessage() {
    return ledState;
}


④その他の関数

 doInitialize()はシリアルモニターの初期化とLEDが接続されたGPIOの設定を行い、LEDの初期点灯状態をOFFにします。
 connectToWifi()は、所定のSSIDとパスワードでWi-Fiルーターに接続して、シリアルモニターに割り当てられたIPアドレスを表示します。
 いずれもsetup()の先頭で呼ばれます。
/*****************************< Other functions >*****************************/
/* 初期化処理 */
void doInitialize() {
    Serial.begin(SPI_SPEED);
    pinMode(ledPin, OUTPUT);           // GPIO設定:LED
    digitalWrite(ledPin, LOW);
    ledState = CST_OFF;
}

/****************************< Connect functions >****************************/
/* 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();
}


5.動作検証

 事前に準備したESP32をPCにUSB接続してスケッチをコンパイル、アップロードします。アップロードが終了すると
    Leaving...
    Hard resetting via RTS pin...
と表示されます。
 次にシリアルモニターを開いて、ESP32のリセットボタン(RST,RESET,ENなどと印刷された)を押します。シリアルモニターに
    WiFi connected.
     *IP address: 192.168.0.32
のように割り当てられたIPアドレスが表示されるので、PCのWebブラウザを起動してアドレスバーにそのIPアドレスを入力します。


 「Current time」の時刻表示は刻々と変わっていて、[CHANGE]ボタンをクリックするとESP32ボードのLEDが点灯して「LED status」表示がONになります。もう一度クリックするとLEDは消灯し、表示はOFFに変わります。そして、これらの通信状況はシリアルモニターに表示されます。

 このようにして、Webサーバーからの値で日時表示を更新しながら、同時にクライアントから指示することも可能なことが確認できました。
 今回はサーバーの特定URL("/update")へ向けて単純に"GET"リクエストを発行しましたが、オプションパラメータでデータを送ることも可能です。このあたりは、次回で実験できればと考えています。では、お楽しみに!


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