1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【ESP32入門】測定値をWebServerに表示する♪

Last updated at Posted at 2022-02-26

記事の推し

・測定値をWebServerに表示する種々のコードを公開
・SPIFFSの利用
・Chart.jsでグラフ表示
前回の記事の参考➂の履歴表示をやってみた
・温湿度・気圧測定は、BME280を利用
・公開を優先し、(コード読めばわかると思うので)余分な解説は記載しないこととする

環境は以下のライブラリを使う

#include "FS.h"
#include "SPIFFS.h"

#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h> // arduinoWebSocketsライブラリ
#include <elapsedMillis.h> // elapsedMillisライブラリ
#include "index_html.h" // web server root index
#include "index_html2.h" // web server draw index
#include "index_html3.h" // web server draw index
#include <Wire.h>
#include <time.h>

センサー用

// (1) センサライブラリのヘッダファイル
#include "Seeed_BME280.h"
//データ格納用配列
#define CHART_SZ 3600
float tempdat[CHART_SZ];
float humiddat[CHART_SZ];
float pressdat[CHART_SZ];
//温湿度気圧データ一時保管用
float temp, humd, pressure;
// (2) センサの定義
BME280 bme280; // temperature & humidity sensor

WiFiルータ接続

// アクセスポイントのESSIDとパスワード
const char* ssid_ = "********";
const char* pass_ = "********";

long timezone = 9; //Tokyo
byte daysavetime = 1;

WebServerとWebSocketsServerインスタンス起動

// Webサーバー
WebServer webServer(80); // 80番ポート
// Websocketサーバー
WebSocketsServer webSocket = WebSocketsServer(81); // 81番ポート

// サンプリング周期
elapsedMillis sensorElapsed;
const unsigned long DELAY = 1000; // ms

SENSER_JSON[] PROGRAM = R"=====()====="の利用

// センサのデータ(JSON形式)
const char SENSOR_JSON[] PROGMEM = R"=====({"time0":%d,"time":%d,"temp":%.1f,"humd":%.1f,"pressure":%.1f})=====";
char payload[200];

// データの更新
time_t Time1; //=time(NULL);

void sensor_loop(time_t Time0) {
//=============================================
// (4) センシング
  time_t tim_ = time(NULL)-Time1;
  temp = bme280.getTemperature();
  humd = bme280.getHumidity();
  pressure = (bme280.getPressure()-100000)/100.;
  snprintf_P(payload, sizeof(payload), SENSOR_JSON, Time0, tim_, temp, humd, pressure);
//============================================= 

  // WebSocketでデータ送信(全端末へブロードキャスト)
  webSocket.broadcastTXT(payload, strlen(payload));
  Serial.println(payload);
}

SenserDataを配列に格納

void charDataSet(void){
  memmove( &tempdat[1], &tempdat[0],(CHART_SZ-1)*sizeof(float));
  memmove( &humiddat[1], &humiddat[0],(CHART_SZ-1)*sizeof(float));
  memmove( &pressdat[1], &pressdat[0],(CHART_SZ-1)*sizeof(float));
  tempdat[0] = temp;
  humiddat[0] = humd;
  pressdat[0] = pressure;
}

SPIFFSのファイル操作

/* You only need to format SPIFFS the first time you run a
   test or else use the SPIFFS plugin to create a partition
   https://github.com/me-no-dev/arduino-esp32fs-plugin */
#define FORMAT_SPIFFS_IF_FAILED true

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\r\n", dirname);

    File root = fs.open(dirname);
    if(!root){
        Serial.println("- failed to open directory");
        return;
    }
    if(!root.isDirectory()){
        Serial.println(" - not a directory");
        return;
    }

    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            Serial.println(file.name());
            if(levels){
                listDir(fs, file.name(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("\tSIZE: ");
            Serial.println(file.size());
        }
        file = root.openNextFile();
    }
}

void readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\r\n", path);

    File file = fs.open(path);
    if(!file || file.isDirectory()){
        Serial.println("- failed to open file for reading");
        return;
    }

    Serial.println("- read from file:");
    while(file.available()){
        Serial.write(file.read());
    }
    file.close();
}

void writeFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Writing file: %s\r\n", path);

    File file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("- failed to open file for writing");
        return;
    }
    if(file.print(message)){
        Serial.println("- file written");
    } else {
        Serial.println("- write failed");
    }
    file.close();
}

