05.ATOM Liteの測定データをweb表示


05.ATOM Liteの測定データをweb表示

ATOM Liteで測定した温湿度,気圧データを、同じwifiに接続しているPCのhttp://ATOM_Lite.local/env に表示します。起動時にwifiからNTP時刻を取得するので、No.,時刻,温湿度,気圧が表示されます。

必要ハード

・ATOM Lite
・環境センサENV2 (温湿度&気圧)

動作

・シリアルモニタには1分ごとの測定データを表示しますが、HTMLには60データの平均値を計算して1Hごとに表示します。(1日24データ、7日で168データ表示)
テスト時は、上記では時間がかかるので、5秒測定、PCに1分送信しました。
・PCで http://ATOM_Lite.local/env を開き、再読込みをすると数秒たってから画面が更新されます。
・ATOMの電源がoffになるとwebで 「このサイトにアクセスできません 応答時間が長すぎます。」 と表示されます。

プログラム

・本ブログの "05.Basicのデータをweb表示" を参考にしています。
・ATOMの電源起動時に1度だけwifiから時刻を取得します。
・シリアルモニタには測定した1データを追加で表示しますが、HTMLでは全データを作り直して再送信します。
・webに表示する測定計算データは999H(=41.6日)までとします。開始から1Hごとに平均値を計算したデータが保存してあります。999まで使えるか未確認です。
・WEBサーバとは、クライアントの指示に応じて静的画面などをWebブラウザーに送り返してくれるサーバーの事です。
・webServerオブジェクトを作り、on関数で指定したURLにアクセスされた時の処理を設定します。
・loop関数内にて、handleClient関数を実行してon関数で指定したURLにブラウザからのアクセスがあると、登録した処理関数が呼ばれます。ルートにアクセスされた場合と "/env" にアクセスされた場合とファイルが見つからない場合の処理をします。5秒ごとに1回処理しています。
・温度は整数配列で、小数点以下第1位まで表示させるためデータを10倍して四捨五入して整数で保存し、表示時に10で割っています。

PC側に送るhtml例


<!DOCTYPE html>   <!-- HTML5を使用 -->
<html lang="ja">  <!-- 日本語      -->
  <head>          <!-- 文書情報    -->
    <meta charset=\"utf-8\">  <!-- 文字コード           -->
    <title>ATOM Lite測定</title>  <!-- 頁タイトル(上のタブ) -->
  </head>
  <body style="line-height:0.1";>    <!-- 行間を狭くする -->
    <h3>ATOM Lite測定データ</h3>   <!-- 見出し(1-6) -->
    <p>No.____年/月/日,__時:分:秒,__℃,\%RH,_hPa</p>  <!-- 段落 --> 
    <p>000.2021/08/29, 16:36:54, 27.9, 58, 1013</p>  <!-- 例 -->
    <p>001.2021/08/29, 16:37:00, 27.9, 58, 1013</p>  <!-- 例 -->
    <p>002.2021/08/29, 16:38:00, 27.9, 58, 1013</p>  <!-- 例 -->
    <!-- No.,時刻,温湿度,気圧を埋込んだ文字列。ここに追加していきます -->
  </body>   <!-- 表示内容 -->
</html>
PC画面のデータを選択して、メモ帳に張り付け、.csvで保存して、エクセルから開くと各データがセルに入っています。そこから手動でグラフも作れます。

スケッチ

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

// ATOM Liteの測定データとwifiからNTP時刻を取得。同wifiに接続しているPCの
// http://atom_Lite.local/env を再読込みすると5秒程度以内に No.,時刻,温湿度,気圧を表示。
// ATOMがoffになると "このサイトにアクセスできません" とweb表示されます。
// buf,buf2が23kだとエラーが発生
#include <M5Atom.h>    // ATOM Lightを使用
#include <WiFi.h>      // wifiを使用
#include <WebServer.h> // ATOMが送信側
#include <ESPmDNS.h>   // 名前(ATOM_Lite.local)をIPアドレスに変換
#include <Adafruit_SHT31.h>  //温湿度センサ(0-60)±0.2℃,(10-90)±2%
#include <Adafruit_BMP280.h> // 気圧センサを使用(300-1100)±1hPa
const char* ssid = "**********";  // 自分のssid
const char* password = "**********";     // そのパスワード
WebServer server(80); //WebServerオブジェクトを作る,80はHTTP通信の一般的ポート
Adafruit_SHT31 sht3x = Adafruit_SHT31(&Wire);  // 定義
Adafruit_BMP280 bme = Adafruit_BMP280(&Wire);  // 定義
char buf[22000], buf2[22000]; // html文字列 データ,全体(NG 23k)
int count = 0, count2 = 0; // データ数小カウント,大カウント
const int n = 1000;        // データ数max 0-999としました。
int sokutei[n][9];         // 0測定年,1月,2日,3時,4分,5秒,6温度,7湿度,8気圧
int soku[65][9];           // 平均化するデータ温度,湿度,気圧
int maeT = 99;             // 前回のsokutei[][]時刻の値

