09.Basicのデータをスマホに表示(非同期)


09.Basicのデータをスマホに表示(非同期)

前回は、測定値を更新させるには、スマホのブラウザでスワイプしていました。今回は、設定間隔で自動更新させます。 これは、スマホ(クライアント)からのリクエストを待って応答するのではなく、Basic(サーバー)の準備が整ったタイミングで通知する非同期処理が必要です。
http://marchan.e5.valueserver.jp/cabin/comp/jbox/arc214/doc21403.html を元にしています。

スマホ画面(測定タイミングは他の画像と異なります。)

必要なもの

・M5Stack Basic x1
・温湿度&気圧センサー ENV2 x1
・スマホ x1

非同期Webサーバーライブラリ

ESPAsyncWebServer

https://github.com/me-no-dev/ESPAsyncWebServer
ESP8266 and ESP32の非同期Webサーバーライブラリです。ESP8266の場合ESPAsyncTCPが、ESP32の場合AsyncTCPが必要です。

https://github.com/me-no-dev/ESPAsyncWebServer/archive/master.zip
をダウンロード > ディスクトップに置く
Arduino-IDE > スケッチ > ライブラリをインクルード > .ZIP形式のライブラリをインストール... > 先程のファイルを指定 > 開く

AsyncTCP

https://github.com/me-no-dev/AsyncTCP
ESP32の非同期TCPライブラリです。

https://github.com/me-no-dev/AsyncTCP/archive/master.zip
を上記と同様にインストールします。

非同期サーバーについて

JavaScriptで組込むHTTP通信のための組込みオブジェクトの XMLHttpRequest が重要です。これは頁を再表示することなくデータを送受信できます。つまり、ウェブページの特定部分だけを書換えることができます。
htmlのbody部にid属性で"tmp", "hum", "pre"を定義しています。これによって、これらの要素を固有の識別名で操作できるようになります。そして測定値表示部分は、両側を%で挟んだプレースホルダー %TMP%, %HUM%, %PRE% を設定しておきます。

HTML

* スマホに表示させる内容です。スケッチの内容に注釈を追加しました。

<!DOCTYPE HTML>            <!-- HTML5.1 -->
<html lang="ja">           <!-- 日本語 -->
<html>                     <!-- html開始 -->
  <head>                   <!-- 情報開始 -->
    <meta charset="utf-8"> <!-- 文字コード -->
    <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 表示領域 画面幅,ズーム倍率 -->
    <style> /* css開始 */
      html {font-family:Helvetica; display:inline-block; margin:0px auto;text-align:center;} /*フォント名;インラインブロック;枠線からの距離;行揃え*/
      h1 {font-size:28px;}      /* h1の{文字サイズ} */
      body {text-align:center;} /* bodyの{行揃え} */
      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;}       /* tdは... */
      .value {color:blue; font-weight:bold; padding:1px;} /* class=valueは{文字色;太さ;距離;} */
    </style>             <!-- css終了 -->
  </head>                <!-- 情報終了 -->
  <body>                 <!-- 表示開始 -->
    <h1>温湿度・気圧</h1> <!-- 見出し(1-6) 最大 -->
    <p style='color:brown; font-weight:bold'>10秒ごとに自動更新</p> <!-- 文字色;太さ -->
    <table>                                 <!-- 表開始 -->
      <tr><th>項目</th><th>測定値</th></tr> <!-- 表の見出し -->
      <tr><td>温度</td><td><span id="tmp" class="value">%TMP%</span></td></tr> <!-- 表のデータ 温度 -->
      <tr><td>湿度</td><td><span id="hum" class="value">%HUM%</span></td></tr> <!-- 表のデータ 湿度 -->
      <tr><td>気圧</td><td><span id="pre" class="value">%PRE%</span></td></tr> <!-- 表のデータ 気圧 -->
    </table> <!-- 表終了 -->
  </body>    <!-- 表示終了 -->
  <script>   // JavaScript開始
    var getTmp = function () { // 
      var xhr = new XMLHttpRequest(); // ブラウザとWEBサーバ間でデータの送受信を行う際に利用できるオブジェクトを作成
      xhr.onreadystatechange = function() { // onreadystatechangeイベントで処理の状況変化を監視
        if (this.readyState == 4 && this.status == 200) { // 受信完了し、成功したら
          document.getElementById("tmp").innerHTML = this.responseText;
          // サーバーから受け取ったテキストをHTMLタグで指定したidの要素に変更する
        } // Webブラウザー画面の温度だけが更新
      };
      xhr.open("GET", "/tmp", true); // HTTPのGETメソッドとアクセスする場所を指定
      xhr.send(null);                // HTTPリクエストを送信
    }
    var getHum = function () {       // humについても同様
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("hum").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/hum", true);
      xhr.send(null);
    }
    var getPre = function () { // preについても同様
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("pre").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/pre", true);
      xhr.send(null);
    }
    setInterval(getTmp, 10000); // 実行間隔(関数,処理間隔mS)
    setInterval(getHum, 10000);
    setInterval(getPre, 10000);
  </script> <!-- JavaScript終了 -->