void appendFile(fs::FS &fs, const char * path, const char * message){
    Serial.printf("Appending to file: %s\r\n", path);

    File file = fs.open(path, FILE_APPEND);
    if(!file){
        Serial.println("- failed to open file for appending");
        return;
    }
    if(file.print(message)){
        Serial.println("- message appended");
    } else {
        Serial.println("- append failed");
    }
    file.close();
}

void renameFile(fs::FS &fs, const char * path1, const char * path2){
    Serial.printf("Renaming file %s to %s\r\n", path1, path2);
    if (fs.rename(path1, path2)) {
        Serial.println("- file renamed");
    } else {
        Serial.println("- rename failed");
    }
}

void deleteFile(fs::FS &fs, const char * path){
    Serial.printf("Deleting file: %s\r\n", path);
    if(fs.remove(path)){
        Serial.println("- file deleted");
    } else {
        Serial.println("- delete failed");
    }
}

setup()//初期化

void setup(){
    Serial.begin(115200);
    WiFi.begin(ssid_, pass_);
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
    }
    // ESP32のIPアドレスを出力
    Serial.println("WiFi Connected.");
    Serial.print("IP = ");
    Serial.println(WiFi.localIP());
    
    if(!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)){
        Serial.println("SPIFFS Mount Failed");
        return;
    }
    // センサの初期化
    if (!bme280.init()) {
        Serial.println("Device error!");
    }
    Serial.println("Device init");

    //ntpより時刻取得
    configTime(3600*timezone, daysavetime*3600, "time.nist.gov", "0.pool.ntp.org", "1.pool.ntp.org");
    struct tm tmstruct ;
    delay(2000);
    tmstruct.tm_year = 0;
    getLocalTime(&tmstruct, 5000);
    Serial.printf("\nNow is : %d-%02d-%02d %02d:%02d:%02d\n",(tmstruct.tm_year)+1900,( tmstruct.tm_mon)+1, tmstruct.tm_mday,tmstruct.tm_hour , tmstruct.tm_min, tmstruct.tm_sec);
    Serial.println("");
    Time1=time(NULL);
    Serial.print(Time1);
   
    // Webサーバーのコンテンツ設定
    // favicon.ico, Chart.min.jsは dataフォルダ内に配置
    SPIFFS.begin();
    //webServer定義
    webServer.serveStatic("/favicon.ico", SPIFFS, "/favicon.ico"); //配置せずdefault
    webServer.serveStatic("/Chart.min.js", SPIFFS, "/Chart.min.js");
    webServer.on("/", handleRoot);
    webServer.on("/draw", handleDraw);
    webServer.on("/value", handleValue);
    webServer.on("/rireki", handleRireki);
    webServer.on("/rireki2", handleRireki2);
    webServer.onNotFound(handleNotFound);
    webServer.begin();
  
    // WebSocketサーバー開始
    webSocket.begin();
    deleteFile(SPIFFS, "/hello.txt");    //file 初期化
    File root = SPIFFS.open("/");
    uint8_t levels=0;
    File file = root.openNextFile();
    while(file){
        if(file.isDirectory()){
            Serial.print("  DIR : ");
            //Serial.println(file.name());
            if(levels){
                listDir(SPIFFS, file.name(), levels -1);
            }
        } else {
            Serial.print("  FILE: ");
            Serial.print(file.name());
            Serial.print("\tSIZE: ");
            Serial.println(file.size());
            //size<3000の時ファイル削除できる            
            if(file.size() < 30000){
              Serial.print(file.name());
              Serial.print("\n");
              deleteFile(SPIFFS, file.name()); 
             }
        }
        file = root.openNextFile();
    }
    
}

一定条件で測定値をSPIFFS上のファイル書き出し

int s = 0;
void save_File(){
  if (s%3600!=0){
    listDir(SPIFFS, "/", 0);
    Serial.println(payload);
    if(s%10==1){
      appendFile(SPIFFS, "/hello.txt", payload);
      appendFile(SPIFFS, "/hello.txt", "\r\n");
    }
  }else{
    char fname[32];
    time_t t = time(NULL);
    //一定時刻毎にファイル出力。ファイル名は書出し時刻
    strftime(fname, sizeof(fname), "/%Y%m%d%H%M.txt", localtime(&t));
    renameFile(SPIFFS, "/hello.txt", fname);
    writeFile(SPIFFS, "/hello.txt", payload);
    appendFile(SPIFFS, "/hello.txt", "\r\n");
    readFile(SPIFFS, fname);
  }
}

