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

 前回は、Webサーバーからの日時情報でブラウザの表示を更新しながら、クライアントからも指示ができることを確認しました。今回は、クライアントからデータを送信してサーバーの動作をコントロールしてみましょう。非同期での双方向通信を体験するとぐっと視野が広がります。

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



1.テーマと事前準備

①テーマ

 Webサーバーに設置した温湿度センサーの計測値をブラウザに表示させながら、同時に、ブラウザ上のスライダーでLEDの明るさを設定して、サーバーのLEDを点灯/消灯させます。
 サーバーの情報によってクライアントの画面を非同期で更新しながら、クライアントからの指示とデータによってサーバーをコントロールできることを検証するのが目的です。


②ハードの準備

 『ESP32による近距離無線通信の実験① 準備作業編』で作成した計測用デバイスを使用します。これから作成する場合は「2.計測用デバイスの作成」を参照してください。

 ブレッドボード上には温湿度センサーDHT11と黄色LED、プッシュスイッチが設置されています。ESP32開発ボードはDOIT ESP32 DEVKIT V1ですが、使用するボードに合わせて、上記リンク先に掲載された配線図に従って配線してください。なお、今回はプッシュスイッチは使用しません。


③ライブラリーのインストール

 計測用デバイスや、今後予定している監視用デバイスを動作させるために、次の専用ライブラリーやドライバーを個別にインストールする必要があります。
  ・DHTセンサーライブラリー
  ・Adafruit Unified Sensor Driver
  ・Adafruit SSD1306ライブラリー
  ・Adafruit GFXライブラリー
 これらのインストールは「4.開発環境の準備 (2)必要なライブラリーのインストール」を参照してください。


2.クライアント画面のデザイン

 いつものように、まず画面のデザインをしておきましょう。テキストエディターで、ざっくりとHTMLを書いて画面イメージを仕上げます。『ESP32による近距離無線通信の実験④ Wi-Fi通信』の「Ⅲ. ESP32非同期サーバーで計測情報のブラウザ表示を自動更新する」のコードを利用する(<!DOCTYPE HTML>から</body>までを複写して</html>を付加する)のが手っ取り早いかも知れません。


 6行目から始まる<style>と</style>の間で、表示文字の字体や色、表示する領域の様式、ボタンの形状、スライダーのサイズなどの表示スタイルを指定しています。先頭が「.」で始まるのはクラス名で、以降でスタイルを参照する際に使用します。
 27~29行はそれぞれ温度と湿度、LEDの点灯状態を表示するコードで、%~~%の部分は表示内容を編集する(埋め込む)ためのプレースホルダーです。32行目はスライダーで、10刻みに0から100までのスケール幅を持たせ、初期値を50、つまり中央値に設定しています。33行目はスライダーのツマミ位置(0~100)の表示領域、35行目はボタンで、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;}
      .slider { width: 200px;}
    </style>
  </head>
  <body>
    <h1>Asynchronous Server</h1>
    <p style='color:brown; font-weight: bold'>計測値は10秒ごとに自動更新されます!</p>
    <p><table>
      <tr><th>ELEMENT</th><th width="100">VALUE</th></tr>
      <tr><td>Temperature</td><td><span id="temperature" class="value">%TEMPERATURE%</span></td></tr>
      <tr><td>Humidity</td><td><span id="humidity" class="value">%HUMIDITY%</span></td></tr>
      <tr><td>LED status</td><td><span id="message" class="value">%MESSAGE%</span></td></tr>
    </table></p>
    <p>
      <input type="range" min="0" max="100" step="10" class="slider", id="slider" valaue="50" />
        <span id="sliderValue"></span>
    </p>
      <button class='btn_on' onclick='btnClicked(this)' id='btn'>CHANGE</button>
  </body>
</html>


3.クライアントのJavaScript

 先のコードと共に、以降に続くJavaScriptについて解説します。
①ハンドリング関数の設定

 90, 100, 110の各行で、温度と湿度およびLEDの点灯状態を表示するための関数を記述しています。いずれの関数もすでに前回で説明したように、XMLHttpRequestのインスタンスを作成して、その状態が変化した時に実行するコールバック関数を設定しています。続いてリクエストメソッド"GET"でリクエスト先のURLを指定して、非同期通信である旨をサーバー宛に送信するようになっています。
 これらの関数は、140~142行のsetIntervalによって指定された時間間隔(ミリ秒単位)で実行されて、サーバーにリクエストを送信します。


②サーバーへの送信処理

 前回と同様に、サーバーへの送信は87行目の[CHANGE]ボタンをクリックすることで動作します。クリックすると120行目のbtnClicked()を実行します。続く121行目でわかるように、宛先URLの後に"?value="を記述してスライダーのツマミの値をオプションパラメータとして指定しています。
 このようにすることで、クライアント側のデーターをサーバーに伝達することができ、サーバーはこの値を取り出して処理に使用することが可能になります。


