【Chart.js入門】測定値をWebServerに描画する♪

![draw_sensor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233744/192b0347-12f4-5ab0-c47b-3f168824531e.png)

#参考
以下の参考は、それぞれ以下の情報を提供してくれました。
参考➀；基本的なアプリ
参考➁；複数データ対応と必要な技術の解説
参考➂；Chart.jsのESP32書き込みなど環境構築と履歴データの格納と表示など
➀[ESP32: Webサーバ上でリアルタイムグラフ表示(Chart.js)](https://web.is.tokushima-u.ac.jp/wp/blog/2019/07/12/esp32-web%E3%82%B5%E3%83%BC%E3%83%90%E4%B8%8A%E3%81%A7%E3%82%B0%E3%83%A9%E3%83%95%E8%A1%A8%E7%A4%BAchart-js/)
➁[ESP32：センサーの値をブラウザにてリアルタイムで表示させる](https://chelog.net/esp32_sensor_webserver_graph/)
➂[ESP32-WROOM-32Eで温湿度データをグラフで表示する](https://smtengkapi.com/engineer-esp32-wroom-32-chart)

#測定＆表示コード；基本的なアプリ
```c
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h> // arduinoWebSocketsライブラリ
#include <elapsedMillis.h> // elapsedMillisライブラリ
#include <SPIFFS.h>
#include "index_html.h" // web server root index

//===============================-
// (1) センサライブラリのヘッダファイル
#include "Seeed_BME280.h"

//===============================-
// (2) センサの定義
BME280 bme280; // temperature sensor

// WiFi設定
const char *ssid = "********"; // 各自のSSIDを入力
const char *pass = "********"; //各自のパスワードを入力

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

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

// センサのデータ(JSON形式)
const char SENSOR_JSON[] PROGMEM = R"=====({"val1":%.1f})=====";

// データの更新
void sensor_loop() {
  char payload[16];
  // (4) センシング
  float temp = bme280.getTemperature();
  snprintf_P(payload, sizeof(payload), SENSOR_JSON, temp);
  // WebSocketでデータ送信(全端末へブロードキャスト)
  webSocket.broadcastTXT(payload, strlen(payload));
  Serial.println(payload);
}

void setup() {
  // シリアル通信設定
  Serial.begin(115200);

  // アクセスポイントに接続
  WiFi.disconnect(true);
  delay(1000);

  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 (!bme280.init()) {
      Serial.println("Device error!");
  }
  Serial.println("Device init");
  
  // Webサーバーのコンテンツ設定
  // favicon.ico, Chart.min.jsは dataフォルダ内に配置
  SPIFFS.begin();
  webServer.serveStatic("/favicon.ico", SPIFFS, "/favicon.ico"); //配置せずdefault
  webServer.serveStatic("/Chart.min.js", SPIFFS, "/Chart.min.js");
  webServer.on("/", handleRoot);
  webServer.onNotFound(handleNotFound);
  webServer.begin();

  // WebSocketサーバー開始
  webSocket.begin();
}

void loop(void) {
  webSocket.loop();
  webServer.handleClient();

  // 一定の周期でセンシング
  if (sensorElapsed > DELAY) {
    sensorElapsed = 0;
    sensor_loop();
  }
}

// Webコンテンツのイベントハンドラ
void handleRoot() {
  String s = INDEX_HTML; // index_html.hより読み込み
  webServer.send(200, "text/html", s);
}
void handleNotFound() {
  webServer.send(404, "text/plain", "File not found.");
}
```
####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: "Sensor-01",
        data: [], // Y軸のデータ(センシング結果)
        fill: false,
        borderColor : "rgba(254,97,132,0.8)",
        backgroundColor : "rgba(254,97,132,0.5)",
  }]
};
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 = new Date().toLocaleTimeString();
  var data_x1 = JSON.parse(evt.data)["val1"];
  console.log(Time);
  console.log(data_x1);

  chart.data.labels.push(Time);
  chart.data.datasets[0].data.push(data_x1);
  chart.update();
};
ws.onclose = function(evt) {
  console.log("ws: onclose");
  ws.close();
}
ws.onerror = function(evt) {
  console.log(evt);
}
</script>
</body></html>
)=====";
```
####Seeed_BME280.cpp
<details><summary>Seeed_BME280.cpp</summary><div>

```Seeed_BME280.cpp
#include "Seeed_BME280.h"