繰り返し測定

void loop(){
  webSocket.loop();
  webServer.handleClient();
  
  // 一定の周期でセンシング
  if (sensorElapsed > DELAY) {
    sensorElapsed = 0;
    sensor_loop(Time1);
  }
  save_File();

  s += 1;
  if(s%1==0){
    charDataSet();
  }
  delay(1000);
}

webに測定値グラフ出力

// Webコンテンツのイベントハンドラ
void handleDraw() {
  String ss = INDEX_HTML; // index_html.hより読み込み
  webServer.send(200, "text/html", ss);
}

index_html.h

index_html.h
// index_html.h
const char INDEX_HTML[] PROGMEM = R"=====(
<!DOCTYPE html><html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Sensor graph</title>
<link rel="shortcut icon" href="/favicon.ico" />
</head>
<div style="text-align:center;"><b>Sensor graph</b></div>
<div class="chart-container" position: relative; height:350px; width:100%">
  <canvas id="myChart" width="600" height="400"></canvas>
</div>
<br><br>
<script src = "/Chart.min.js"></script>  
<script>
var graphData = {
  labels: [],  // X軸のデータ (時間)
  datasets: [{
                label: "温度",
                data: [], // Y temp
                fill: false,
                borderColor: "rgba(255, 99, 132, 0.2)",
                backgroundColor: "rgba(254,97,132,0.5)",
            },
            {
                label: "湿度",
                data: [], // Y humd
                fill: false,
                borderColor: "rgba(54, 162, 235, 0.2)",
                backgroundColor: "rgba(54, 162, 235, 1)",
            },
            {
                label: "気圧",
                data: [], // Y pressure
                fill: false,
                borderColor: "rgba(255, 206, 86, 0.2)",
                backgroundColor: "rgba(255, 206, 86, 1)",
            }
  
  ]
};
var graphOptions = {
  maintainAspectRatio: false,
  scales: {
    yAxes: [{
      ticks: {beginAtZero:true}
    }]
  }
};

var ctx = document.getElementById("myChart").getContext('2d');
var chart = new Chart(ctx, {
  type: 'line',
  data: graphData,
  options: graphOptions
});

var ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onmessage = function(evt) {
  var Time = JSON.parse(evt.data)["time"]; //new Date().toLocaleTimeString();
  var data_x1 = JSON.parse(evt.data)["temp"];
  var data_x2 = JSON.parse(evt.data)["humd"];
  var data_x3 = JSON.parse(evt.data)["pressure"];
  console.log(Time);
  console.log(data_x1);
  console.log(data_x2);
  console.log(data_x3);

  chart.data.labels.push(Time);
  chart.data.datasets[0].data.push(data_x1);
  chart.data.datasets[1].data.push(data_x2);
  chart.data.datasets[2].data.push(data_x3);
  chart.update();
};
ws.onclose = function(evt) {
  console.log("ws: onclose");
  ws.close();
}
ws.onerror = function(evt) {
  console.log(evt);
}
</script>
</body></html>
)=====";

webコンテンツメニュー出力

// Webコンテンツのイベントハンドラ
void handleRoot() {
  String ss = INDEX_HTML2; // index_html2.hより読み込み
  webServer.send(200, "text/html", ss);
}

index_html2.h

index_html2.h
// index_html2.h
const char INDEX_HTML2[] PROGMEM = R"=====(
<!DOCTYPE html><html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Sensor Value</title>
</head>
<body>
<p>リンクをクリックすると測定します</p>
<ul>
<li><a href=\draw>Draw</a></li>
<li><a href=\value>Value</a></li>
<li><a href=\rireki>Rireki</a></li>
<li><a href=\rireki2>Rireki2</a></li>
</ul>
</body></html>
)=====";

webに測定値表示