③スライダーの動作

 127~138行ではスライダーの動作を規定しています。この内容は少し込み入っているので細かく見ていきましょう。127行と128行の変数sliderとsliderValueは、それぞれidが'slider'と'sliderValue'をもつオブジェクトを格納するためのものです。
 次に129行目はアロー関数式で、無名関数をsetValueとして定義しています。スライダーの値(ツマミが示す値)slider.valueを取得してsliderValueに設定する、つまりツマミの示す値を右隣のエリアに表示する機能を担っています。
 133行目のwindow.addEventListenerは、イベントが"load"の時、すなわちページ読み込みが完了した時に、一連の関係を関数として設定します。その内容は、変数sliderにidが'slider'のオブジェクトを取り出し、変数slideValueにはidが'slideValue'のオブジェクトを取り出します。そしてsliderに"input"イベントが発生した時、つまりツマミの移動で値が変更された時に、先のsetValueを実行するようコールバック関数として登録します。こうすることによって、ツマミの位置が変化するとこのコールバック関数が実行されて、スライダー右隣の表示が更新されるわけです。そして最後に、setValue関数でスライダーの初期値を表示します。
 このようにして表示が更新され、[CHANGE]ボタンがクリックされればいつでもその値をオプションパラメータに組み込める準備が整っているのです。
/* 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;}
      .slider { width: 200px;}
    </style>
  </head>
  <body>
    <h1>Asynchronous Server</h1>
    <p style='color:brown; font-weight: bold'>計測値は10秒ごとに自動更新されます!</p>
    <p><table>
      <tr><th>ELEMENT</th><th width="100">VALUE</th></tr>
      <tr><td>Temperature</td><td><span id="temperature" class="value">%TEMPERATURE%</span></td></tr>
      <tr><td>Humidity</td><td><span id="humidity" class="value">%HUMIDITY%</span></td></tr>
      <tr><td>LED status</td><td><span id="message" class="value">%MESSAGE%</span></td></tr>
    </table></p>
    <p>
      <input type="range" min="0" max="100" step="10" class="slider", id="slider" valaue="50" />
        <span id="sliderValue"></span>
    </p>
      <button class='btn_on' onclick='btnClicked(this)' id='btn'>CHANGE</button>
  </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);
    }
    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) {
      value = "/update?value=" + slider.value;
      var xhr = new XMLHttpRequest();
      xhr.open("GET", value, true);
      xhr.send();
    }

    slider = null;
    sliderValue = null;
    setValue = ()=> {
      const value = slider.value;
      sliderValue.textContent = value;
    }
    window.addEventListener("load", ()=> {
      slider = document.getElementById("slider");
      sliderValue = document.getElementById("sliderValue");
      slider.addEventListener("input", setValue);
      setValue();
    });

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


4.サーバー側の処理

 サーバー側の処理は、クライアントから"/update"へのリクエストを受けてオブションパラメータを処理する部分以外は、ほとんど前回と同じです。
①コードの冒頭部

 温湿度センサーDHT11に関連した網掛け部分以外は、ほぼ前回と同じです。
#include <Arduino.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <DHT.h>                    // DHTセンサー用

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

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

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

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

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

/* LEDピン */
const int ledPin = 25;              // LED出力接続ピン
String ledState = "";               // 出力ピンの状態
int sliderValue = 0;                // スライダーの値

②定型処理部

 網掛けしているのが前回と異なっている部分です。"/update"に向けてGETリクエストが発行された場合のハンドラーに注目してみましょう。
 167行のrequest->getParam("value")で、クライアントから届いたオプションパラメータvalueの値を取り出しています。結果として、sliderValueには整数型に変換された0~100までの値が設定されます。
 174行目の式は試行の結果から作成したもので、slideValueの値を125~255のアナログ値に変換したものを変数valに格納しています。
 171行と175行では、前回のLEDへのデジタル出力digitalWrite()に代えてdacWrite()で出力し、これによってLEDの明るさを制御しています。ESP32にはArduinoUNOなどで実行できるanalogWrite()のようなアナログ出力のメソッドがありません。そこで、ESP32のDAC(Digital Analog Converter)機能を使って明るさを制御します。DAC出力に使用できるピンはGPIOの25,26ピンに限定されていますが、監視用デバイスのLEDはGPIOの25ピン(DAC1)に接続しているので、そのまま配線を変更することなくDACを使用することができます。
 LEDを消灯する場合はdacWrite()でゼロを出力し、点灯の場合はvalの値を出力して明るさを決めています。
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.on("/message", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send_P(200, "text/plain", getMessage().c_str());
    });
    server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){
        sliderValue = request->getParam("value")->value().toInt();
        String state = ledState + " (" + sliderValue + "%)";	// スライダーの値を取得
        if (ledState == CST_ON) {
            ledState = CST_OFF;
            dacWrite(ledPin, 0);
        } else {
            ledState = CST_ON;
            int val = 125 + 13 * sliderValue / 10;   // スライダー値のDAC変換
            dacWrite(ledPin, val);
        }
        Serial.print("LED changed : ");
        Serial.println(state);
        request->send_P(200, "text/plain", getMessage().c_str());
    });
    // サーバーを開始する
    server.begin();
}
 
