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


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

Basicで測定した温湿度&気圧をスマホに表示します。BasicをWebサーバーにして、同期サーバーにより表示します。表示の更新はスマホをスワイプします。次回は非同期の予定です。
http://marchan.e5.valueserver.jp/cabin/comp/jbox/arc214/doc21402.htmlを修正しています。

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

生文字列リテラル

C++11以前では、
char* kJsonData = "{\"data\": \"Hello World\"}";
の表記とすれば、C++11にある生文字列リテラルを使用すると
char* kJsonData = R"({"data": "Hello World"})";
と書けます。これは、R"(*****)" で表され、*****の中では " や \ などの文字をエスケープなしで用いることができます。改行文字等の特殊文字も書いた通りに解釈してくれます。ここではこれを使用します。
例) char* kJsonData = R"({
  "data": "Hello World"
})";
は、char* kJsonData = "{\n  \"data\": \"Hello World\"\n}" と同じです。
上記の*****の中に )"は入れられませんが、その時は終端部分に追加文字列を設定します。
例) char* kTestData = R"TEST("("+")")TEST";
これは、"("+")" と同じです。

HTML

HTTPヘッダー部

"HTTP/1.1 200 OK\r\n     HTTPバージョン1.1 正常応答
Content-Type:text/html\r\n" メディアタイプ:HTML文書
"Connection:close\r\n\r\n"; コネクションを切断

htmlヘッダー部・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>%PAGE_TITLE%</h1> <!-- 見出し(1-6) 最大 -->
    <p style="color:brown; font-weight:bold">測定値の更新は[再読込み]をクリック</p> <!-- 文字色;太さ -->
    <table>                                                                   <!-- 表開始 -->
      <tr><th>項目</th><th>測定値</th></tr>                                   <!-- 表の見出し -->
      <tr><td>温度</td><td><span class="value">%TEMPERATURE%</span></td></tr> <!-- 表のデータ 温度 -->
      <tr><td>湿度</td><td><span class="value">%HUMIDITY%</span></td></tr>    <!-- 表のデータ 湿度 -->
      <tr><td>気圧</td><td><span class="value">%PRE%</span></td></tr>         <!-- 表のデータ 気圧 -->
    </table>                                                                  <!-- 表終了 -->
  </body>                                                                     <!-- 表示終了 -->
</html>                                                                       <!-- html終了 -->

スケッチの解説

実行開始してwifi接続後にシリアルモニターにIPアドレスが表示されます。(例 192.168.68.67)
スマホ(or PC)のWebブラウザーのアドレスバーに http://192.168.68.67 を入力すると測定値が表示されます。表示後はクライアントとの接続が切断されます。
ブラウザからの接続によって、一回だけ測定値を表示して動作を完結します。表示を更新する時は、スワイプ(PCではブラウザーの更新ボタンをクリック)することで再度リクエストを送信します。

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

loop内にあるstrBufferは受信した文字列ですが、ここでは利用していません。currentLineは、1行文の受信文を格納するラインバッファで終端を検出しています。

loop最初で受信するまで待ちます。
指定したIPアドレスの再読込みをすると色々送られてきます(下記シリアルモニタ画面参照)が、スマホから"\r\n"の空白行が届くことでリクエストの終端を検出しています。

シリアルモニタ画面


新規クライアント GET /favicon.ico HTTP/1.1
Host: 192.168.68.67
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://192.168.68.67/
Accept-Encoding: gzip, deflate
Accept-Language: ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7

 19.3℃   47%  1018hPa クライアントを切断

終端を検出後、温湿度&気圧を読取りhtmlを送信します。
"\r\n"の検出は、'\r'以外の文字はラインバッファに繋いでいます。'\n'を受信するたびにラインバッファをクリアしているので、読んだ文字が'\n'でラインバッファが空なら、クライアントからのリクエストが終了です。
最終的に受信した文のすべてが収納されているstrBufferをもとにHTTPリクエスト処理のhttpRequestProccess(String*)で受信データの解析と処理を行えますが、今回はスマホからのデータを受信して処理はないので、HTTPリクエスト処理はありません。