// Webコンテンツのイベントハンドラ
void handleValue() {
  String html;
  int i;
  time_t Time0=time(NULL);
  sensor_loop(Time0);
  configTime(3600*timezone, daysavetime*3600, "time.nist.gov", "0.pool.ntp.org", "1.pool.ntp.org");
  struct tm tmstruct ;
  delay(2000);
  tmstruct.tm_year = 0;
  getLocalTime(&tmstruct, 5000);
  const char JSON_time[] PROGMEM = R"=====( %d年%02d月%02d日 %02d:%02d:%02d)=====";
  char timNow[200];
  
  snprintf_P(timNow,sizeof(timNow), JSON_time,(tmstruct.tm_year)+1900,( tmstruct.tm_mon)+1, tmstruct.tm_mday,tmstruct.tm_hour , tmstruct.tm_min, tmstruct.tm_sec);

  // HTMLを組み立てる
  html = "<!DOCTYPE html>";
  html += "<html>";
  html += "<head>";
  html += "<meta charset=\"utf-8\">";
  html += "<title>測定する</title>";
  html += "</head>";
  html += "<body>";
  html += "<p>現在の測定値</p>";
  html += "<ul>";
  html += "測定日";
  html += timNow;
  //html += "<li>";
  //html += payload;
  html += "<li>気圧 ";
  html += pressure+1000;
  html += " Pa"; //text_gP;
  html += "<li>温度 ";
  html += temp;
  html += " C"; //text_gT;
  html += "<li>湿度 ";
  html += humd;
  html += " %"; //text_gH;
  html += "</li>";
  html += "</ul>";
  html += "</body>";
  html += "</html>";
  webServer.send(200, "text/html", html);
}

Webに格納測定値をグラフ描画

void handleRireki() {
  String str = "";

  str += "<!DOCTYPE html><html><head>";
  str += "<meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>\n";
  str += "<title>Sensor graph</title>\n";
  str += "<link rel='shortcut icon' href='/favicon.ico' />\n";
  str += "</head>\n";
  str += "<div style='text-align:center;'><b>Sensor graph</b></div>\n";
  str += "<div class='chart-container' position: relative; height:350px; width:100%>\n";
  str += "<canvas id='myChart' width='600' height='400'></canvas>\n";
  str += "</div>\n";
  str += "<br><br>\n";
  str += "<script src = '/Chart.min.js'></script>\n";  
  str += "<script>\n";
  str += "var graphData = {\n";
  //str += "  labels: ["; //1,2,3],  // X軸のデータ (時間)\n";
  str += "  labels: [";
  for(uint16_t i = 0; i< CHART_SZ; i=i+10 ){
    str += i; 
    if(i != CHART_SZ-1 )str += ",";
  }
  str += "],\n";  
  str += "  datasets: [{\n";
  str += "                label: '温度',\n";
  str += "                data: [";
  for(uint16_t i = 0; i< CHART_SZ; i=i+10 ){
    str += tempdat[i]; 
    if(i != CHART_SZ-1 )str += ",";
  }
  str += "],\n";  
  str += "                fill: false,\n";
  str += "                borderColor: 'rgba(255, 99, 132, 0.2)',\n";
  str += "                backgroundColor: 'rgba(254,97,132,0.5)',\n";
  str += "            },\n";
  str += "            {\n";
  str += "                label: '湿度',\n";
  str += "                data: [";
  for(uint16_t i = 0; i< CHART_SZ; i=i+10 ){
    str += humiddat[i]; 
    if(i != CHART_SZ-1 )str += ",";
  }
  str += "],\n";
  str += "                fill: false,\n";
  str += "                borderColor: 'rgba(54, 162, 235, 0.2)',\n";
  str += "                backgroundColor: 'rgba(54, 162, 235, 1)',\n";
  str += "            },\n";
  str += "            {\n";
  str += "                label: '気圧',\n";
  str += "                data: [";
  for(uint16_t i = 0; i< CHART_SZ; i=i+10 ){
    str += pressdat[i]; 
    if(i != CHART_SZ-1 )str += ",";
  }
  str += "],\n";  
  str += "                fill: false,\n";
  str += "                borderColor: 'rgba(255, 206, 86, 0.2)',\n";
  str += "                backgroundColor: 'rgba(255, 206, 86, 1)',\n";
  str += "            }\n";
  str += "  ]\n";
  str += "};\n";
  str += "var graphOptions = {\n";
  str += "  maintainAspectRatio: false,\n";
  str += "  scales: {\n";
  str += "    yAxes: [{\n";
  str += "      ticks: {beginAtZero:true}\n";
  str += "    }]\n";
  str += "  }\n";
  str += "};\n";
  str += "var ctx = document.getElementById('myChart').getContext('2d');\n";
  str += "var chart = new Chart(ctx, {\n";
  str += "  type: 'line',\n";
  str += "  data: graphData,\n";
  str += "  options: graphOptions\n";
  str += "});\n";
  str += "ws.onclose = function(evt) {\n";
  str += "  console.log('ws: onclose');\n";
  str += "  ws.close();\n";
  str += "}\n";
  str += "ws.onerror = function(evt) {\n";
  str += "  console.log(evt);\n";
  str += "}\n";
  str += "</script>\n";
  str += "</body></html>\n";  

  webServer.send(200,"text/html", str);
  Serial.print("########CHART_SZ######\n");
}