bool BME280::init(int i2c_addr) {
    uint8_t retry = 0;
    uint8_t chip_id = 0;


    _devAddr = i2c_addr;
    Wire.begin();

    while ((retry++ < 5) && (chip_id != 0x60)) {
        chip_id = BME280Read8(BME280_REG_CHIPID);
        #ifdef BMP280_DEBUG_PRINT
        Serial.print("Read chip ID: ");
        Serial.println(chip_id);
        #endif
        delay(100);
    }
    if (chip_id != 0x60){
        Serial.println("Read Chip ID fail!");
        return false;
    }

    dig_T1 = BME280Read16LE(BME280_REG_DIG_T1);
    dig_T2 = BME280ReadS16LE(BME280_REG_DIG_T2);
    dig_T3 = BME280ReadS16LE(BME280_REG_DIG_T3);

    dig_P1 = BME280Read16LE(BME280_REG_DIG_P1);
    dig_P2 = BME280ReadS16LE(BME280_REG_DIG_P2);
    dig_P3 = BME280ReadS16LE(BME280_REG_DIG_P3);
    dig_P4 = BME280ReadS16LE(BME280_REG_DIG_P4);
    dig_P5 = BME280ReadS16LE(BME280_REG_DIG_P5);
    dig_P6 = BME280ReadS16LE(BME280_REG_DIG_P6);
    dig_P7 = BME280ReadS16LE(BME280_REG_DIG_P7);
    dig_P8 = BME280ReadS16LE(BME280_REG_DIG_P8);
    dig_P9 = BME280ReadS16LE(BME280_REG_DIG_P9);

    dig_H1 = BME280Read8(BME280_REG_DIG_H1);
    dig_H2 = BME280Read16LE(BME280_REG_DIG_H2);
    dig_H3 = BME280Read8(BME280_REG_DIG_H3);
    dig_H4 = (BME280Read8(BME280_REG_DIG_H4) << 4) | (0x0F & BME280Read8(BME280_REG_DIG_H4 + 1));
    dig_H5 = (BME280Read8(BME280_REG_DIG_H5 + 1) << 4) | (0x0F & BME280Read8(BME280_REG_DIG_H5) >> 4);
    dig_H6 = (int8_t)BME280Read8(BME280_REG_DIG_H6);

    writeRegister(BME280_REG_CONTROLHUMID, 0x05);  //Choose 16X oversampling
    writeRegister(BME280_REG_CONTROL, 0xB7);  //Choose 16X oversampling

    return true;
}

float BME280::getTemperature(void) {
    int32_t var1, var2;

    int32_t adc_T = BME280Read24(BME280_REG_TEMPDATA);
    // Check if the last transport successed
    if (!isTransport_OK) {
        return 0;
    }
    adc_T >>= 4;
    var1 = (((adc_T >> 3) - ((int32_t)(dig_T1 << 1))) *
            ((int32_t)dig_T2)) >> 11;

    var2 = (((((adc_T >> 4) - ((int32_t)dig_T1)) *
              ((adc_T >> 4) - ((int32_t)dig_T1))) >> 12) *
            ((int32_t)dig_T3)) >> 14;

    t_fine = var1 + var2;
    float T = (t_fine * 5 + 128) >> 8;
    return T / 100;
}

uint32_t BME280::getPressure(void) {
    int64_t var1, var2, p;

    // Call getTemperature to get t_fine
    getTemperature();
    // Check if the last transport successed
    if (!isTransport_OK) {
        return 0;
    }

    int32_t adc_P = BME280Read24(BME280_REG_PRESSUREDATA);
    adc_P >>= 4;

    var1 = ((int64_t)t_fine) - 128000;
    var2 = var1 * var1 * (int64_t)dig_P6;
    var2 = var2 + ((var1 * (int64_t)dig_P5) << 17);
    var2 = var2 + (((int64_t)dig_P4) << 35);
    var1 = ((var1 * var1 * (int64_t)dig_P3) >> 8) + ((var1 * (int64_t)dig_P2) << 12);
    var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)dig_P1) >> 33;
    if (var1 == 0) {
        return 0; // avoid exception caused by division by zero
    }
    p = 1048576 - adc_P;
    p = (((p << 31) - var2) * 3125) / var1;
    var1 = (((int64_t)dig_P9) * (p >> 13) * (p >> 13)) >> 25;
    var2 = (((int64_t)dig_P8) * p) >> 19;
    p = ((p + var1 + var2) >> 8) + (((int64_t)dig_P7) << 4);
    return (uint32_t)p / 256;
}