void handleRoot() {        // "/"(ルート)にアクセスされた時の処理関数
  String message = "/envへ移動してください。\n"; // 表示文字
  server.send(200, "text/plain", message);   //(ok,ファイルの分類/種類,web表示文字)
  Serial.println("ルートにアクセスされました");   // シリアルモニタに表示
}

void handleEnv() {                  // "/env"にアクセスされた時の処理関数
  memset(buf2, '\0', sizeof(buf2)); // 配列全体をヌル文字でクリア(メモリのポンイタ,セット値,セットサイズ)
  int i = 0;                        // 最初のデータから
  while (i <= count2 - 1) {         // データの最初から最後-1まで連結 最後は集計中
    sprintf(buf, "<p>%03d,%4d/%02d/%02d,%02d:%02d:%02d,%4.1f,%3d,%4d</p>",
            i, sokutei[i][0], sokutei[i][1], sokutei[i][2], sokutei[i][3],
            sokutei[i][4], sokutei[i][5], (float)sokutei[i][6] / 10.0, sokutei[i][7],
            sokutei[i][8]);//HTML用にNo.,年月日時分秒,温湿度,気圧を埋込んだ文字列
    strcat(buf2, buf);     // 今までのbuf2に今回のbufを結合
    i++;                   // 次のデータNo.
  }
  sprintf(buf, "%s%s%s",
          "<!DOCTYPE html><html lang=\"ja\">"
          "<head><meta charset=\"utf-8\"><title>ATOM Lite測定</title></head>"
          "<body style=\"line-height:0.1\";>"
          "<h3>ATOM Lite測定データ</h3>"
          "<p>No.____年/月/日,__時:分:秒,__℃,\%RH,_hPa</p>",
          buf2,
          "</body>"
          "</html>");
  //Serial.println(buf);
  server.send(200, "text/html", buf);    // ブラウザに送る
}

void handleNotFound() {                  // ファイルが見つからない時の関数
  String message = "File Not Found\n\n"; // 表示文字
  server.send(404, "text/plain", message);
  //         (見つからない,ファイルの分類/種類,web表示文字)
  Serial.println("ファイルが見つかりません"); // シリアルモニタに表示
}

void setup() {
  M5.begin(true, false, true);      // 初期化(Serial,I2C?,Display)
  Wire.begin(26, 32);               // I2CでG26とG32を使用(センサー)
  Serial.println();                 // シリアルモニタ 改行
  while (!bme.begin(0x76))Serial.println("エラー BMP280");//気圧センサー未接続
  while (!sht3x.begin(0x44)) Serial.println("エラー SHT3X");//温湿度センサー未接続
  WiFi.begin(ssid, password);       // wifi接続開始(Webサーバを起動)
  Serial.print("wifi接続開始");       // シリアルモニタに表示
  while (WiFi.status() != WL_CONNECTED) { // wifi接続完待ち
    delay(500);                     // 0.5秒待つ
    Serial.print(".");              // シリアルモニタに表示
  }
  Serial.print("\n接続中 ");         // シリアルモニタに表示
  Serial.print("IPアドレス ");        // シリアルモニタに表示
  Serial.println(WiFi.localIP());   // IPアドレス表示
  configTime(3600L * 9, 0, "ntp.nict.jp",
             "time.google.com", "ntp.jst.mfeed.ad.jp");
  // 時刻の同期(GMTとローカル時刻との差(秒),夏時間で進める秒,NTPサーバ)
  if (MDNS.begin("atom_lite")) {    // マルチキャストDNSにホスト名ATOM_Liteを
    // 登録成功で http://atom_lite.local でこのWebサーバにアクセスできる
    Serial.println("MDNSレスポンダー開始");  // シリアルモニタに表示
  }
  server.on("/", handleRoot);    // ルートアクセス時に実行する関数へ ここのみ使用
  server.on("/env", handleEnv);  // envフォルダアクセス時に実行する関数へ ここのみ
  server.onNotFound(handleNotFound); // 見つからない時に実行する関数へ ここのみ
  server.begin();                    // Webサーバ開始
  Serial.println("HTTPサーバー起動");  // シリアルモニタに表示
  for (int j = 0; j <= 8; j++) {     // j=0-8 (1測定データ)
    for (int i = 0; i <= 999; i++) { // i=0-999 (測定データ数)
      sokutei[i][j] = 0;             // PC表示用平均計算データクリア
    }
    for (int i = 0; i <= 65; i++) {  // i=0-65 (測定データ数)
      soku[i][j] = 0;                // シリアルモニタ表示用測定データクリア
    }
  }
}