Webにグラフ出力

アクセス開始から360個分表示

// Webコンテンツのイベントハンドラ
time_t Time0;
void handleRireki2() {
  Time0 = time(NULL)-Time1;
  Serial.print(Time0);
  String ss = INDEX_HTML3; // index_html.hより読み込み
  webServer.send(200, "text/html", ss);
  for(uint16_t i = 0; i< 360; i++ ){
    webSocket.loop();
    webServer.handleClient();
    sensor_loop(Time0);
    delay(1000);
    save_File();
    s += 1;
    charDataSet();
  }
}

index_html3.h

index_html3.h
// index_html3.h
const char INDEX_HTML3[] PROGMEM = R"=====(
<!DOCTYPE html><html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Sensor graph</title>
<link rel="shortcut icon" href="/favicon.ico" />
</head>
<div style="text-align:center;"><b>Sensor graph</b></div>
<div class="chart-container" position: relative; height:350px; width:100%">
  <canvas id="myChart" width="600" height="400"></canvas>
</div>
<br><br>
<script src = "/Chart.min.js"></script>  
<script>
var graphData = {
  labels: [0,1,2],  // X軸のデータ (時間)
  datasets: [{
                label: "温度",
                data: [1,2,3], // Y temp
                fill: false,
                borderColor: "rgba(255, 99, 132, 0.2)",
                backgroundColor: "rgba(254,97,132,0.5)",
            },
            {
                label: "湿度",
                data: [4,5,6], // Y humd
                fill: false,
                borderColor: "rgba(54, 162, 235, 0.2)",
                backgroundColor: "rgba(54, 162, 235, 1)",
            },
            {
                label: "気圧",
                data: [7,5,3], // Y pressure
                fill: false,
                borderColor: "rgba(255, 206, 86, 0.2)",
                backgroundColor: "rgba(255, 206, 86, 1)",
            }
  ]
};
var graphOptions = {
  maintainAspectRatio: false,
  scales: {
    yAxes: [{
      ticks: {beginAtZero:true}
    }]
  }
};

var ctx = document.getElementById("myChart").getContext('2d');
var chart = new Chart(ctx, {
  type: 'line',
  data: graphData,
  options: graphOptions
});

var ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onmessage = function(evt) {
  var Time0 = JSON.parse(evt.data)["time0"];
  var Time = JSON.parse(evt.data)["time"]; //new Date().toLocaleTimeString();
  var data_x1 = JSON.parse(evt.data)["temp"];
  var data_x2 = JSON.parse(evt.data)["humd"];
  var data_x3 = JSON.parse(evt.data)["pressure"];
  console.log(Time);
  console.log(data_x1);
  console.log(data_x2);
  console.log(data_x3);
  console.log(Time0);
  if(Time-Time0<360){
    chart.data.labels.push(Time);
    chart.data.datasets[0].data.push(data_x1);
    chart.data.datasets[1].data.push(data_x2);
    chart.data.datasets[2].data.push(data_x3);
    chart.update();
  }
};

ws.onclose = function(evt) {
  console.log("ws: onclose");
  ws.close();
}
ws.onerror = function(evt) {
  console.log(evt);
}
</script>
</body></html>
)=====";

handleNotFound()

void handleNotFound() {
  webServer.send(404, "text/plain", "File not found.");
}
1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?