</html>     <!-- html終了 -->  

解説

前回と異なる場所

・htmlのbody内はほぼ同じです。
・htmlのJavascriptを追加
・getTmp,getHum,getPre関数を呼ぶと測定し測定値を文字で返します。
・editPlaceHolder関数でhtml内の両側を%に囲まれた指定文字を測定値に変更します。
・onメソッドで検索文字列があった時に呼出す関数を登録します。

プレースホルダー以外の%

webで湿度の単位表示に半角の"%"を使うとプレースホルダーと間違え以後の気圧表示をしなくなるので、全角の"%"を使用します。

onメソッド

検索文字列があった時に呼出す関数を登録(パスを含む文字列,メソッド,関数)

GETメソッド

WebブラウザからURLで指定したファイル送信を要求

send_Pメソッド

クライアントに応答を返します。
send_Pメソッド(HTTPステータス成功,text/html,処理するHTML文字列, 処理関数 ここでは文字変換関数)
send_P(200, "text/plain", getTmp().c_str())で
200は、HTTP応答コードOK
text/htmlは、htmlファイル
text/plainは、テキストファイル
.c_str()は、文字配列表現を取得

動作

実行を開始して、wifi接続後にシリアルモニターにIPアドレスが表示されます。(例 192.168.68.67)。
スマホ(or PC)のアドレスバーにIPアドレス(192.168.68.67)を入力すると測定値が表示されます。この値は設定時間(10秒)ごとに更新され、シリアルモニターにも表示されます。
各測定値はばらばらに10秒ごとなので、表示タイミングによりいつも同じ順番に表示はされません。
スケッチ作成中で変更時の一番最初は、ブラウザの再読込みをクリックします。

Basic画面(測定タイミングは他の画像と異なります)

スケッチ

"**********"の2か所は自分のssidとそのパスワードを記入してください。

// WiFiMeasureAsync.ino   Basic用
// Basicを非同期Webサーバーにして、シリアルモニタに表示されたIPアドレスが 192.168.68.67 なら
// スマホ(クライアント)で http://192.168.68.67 を開くと温湿度&気圧を表示し、自動更新します。
// http://marchan.e5.valueserver.jp/cabin/comp/jbox/arc214/doc21403.html を修正
#include <M5Stack.h>                           // M5Stack Basicを使用
#include <WiFi.h>                              // wifiを使用
#include <ESPAsyncWebServer.h>                 // 非同期http
AsyncWebServer server(80);                     // デフォルトのhttpポートを使用
const char *ssid = "**********";      // 自分のネットワークのssid
const char *password = "**********";         // そのパスワード
#define LGFX_AUTODETECT                        // LovyanGFX自動認識
#define LGFX_USE_V1                            // LovyanGFX Ver1を使用
#include <LGFX_AUTODETECT.hpp>                 // クラス"LGFX"を用意
static LGFX lcd;                               // LGFXのインスタンスを作成
#include <Wire.h>                              // I2Cを使用
#include <Adafruit_SHT31.h>                    // 温湿度センサを使用
#include <Adafruit_BMP280.h>                   // 気圧センサを使用
Adafruit_SHT31 sht = Adafruit_SHT31(&Wire);    // sht定義
Adafruit_BMP280 bme = Adafruit_BMP280(&Wire);  // bmp定義だがbmeとする