スマホでの表示動作

1.センサー値を読取る
2.httpヘッダー部を送信
3.htmlヘッダー部を送信
4.htmlのボディー部のデータ値を置換して送信
 変更部分は両側を%で挟んだプレースホルダーとして設定しておきます。テンプレートを作業領域に複写した後、プレースホルダーを対応するデータ値に変換します。
5.終端の空行を送信
です。

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

スケッチ

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

// WiFiMeasurement.ino  Basic用
// http://marchan.e5.valueserver.jp/cabin/comp/jbox/arc214/doc21402.html を修正
// BasicをWebサーバーにして、シリアルモニタに表示されたIPアドレスが 192.168.68.67 なら
// スマホ(クライアント)で http://192.168.68.67 を開くと温湿度&気圧を表示します。
// 更新はブラウザの更新です。PCでも表示します。
#include <M5Stack.h>                           // M5Stack Basicを使用
#include<WiFi.h>                              // wifiを使用
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とする
WiFiServer server(80);                         // デフォルトのhttpポートを使用
String strTmp, strHum, strPre;                 // 単位付測定値

const String strResponseHeader = "HTTP/1.1 200 OK\r\nContent-Type:text/html\r\n"
                                 "Connection:close\r\n\r\n";  // httpヘッダー

const String strHtmlHeader = 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>
)rawliteral";  // htmlページのヘッダー

const String strHtmlBody = R"rawliteral(
 <body>
   <h1>%PAGE_TITLE%</h1>
   <p style="color:brown; font-weight:bold">更新は[再読込]をクリック</p>
   <table>
     <tr><th>項目</th><th>測定値</th></tr>
     <tr><td>温度</td><td><span class="value">%TEP%</span></td></tr>
     <tr><td>湿度</td><td><span class="value">%HUM%</span></td></tr>
     <tr><td>気圧</td><td><span class="value">%PRE%</span></td></tr>
   </table>
  </body></html>
)rawliteral";  // htmlページのボディー

void sokutei() {                                 // センサー読取りとBasic画面表示関数
  float tmp = sht.readTemperature() + 0.5;       // センサー温度を読取る
  float hum = sht.readHumidity() + 0.5;          // センサー湿度を読取る
  float pre = bme.readPressure() / 100.0 + 0.5;  // センサー気圧を読取る
  if (isnan(hum) || isnan(tmp) || isnan(pre)) {  // 測定値が数値でなければ
    Serial.println("失敗 センサー読込み");       // 表示
    return;
  }
  char tmpT[7], humT[7], preT[7];                          // 文字データ定義
  dtostrf(tmp, 5, 1, tmpT);                                // double→文字列(数値,文字列長,小数点以下の桁数,文字列)
  dtostrf(hum, 3, 0, humT);                                // 湿度データの文字列変換
  dtostrf(pre, 4, 0, preT);                                // 気圧データの文字列変換
  strTmp = String(tmpT) + "℃ ";                            // 単位も結合
  strHum = String(humT) + "% ";                           // 単位も結合
  strPre = String(preT) + "hPa";                           // 単位も結合
  Serial.printf("%s %s %s", strTmp, strHum, strPre);       // 表示
  lcd.fillScreen(WHITE);                                   // 画面クリア
  lcd.setCursor(0, 0);                                     // カーソル指定
  lcd.setTextColor(BLACK, WHITE);                          // (文字色{,背景色})
  lcd.println("  温湿度・気圧\n");                         // 表示
  lcd.setTextColor(WHITE, WHITE);                          // (文字色{,背景色})
  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.print(" 温度 ");                                   // 表示 温度
  lcd.setTextColor(BLUE, WHITE);                           // (文字色{,背景色})
  lcd.println(strTmp);                                     // 表示 温度
  lcd.setTextColor(DARKGREY, WHITE);                       // (文字色{,背景色})
  lcd.print(" 湿度  ");                                 // 表示 湿度
  lcd.setTextColor(BLUE, WHITE);                           // (文字色{,背景色})
  lcd.println(strHum);                                     // 表示 湿度
  lcd.setTextColor(DARKGREY, WHITE);                       // (文字色{,背景色})
  lcd.print(" 気圧 ");                                   // 表示 気圧
  lcd.setTextColor(BLUE, WHITE);                           // (文字色{,背景色})
  lcd.println(strPre);                                     // 表示 気圧
  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 httpSendResponse(WiFiClient *client) {    // http送信関数
  sokutei();                                   // センサー読取りとBasic画面表示関数へ
  client->println(strResponseHeader);          // httpヘッダ部を送信 アロー演算子
  client->println(strHtmlHeader);              // ヘッダー部を送信
  String buf = strHtmlBody;                    // ボディー部をbufに代入
  buf.replace("%PAGE_TITLE%", "温湿度・気圧");  // bufの文字列置換1
  buf.replace("%TEP%", strTmp);                // bufの文字列置換2
  buf.replace("%HUM%", strHum);                // bufの文字列置換3
  buf.replace("%PRE%", strPre);                // bufの文字列置換4
  client->println(buf);                        // bufを送信
  client->println();                           // http終端の空行を送信
}

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.setTextColor(GREEN, BLACK);           // 色設定(文字色[,背景色])
  lcd.setTextColor(BLACK, WHITE);  // 色設定(文字色[,背景色])
}

