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/ESPAsyncWebServerESP8266 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/AsyncTCPESP32の非同期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で計算)