const char *strHtml = R"rawliteral(
<!DOCTYPE HTML>
<html lang="ja">
<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>温湿度・気圧</h1>
    <p style='color:brown; font-weight:bold'>10秒ごとに自動更新</p>
    <table>
      <tr><th>項目</th><th>測定値</th></tr>
      <tr><td>温度</td><td><span id="tmp" class="value">%TMP%</span></td></tr>
      <tr><td>湿度</td><td><span id="hum" class="value">%HUM%</span></td></tr>
      <tr><td>気圧</td><td><span id="pre" class="value">%PRE%</span></td></tr>
    </table>
  </body>
  <script>
    var getTmp = function () {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("tmp").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/tmp", true);
      xhr.send(null);
    }
    var getHum = function () {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("hum").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/hum", true);
      xhr.send(null);
    }
     var getPre = function () {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          document.getElementById("pre").innerHTML = this.responseText;
        }
      };
      xhr.open("GET", "/pre", true);
      xhr.send(null);
    }
    setInterval(getTmp, 10000);
    setInterval(getHum, 10000);
    setInterval(getPre, 10000);
  </script>
</html>)rawliteral";  // スマホ表示用html

String getTmp() {                          // 温度を測定し単位付で戻る
  char strt[9];                            // 文字データ定義
  float t = sht.readTemperature() + 0.05;  // センサーで温度を読取る
  sprintf(strt, "%5.1f℃", t);              // 変数→文字列(文字列のポインタ,出力書式,変数)
  lcd.setTextColor(BLUE, WHITE);           // (文字色{,背景色})
  lcd.setCursor(36 * 4, 36 * 3);           // 表示位置(x,y)
  lcd.print(strt);                         // 表示 温度 Basicに
  Serial.printf("%s  ", strt);             // 表示 シリアルモニタに
  return strt;                             // 単位を付けて戻る
}

String getHum() {                                             // 湿度を測定し単位付で戻る
  String h = String((int)(sht.readHumidity() + 0.5)) + "%";  // センサーで湿度を読取る
  lcd.setTextColor(BLUE, WHITE);                              // 上と同様
  lcd.setCursor(36 * 5, 36 * 4);
  lcd.print(h);
  Serial.printf("        %s  \n", h);
  return h;
}

String getPre() {                                                      // 気圧を測定し単位付で戻る
  String p = String((int)(bme.readPressure() / 100.0 + 0.5)) + "hPa";  // センサーで気圧を読取る
  lcd.setTextColor(BLUE, WHITE);                                       // 上と同様
  lcd.setCursor(36 * 4, 36 * 5);
  lcd.print(p);
  Serial.printf("             %s \n", p);
  return p;
}

String editPlaceHolder(const String &var) {  // 文字変換関数
  if (var == "TMP") {                        // (html内の)"TMP"なら
    return getTmp();                         // 温度測定へ
  } else if (var == "HUM") {                 // "HUM"なら
    return getHum();                         // 湿度測定へ
  } else if (var == "PRE") {                 // "PRE"なら
    return getPre();                         // 気圧測定へ
  }                                          //
  return "??";                               // それ以外は"??"で戻る
}

void LCDset() {                             // 初期画面設定関数
  lcd.init();                               // 画面初期化
  lcd.setRotation(1);                       // 回転方向(0-3)
  lcd.setBrightness(20);                    // バックライト輝度(0-255)
  lcd.setColorDepth(24);                    // RGB888 24bit (16=RGB565 16bit)
  lcd.setCursor(0, 0);                      // 表示位置(x,y)
  lcd.setFont(&fonts::lgfxJapanGothic_36);  // ゴシック(8,12,16,20,24,28,32,36,40)
  lcd.fillScreen(WHITE);                    // 画面クリア(色)
  lcd.setTextColor(BLACK, WHITE);           // 色設定(文字色[,背景色])
}