void loop() {
  struct tm tm;                             // 時計用
  if (getLocalTime(&tm)) {                  // 現在時刻を取得
    soku[count][0] = tm.tm_year + 1900;     // 年
    soku[count][1] = tm.tm_mon + 1;         // 月
    soku[count][2] = tm.tm_mday;            // 日
    soku[count][3] = tm.tm_hour;            // 時
    soku[count][4] = tm.tm_min;             // 分
    soku[count][5] = tm.tm_sec;             // 秒
    soku[count][6] = (int)(sht3x.readTemperature() * 10.0 + 0.5);// センサーの温度x10を読取る
    soku[count][7] = (int)(sht3x.readHumidity() + 0.5);          // センサーの湿度を読取る
    soku[count][8] = (int)(bme.readPressure() / 100.0 + 0.5);    // センサーの圧力を読取る
    if (maeT == soku[count][3]) {         // 前と時が同じなら温湿度気圧を集計する
      //if (maeT == soku[count][4]) {           // 前と分が同じなら test
      Serial.printf("%03d %02d %02d/%02d"" %02d:%02d:%02d"" %4.1f℃"" %3d%%""  %4dhPa\n",
                    count2, count, soku[count][1], soku[count][2], soku[count][3],
                    soku[count][4], soku[count][5], (float)soku[count][6] / 10.0,
                    soku[count][7], soku[count][8]);//大count,小count,月日時分秒,温湿度,気圧表示
      sokutei[count2][0] = soku[count][0];  // 年
      sokutei[count2][1] = soku[count][1];  // 月
      sokutei[count2][2] = soku[count][2];  // 日
      sokutei[count2][3] = soku[count][3];  // 時
      sokutei[count2][4] = soku[count][4];  // 分
      sokutei[count2][5] = soku[count][5];  // 秒
      sokutei[count2][6] = sokutei[count2][6] + soku[count][6]; // 温度の合計
      sokutei[count2][7] = sokutei[count2][7] + soku[count][7]; // 湿度の合計
      sokutei[count2][8] = sokutei[count2][8] + soku[count][8]; // 気圧の合計
      count++;                              // 小カウント1up
    } else {  // 次の時間が来たら 平均を計算し データを0に格納し 他の集計配列をクリアする
      //maeT = soku[count][4];              // 今の分を前の値とする test
      maeT = soku[count][3];                // 今の時を前の値とする
      if (count > 0) {
        sokutei[count2][6] = ((sokutei[count2][6]) / count + 0.5);         // 温度の平均値
        sokutei[count2][7] = int(float(sokutei[count2][7]) / count + 0.5); // 湿度の平均値
        sokutei[count2][8] = int(float(sokutei[count2][8]) / count + 0.5); // 気圧の平均値
        Serial.printf("%03d,,,,%02d/%02d,%02d:%02d:%02d,%4.1f℃,%3d%%, %4dhPa -PC-\n",
                      count2, sokutei[count2][1], sokutei[count2][2], sokutei[count2][3],
                      sokutei[count2][4], sokutei[count2][5],
                      (float)sokutei[count2][6] / 10.0, sokutei[count2][7],
                      sokutei[count2][8]);    // 大カウント,小カウント,月日時分秒,温湿度,気圧表示
        count2++;                             // 大カウント1up
        sokutei[count2][0] = soku[count][0];  // 年
        sokutei[count2][1] = soku[count][1];  // 月
        sokutei[count2][2] = soku[count][2];  // 日
        sokutei[count2][3] = soku[count][3];  // 時
        sokutei[count2][4] = soku[count][4];  // 分
        sokutei[count2][5] = soku[count][5];  // 秒
        sokutei[count2][6] = soku[count][6];  // 温度
        sokutei[count2][7] = soku[count][7];  // 湿度
        sokutei[count2][8] = soku[count][8];  // 気圧
        count = 0;                            // 読んだデータは0の時のデータとする
        Serial.printf("%03d %02d %02d/%02d %02d:%02d:%02d %4.1f℃ %3d%%  %4dhPa\n",
                      count2, count, sokutei[count2][1], sokutei[count2][2],
                      sokutei[count2][3], sokutei[count2][4], sokutei[count2][5],
                      (float)sokutei[count2][6] / 10.0, sokutei[count2][7],
                      sokutei[count2][8]); // 大カウント,小カウント,月日時分秒,温湿度,気圧表示
        count = 1;                         // 小カウントを次に
      }
      for (int i = 1; i < 65; i++) {    // i=1-64 (測定データ数) 0は上を使用
        for (int j = 0; j <= 8; j++) {  // j=0-8 (測定項目数)
          soku[i][j] = 0;               // 測定データクリア
        }
      }
      server.handleClient();  // クライアントからのアクセスの処理(ブラウザからのリクエストを処理)
    }
  }
  for (int t = 0; t < 12; t++) { // 5秒→t<1, 1分→t<12
    delay(5000);                 // 5秒ごとに送信するが最後のcount2は集計中
    server.handleClient();       // クライアントからのアクセスの処理(ブラウザからのリクエストを処理)
  }
}
* フラッシュメモリ(1.3Mbyte)を、スケッチが58%使用。RAM(327kbyte)は、グローバル変数が37%使用、ローカル変数で203kbyte使用可能。(1000byte=1kbyteで計算)