void loop(){
}


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

 一連のコールバック関数を記述しています。213行目の湿度の単位は、半角"%"を使うと文字化けするので全角に変更しています。
/*****************************************************************************
 *                          'GET' Request Handlers                           *
 *****************************************************************************/
/* 温度の計測処理 */
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) + " %";			// <--- 全角に変更した!
    }
}

/* LEDの状態を通知 */
String getMessage() {
    return ledState + " (" + sliderValue + "%)";
}

/* プレースホルダー処理 */
String editPlaceHolder(const String& var){
    if(var == "TEMPERATURE"){
        return getTemperature();
    }
    else if(var == "HUMIDITY"){
        return getHumidity();
    }
    else if(var == "MESSAGE"){
        return getMessage();
    }
}


④その他の関数

 doInitialize()でDHTセンサーを起動している以外は前回と同じです。
/*****************************< Other functions >*****************************/
/* 初期化処理 */
void doInitialize() {
    Serial.begin(SPI_SPEED);
    pinMode(ledPin, OUTPUT);           // GPIO設定:LED
    digitalWrite(ledPin, LOW);
    ledState = CST_OFF;
    dht.begin();                       // DHTセンサーを起動
}

/****************************< 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のリセットボタン(RST,RESET,ENなどと印刷された)を押します。シリアルモニターに
    WiFi connected.
     *IP address: 192.168.0.29
のように割り当てられたIPアドレスが表示されるので、PCのWebブラウザを起動してアドレスバーにそのIPアドレスを入力します。


 10秒毎に温度と湿度の値が更新表示されます。スライドバーの初期値は50に設定されていて、[CHANGE]ボタンをクリックするとESP32ボードのLEDが点灯して、LED statusの表示が「ON (50%)」に変わります。
 この状態でスライダーのノブを操作すると右側の値表示が変わります。例えば80にセットして[CHANGE]をクリックするとLEDが消灯し、LED statusが「OFF (80%)」に変わります。再び[CHANGE]をクリックすると上の画面のようにLED statusの内容が更新され、下のようにLEDが点灯します。


 ここで取り上げたのはごく限られた範囲のテーマですが、以上の実験を通して非同期サーバーで双方向通信がどのようにできるかが理解できたのではないでしょうか。
 ところで、今回の実験では点灯/消灯の切り替えをボタン操作で行っていますが、ボタンを取り除いて、スライダーのノブ操作を直ちにサーバー側に送信することも可能です。システムとしては、そのほうが以下の点で優れていると言えるかも知れません。
  ・ボタンが不要になり、操作性が改善される。
  ・ノブの操作が即時にサーバーのLED照度に反映され、応答性能が改善される。
  ・電力消費面のコストが改善される。
 最後の点については少し説明が必要です。スケッチのHTMLのScript記述(142行目)で、サーバー側からのLEDの点灯状態を取得するために200ミリ秒ごとにリクエストを送信しています。これは、ボタン操作の結果が違和感がない程度の遅延で画面表示に反映するよう短く設定しているのですが、頻繁なリクエストの送信には電力消費というコストを伴います。
 LEDの状態をサーバーから受信して表示する方法は、クライアント側で状態を確実に把握する方法としては有効なのですが、そこまでの必要があるかどうかは検討の余地がありそうです。
 今回の実験で使用した計測用デバイスの回路とDAC変換式のもとでは、スライダーをゼロ位置にするとLEDはほぼ消灯状態になります。それにスライダーノブの値が画面上に表示されることから、明暗を制御する目的だけであれば削除しても問題ないように思われます。画面上のLED status表示が不要になれば、200ミリ秒ごとのリクエストと関連処理を削除することができます。コードは簡素化され、サーバーへの呼び出し減少による省エネが期待できます。
 すでにおわかりのように、非同期通信ではリクエストを出してレスポンスが返ってくるまでブラウザはフリーの状態になります。その間はブラウザ上で他の処理を行うなり、何もしないでいるなりで、サーバーからのレスポンスが返ってきたら取得した情報を表示するだけです。開発ではこのような特性を十分活用したいですね。
 なお、ここではオプションパラメータとしてスライダーの値だけを送信しましたが、この方法を拡張して複数のデータセットを送ることもできます。適当なセパレーター記号で複数データを連結して送信し、サーバー側で分離すればいいわけで、そうすることで応用範囲はさらに広がりそうです。


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