uint32_t BME280::getHumidity(void) {
    int32_t v_x1_u32r, adc_H;

    // Call getTemperature to get t_fine
    getTemperature();
    // Check if the last transport successed
    if (!isTransport_OK) {
        return 0;
    }

    adc_H = BME280Read16(BME280_REG_HUMIDITYDATA);

    v_x1_u32r = (t_fine - ((int32_t)76800));
    v_x1_u32r = (((((adc_H << 14) - (((int32_t)dig_H4) << 20) - (((int32_t)dig_H5) * v_x1_u32r)) + ((
                       int32_t)16384)) >> 15) * (((((((v_x1_u32r * ((int32_t)dig_H6)) >> 10) * (((v_x1_u32r * ((int32_t)dig_H3)) >> 11) + ((
                                   int32_t)32768))) >> 10) + ((int32_t)2097152)) * ((int32_t)dig_H2) + 8192) >> 14));
    v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * ((int32_t)dig_H1)) >> 4));
    v_x1_u32r = (v_x1_u32r < 0 ? 0 : v_x1_u32r);
    v_x1_u32r = (v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r);
    return (uint32_t)(v_x1_u32r >> 12) / 1024.0;
}

float BME280::calcAltitude(float pressure) {
    if (!isTransport_OK) {
        return 0;
    }

    float A = pressure / 101325;
    float B = 1 / 5.25588;
    float C = pow(A, B);
    C = 1.0 - C;
    C = C / 0.0000225577;
    return C;
}

uint8_t BME280::BME280Read8(uint8_t reg) {
    Wire.beginTransmission(_devAddr);
    Wire.write(reg);
    Wire.endTransmission();

    Wire.requestFrom(_devAddr, 1);
    // return 0 if slave didn't response
    if (Wire.available() < 1) {
        isTransport_OK = false;
        return 0;
    } else {
        isTransport_OK = true;
    }

    return Wire.read();
}

uint16_t BME280::BME280Read16(uint8_t reg) {
    uint8_t msb, lsb;

    Wire.beginTransmission(_devAddr);
    Wire.write(reg);
    Wire.endTransmission();

    Wire.requestFrom(_devAddr, 2);
    // return 0 if slave didn't response
    if (Wire.available() < 2) {
        isTransport_OK = false;
        return 0;
    } else {
        isTransport_OK = true;
    }
    msb = Wire.read();
    lsb = Wire.read();

    return (uint16_t) msb << 8 | lsb;
}

uint16_t BME280::BME280Read16LE(uint8_t reg) {
    uint16_t data = BME280Read16(reg);
    return (data >> 8) | (data << 8);
}

int16_t BME280::BME280ReadS16(uint8_t reg) {
    return (int16_t)BME280Read16(reg);
}

int16_t BME280::BME280ReadS16LE(uint8_t reg) {
    return (int16_t)BME280Read16LE(reg);
}

uint32_t BME280::BME280Read24(uint8_t reg) {
    uint32_t data;

    Wire.beginTransmission(_devAddr);
    Wire.write(reg);
    Wire.endTransmission();

    Wire.requestFrom(_devAddr, 3);
    // return 0 if slave didn't response
    if (Wire.available() < 3) {
        isTransport_OK = false;
        return 0;
    } else if (isTransport_OK == false) {
        isTransport_OK = true;
        if (!init(_devAddr)) {
            #ifdef BMP280_DEBUG_PRINT
            Serial.println("Device not connected or broken!");
            #endif
        }
    }
    data = Wire.read();
    data <<= 8;
    data |= Wire.read();
    data <<= 8;
    data |= Wire.read();

    return data;
}

void BME280::writeRegister(uint8_t reg, uint8_t val) {
    Wire.beginTransmission(_devAddr); // start transmission to device
    Wire.write(reg);       // send register address
    Wire.write(val);         // send value to write
    Wire.endTransmission();     // end transmission
}
```
</div></details>
####Seeed_BME280.h
<details><summary>Seeed_BME280.h</summary><div>

```Seeed_BME280.h
#pragma once

#ifndef _SEEED_BME280_H_
#define _SEEED_BME280_H_

#include <Arduino.h>
#include <Wire.h>

#define BME280_ADDRESS   0x76

#define BME280_REG_DIG_T1    0x88
#define BME280_REG_DIG_T2    0x8A
#define BME280_REG_DIG_T3    0x8C

#define BME280_REG_DIG_P1    0x8E
#define BME280_REG_DIG_P2    0x90
#define BME280_REG_DIG_P3    0x92
#define BME280_REG_DIG_P4    0x94
#define BME280_REG_DIG_P5    0x96
#define BME280_REG_DIG_P6    0x98
#define BME280_REG_DIG_P7    0x9A
#define BME280_REG_DIG_P8    0x9C
#define BME280_REG_DIG_P9    0x9E

#define BME280_REG_DIG_H1    0xA1
#define BME280_REG_DIG_H2    0xE1
#define BME280_REG_DIG_H3    0xE3
#define BME280_REG_DIG_H4    0xE4
#define BME280_REG_DIG_H5    0xE5
#define BME280_REG_DIG_H6    0xE7

