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

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

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

```c
#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>
```
### センサー用

```c
// (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ルータ接続

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

long timezone = 9; //Tokyo
byte daysavetime = 1;
```
### WebServerとWebSocketsServerインスタンス起動
```c
// Webサーバー
WebServer webServer(80); // 80番ポート
// Websocketサーバー
WebSocketsServer webSocket = WebSocketsServer(81); // 81番ポート

// サンプリング周期
elapsedMillis sensorElapsed;
const unsigned long DELAY = 1000; // ms
```
### SENSER_JSON[] PROGRAM = R"=====()====="の利用
```c
// センサのデータ(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を配列に格納
```c
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のファイル操作
```c
/* 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()//初期化
```c
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上のファイル書き出し
```c
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);
  }
}
```
# 繰り返し測定
```c
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に測定値グラフ出力
```c
// 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コンテンツメニュー出力
```c
// 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に測定値表示
```c
// 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に格納測定値をグラフ描画

```c
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にグラフ出力
アクセス開始から３６０個分表示

```c
// 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()
```c
void handleNotFound() {
  webServer.send(404, "text/plain", "File not found.");
}
```