void setup() {
  M5.begin();                               // M5stack初期化
  Serial.begin(115200);                     // シリアルモニタ通信速度設定
  LCDset();                                 // 初期画面設定関数へ
  while (!bme.begin(0x76)) {                // BMP280のアドレスで開始できない時
    M5.lcd.println("エラー BMP280未接続");  // 表示
  }
  while (!sht.begin(0x44)) {               // shtのアドレスで開始できない時
    M5.lcd.println("エラー SHT3X未接続");  // 表示
  }
  WiFi.mode(WIFI_AP_STA);                            // AP(アクセスポイント)+STA(ステーション)モード
  WiFi.begin(ssid, password);                        // ネットワーク設定を初期化
  while (WiFi.status() != WL_CONNECTED) {            // 接続されなかったら
    delay(1000);                                     // 1秒待つ (接続されるまで待つ)
    Serial.println("wifiステーションとして設定中");  // 表示
  }
  Serial.print("IPアドレス: ");      // 表示
  Serial.println(WiFi.localIP());    // IPアドレスを取得し表示
  Serial.print("wifiチャンネル: ");  // 表示
  Serial.println(WiFi.channel());    // wifiチャネルを取得し表示
  server.begin();                    // WiFiサーバを開始
  sokutei();                         // センサー読取りとBasic画面表示関数へ
}

void loop() {
  String strBuffer = "";                    // 受信文字列をクリア
  WiFiClient client = server.available();   // wifiクライアントからの接続を取得
  if (client) {                             // クライアントから着信があれば(ずっと待っている)
    Serial.print(" 新規クライアント ");     // 表示
    String currentLine = "";                // ラインバッファをクリア
    while (client.connected()) {            // 接続している間は以下の処理
      if (client.available()) {             // 利用可能なバイト数があれば
        char c = client.read();             // 受信データを1バイト読む
        Serial.print(c);                    //表示
        strBuffer += c;                     // 受信文字列に結合
        if (c == '\n') {                    // もし改行なら
          if (currentLine.length() == 0) {  // (改行で)さらにラインバッファが空なら
            httpSendResponse(&client);      // 上記のHTTPレスポンス関数へ ここのみから
            break;                          // whileループから抜ける
          } else {                          // それ以外(ラインバッファが空でなく改行の時)なら
            currentLine = "";               // ラインバッファをクリア
          }
        } else if (c != '\r') {  // (改行と)復帰以外なら
          currentLine += c;      //ラインバッファに結合する
        }
      }
    }
    client.stop();                          // (接続かなくなったら)TCP接続を切断
    Serial.println(" クライアントを切断");  // 表示
  }
}
* flash memory(2.0Mbyte)のうち、スケッチが65%使用。RAM(327kbyte)のうち、global変数が12%使用、local変数で287kbyte使用可能。(1000byte=1kbyteで計算)