#define BME280_REG_CHIPID          0xD0
#define BME280_REG_VERSION         0xD1
#define BME280_REG_SOFTRESET       0xE0

#define BME280_REG_CAL26           0xE1

#define BME280_REG_CONTROLHUMID    0xF2
#define BME280_REG_CONTROL         0xF4
#define BME280_REG_CONFIG          0xF5
#define BME280_REG_PRESSUREDATA    0xF7
#define BME280_REG_TEMPDATA        0xFA
#define BME280_REG_HUMIDITYDATA    0xFD

class BME280 {
  public:
    bool init(int i2c_addr = BME280_ADDRESS);
    float getTemperature(void);
    uint32_t getPressure(void);
    uint32_t getHumidity(void);
    float calcAltitude(float pressure);
  private:
    int _devAddr;
    bool isTransport_OK;

    // Calibration data
    uint16_t dig_T1;
    int16_t dig_T2;
    int16_t dig_T3;
    uint16_t dig_P1;
    int16_t dig_P2;
    int16_t dig_P3;
    int16_t dig_P4;
    int16_t dig_P5;
    int16_t dig_P6;
    int16_t dig_P7;
    int16_t dig_P8;
    int16_t dig_P9;
    uint8_t dig_H1;
    int16_t dig_H2;
    uint8_t dig_H3;
    int16_t dig_H4;
    int16_t dig_H5;
    int8_t  dig_H6;
    int32_t t_fine;

    // private functions
    uint8_t BME280Read8(uint8_t reg);
    uint16_t BME280Read16(uint8_t reg);
    uint16_t BME280Read16LE(uint8_t reg);
    int16_t BME280ReadS16(uint8_t reg);
    int16_t BME280ReadS16LE(uint8_t reg);
    uint32_t BME280Read24(uint8_t reg);
    void writeRegister(uint8_t reg, uint8_t val);
};

#endif
```

</div></details>
#WiFi.softAPでESP32をWiFiアクセスポイントにする
この手法のメリットは、ルーターがない場所でも利用できること、そして固定IPAddressを利用できることであろ。

上記の//WiFi設定を以下に変更する。

```c
// WiFi設定
const char ssid[] = "ESP32-WiFi";  // SSID
const char pass[] = "esp32wifi";   // password

const IPAddress ip(192, 168, 20, 2); //fixed IP adress
const IPAddress subnet(255, 255, 255, 0);
```
そして、以下の通り、WiFi.softAP()などで、アクセスポイント確立します。

```c
  // アクセスポイントを確立
  // Wi-Fi設定
  Serial.println("wifi setup start");
  WiFi.disconnect(true);
  delay(1000);
  WiFi.softAP(ssid, pass);
  delay(100);
  WiFi.softAPConfig(ip, ip, subnet);
  delay(100);
  IPAddress myIP = WiFi.softAPIP();
```
そして、確立したアクセスポイントを出力する。

```c
  // 各種情報を表示 
  Serial.print("SSID: ");
  Serial.println(ssid);
  Serial.print("AP IP address: ");
  Serial.println(myIP);
```
上で定義した、アクセスポイントが出力されるので、**そのWiFiポイントにWiFiで接続する。**
そして、上記で定義したmyIPで出力されたURLをブラウズすると、上記と同じようなグラフが見える。
#3つの測定値（温度、湿度、気圧）のグラフ表示
まず、SENSOR_JSON[]を以下のように３測定値が入るように拡張する。
そして、３測定値を定義して、格納する配列payload[200]を拡大している。
気圧は、縦軸が合うように適当に演算している。

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

void sensor_loop() {
  char payload[200];
  // (4) センシング
  float temp = bme280.getTemperature();
  float humd = bme280.getHumidity();
  float pressure = (bme280.getPressure()-100000)/100.;
  snprintf_P(payload, sizeof(payload), SENSOR_JSON, temp, humd, pressure);
  // WebSocketでデータ送信(全端末へブロードキャスト)
  webSocket.broadcastTXT(payload, strlen(payload));
  Serial.println(payload);
}
```

一方、index_html.h内のscriptも以下のように3測定値に拡張する。

```index_html.h
<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 = 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>
```

結果は、以下のように表示される。
![draw_sensor3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233744/d34cca5f-a2b1-2d62-adaf-47b77063fd73.png)
###課題
この手法だと、表示はアクセスした時点からの経過を表示できるが、過去のデータは表示できない。
そこで、上記参考➂の手法を取り入れるか、そもそも一度格納してそれを表示できるように変更しようと思うが、時間がかかりそうなので次回にしようと思う。
、