void LCDwaku() {                                           // 測定値以外の表示 Basic画面
  lcd.fillScreen(WHITE);                                   // 画面クリア
  lcd.setCursor(0, 0);                                     // 表示位置(x,y)
  lcd.setTextColor(BLACK, WHITE);                          // (文字色{,背景色})
  lcd.println("  温湿度・気圧");                           // 表示
  lcd.setTextColor(MAROON, WHITE);                         // (文字色{,背景色})
  lcd.setCursor(36 * 1 + 30, 36 * 1 + 8);                  // 表示位置(x,y)
  lcd.setTextSize(0.556, 0.556);                           // 文字倍率 36*0.556=20
  lcd.println("10秒ごとに自動更新");                       // 表示
  lcd.setTextSize(1, 1);                                   // 文字倍率を戻す
  lcd.setCursor(0, 36 * 2);                                // 表示位置(x,y)
  lcd.print(" ");                                         // 表示
  lcd.setTextColor(WHITE, BLUE);                           // (文字色{,背景色})
  lcd.print("項目");                                       // 表示
  lcd.setTextColor(WHITE, WHITE);                          // (文字色{,背景色})
  lcd.print("  ");                                        // 表示
  lcd.setTextColor(WHITE, BLUE);                           // (文字色{,背景色})
  lcd.println("測定値");                                   // 表示
  lcd.setTextColor(DARKGREY, WHITE);                       // (文字色{,背景色})
  lcd.println(" 温度");                                   // 表示 温度
  lcd.setTextColor(DARKGREY, WHITE);                       // (文字色{,背景色})
  lcd.println(" 湿度");                                   // 表示 湿度
  lcd.setTextColor(DARKGREY, WHITE);                       // (文字色{,背景色})
  lcd.print(" 気圧");                                     // 表示 気圧
  for (int i = 2; i < 7; i++) {                            // 5本
    lcd.drawFastHLine(18, i * 36, 36 * 7 + 18, DARKGREY);  // 水平線(x,y,w,color)
  }
  lcd.drawFastVLine(18, 36 * 2, 36 * 4, DARKGREY);           // 垂直線(x,y,h,color)
  lcd.drawFastVLine(36 * 3 + 18, 36 * 2, 36 * 4, DARKGREY);  // 垂直線 中
  lcd.drawFastVLine(36 * 8, 36 * 2, 36 * 4, DARKGREY);       // 垂直線 右
}

void setup() {
  M5.begin();                                                    // M5stack初期化
  Serial.begin(115200);                                          // シリアルモニタ通信速度設定
  LCDset();                                                      // 初期画面設定関数へ
  LCDwaku();                                                     // Basicの測定値以外表示関数へ
  while (!bme.begin(0x76)) {                                     // BMP280のアドレスで開始できない時
    M5.lcd.println("エラー BMP280未接続");                       // 表示
  }                                                              //
  while (!sht.begin(0x44)) {                                     // shtのアドレスで開始できない時
    M5.lcd.println("エラー SHT3X未接続");                        // 表示
  }                                                              //
  WiFi.begin(ssid, password);                                    // ネットワーク設定を初期化
  while (WiFi.status() != WL_CONNECTED) {                        // 接続されなかったら
    delay(1000);                                                 // 1秒待つ (接続されるまで待つ)
    Serial.println("wifiステーションとして設定中");              // 表示
  }                                                              //
  Serial.print("IPアドレス:");                                   // 表示
  Serial.println(WiFi.localIP());                                // IPアドレスを取得し表示
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {  // "/"へアクセス時の関数
    request->send_P(200, "text/html", strHtml, editPlaceHolder);
  });
  // send_Pメソッド(HTTPステータス成功,html,処理するHTML文字列,処理 ここでは文字変換関数)
  server.on("/tmp", HTTP_GET, [](AsyncWebServerRequest *request) {  // tmpがあったら呼出す関数
    request->send_P(200, "text/plain", getTmp().c_str());           // getTmp()の文字を返す
  });
  server.on("/hum", HTTP_GET, [](AsyncWebServerRequest *request) {  // humがあったら呼出す関数
    request->send_P(200, "text/plain", getHum().c_str());           // getHum()の文字を返す
  });
  server.on("/pre", HTTP_GET, [](AsyncWebServerRequest *request) {  // preがあったら呼出す関数
    request->send_P(200, "text/plain", getPre().c_str());           // getPre()の文字を返す
  });
  server.begin();  // WiFiサーバを開始
  getTmp();        // 温度を測定しBasicの画面に表示
  getHum();        // 湿度を測定しBasicの画面に表示
  getPre();        // 気圧を測定しBasicの画面に表示
}

void loop() {}
* flash memory(2Mbyte)のうち、スケッチが69%使用。RAM(327kbyte)のうち、global変数が14%使用、local変数で281kbyte使用可能。(1000byte=1kbyteで計算)