ブラウザ上でのリアルタイムグラフ【Chart.js+chartjs-plugin-streaming】

はじめに

センサーの測定値を見える化したい場合、

ブラウザ上で、オシロスコープのロールモードのようにリアルタイムで表示されるグラフができないか、調べてみましたのでご紹介します。

 

システムの構成

  • 表示データ:ESP32のアナログピンの電圧値
  • ネットワーク:ESP32のsoftAPモードでのローカル環境

 

 

ライブラリ

いろいろと探しましたが、(※)

「Chart.js」とプラグイン「chartjs-plugin-streaming」を使ってやってみました。

古いバージョン構成ではなかなか思うようにならなかったので、

全て最新バージョンを使うことにします。

まず、グラフ表示には「Chart.js (3.9.1)」を使用します。

そして、そのプラグインである「chartjs-plugin-streaming (2.0)」で

リアルタイムストリーミング表示をおこないます。

さらに、このプラグインに必要な「luxon (3.0.3)」とアダプタの「chartjs-adapter-luxon」を使います。

ローカル環境で動作させたいので、以下の必要なファイルをダウンロードしておきます。

(ダウンロード元はcdnjs.com やgithubで探してみてください。)

  • chart.min.js
  • luxon.min.js
  • chartjs-adapter-luxon.min.js
  • chartjs-plugin-streaming.min.js

 

(※参考)

ESP32: Webサーバ上でリアルタイムグラフ表示(Chart.js)

Chart.js でリアルタイムストリーミングデータをグラフ化

 

プログラム

Arduino IDEで以下のようなプログラムを作成・保存して、ESP32に書き込みます。

 

・「esp32_chart.ino」

/*************************************************
 * Include
**************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WiFi.h>
#include <Wire.h>
#include <WebServer.h>
#include "FS.h"
#include "SPIFFS.h"
#include <WebSocketsServer.h>
#include "ArduinoJson.h"

// SSIDとPASSWORD
const char* esp32ssid     = "evo_esp32-011";
const char* esp32pass     = "1234567890";
IPAddress apip(192,168,1,100);  // IPアドレス

WebServer server(80);  // webサーバー port80
WebSocketsServer webSocket = WebSocketsServer(8001); // port8001

String html_chart;
StaticJsonDocument<100> sendData;

#define AIN_PIN 32  // 測定端子


/*************************************************
 * Function
**************************************************/
void handleNotFound(void)
{
  String message = F("File Not Found\n\n");
  message += F("URI: ");
  message += server.uri();
  message += F("\nMethod: ");
  message += (server.method() == HTTP_GET)?F("GET"):F("POST");
  message += F("\nArguments: ");
  message += server.args();
  message += F("\n");
  
  for(uint8_t i=0; i<server.args(); i++){
    message += " " + server.argName(i) + F(": ") + server.arg(i) + F("\n");
  }
  Serial.println(message);
  server.send(404, F("text/plain"), message);
}


void handleRoot()
{
  int res = 0;
  
  res = rd_SPIFFS("/index.html", html_chart);
  server.sendHeader("Connection", "close");
  server.send(200, "text/html", html_chart);
}


int rd_SPIFFS(String fname, String &sname)
{
  File fp = SPIFFS.open(fname, "r");
  if(!fp){
    return -1;
  }
  else{
    sname = fp.readString();
    fp.close();
  }
  return 0;
}

/*************************************************
 * Setup
**************************************************/
void setup()
{
  Serial.begin(115200);
  SPIFFS.begin(true);

  // softAPモード
  WiFi.softAP(esp32ssid, esp32pass);
  delay(100);
  WiFi.softAPConfig(apip,apip,IPAddress(255,255,255,0));
  IPAddress myIP = WiFi.softAPIP();
  Serial.println(myIP);
  // Javascriptファイルのホスティング
server.serveStatic("/luxon.min.js", SPIFFS, "/luxon.min.js");
server.serveStatic("/chart.min.js", SPIFFS, "/chart.min.js");
server.serveStatic("/chartjs-lux.min.js", SPIFFS, "/chartjs-lux.min.js");
server.serveStatic("/chartjs-str.min.js", SPIFFS, "/chartjs-str.min.js");
// リクエストに対するハンドラー
  server.on("/chart", HTTP_GET, handleRoot);
  server.onNotFound(handleNotFound);

  server.begin();
  webSocket.begin();
}


/*************************************************
 * Main Loop
**************************************************/
void loop()
{
  int vol;
  char payload[64];
  char sdata[6];
  
  webSocket.loop();
  server.handleClient();
  
  unsigned long currentMillis = millis();
  vol = analogRead(AIN_PIN);  // ピン電圧取得
  float fdata = vol * 3.0 / 4095.0;
  Serial.println(fdata);
  sprintf(sdata, "%.2f", fdata);
  sendData.clear();
  sendData["msec"] = currentMillis;
  sendData["vol1"] = sdata;
  serializeJson(sendData, payload);  // JSONデータ
  // websocketでデータ送信
  webSocket.broadcastTXT(payload, strlen(payload));
  Serial.println(payload);
  
  delay(100);
}

 

 

「data」フォルダの中にダウンロードした4つのファイル(短い名前に変えています)と、以下の内容で作成・保存した「index.html」を

「ESP32 Sketch Data Upload」を実行して書き込みます。

 

・「index.html」

<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>ESP32 chart</title>
</head>
<body>
<div style="text-align:center;"><b>ESP32 Aport-V</b></div>
<div style="width:850px; margin:0 auto;">
  <canvas id="esp32Chart"></canvas>
</div>
<br><br>
  <script src="/luxon.min.js"></script>
  <script src="/chart.min.js"></script>
  <script src="/chartjs-lux.min.js"></script>
  <script src="/chartjs-str.min.js"></script>

  <script>
    var ctx = document.getElementById("esp32Chart").getContext("2d");
    var chart = new Chart(ctx, {
      type: "line",  // 折れ線グラフ
      data: {
        datasets: [
          {
            data: [],
            label: "V",
            radius: 0,  // 点なし
            tension: 0.1,  // 曲率
            borderColor : "red",
          },
        ],
      },
      options: {
        plugins: {
          streaming: {
            duration: 10000,
          },
          legend: {
            display: false,  // 凡例なし
          },
        },
        scales: {
          x: {
            type: "realtime",  // realtime streaming
            realtime: {
              duration: 10000,  // 表示時間10sec
              refresh: 100,  // 100ms毎
              delay: 100,  // 遅延100ms

              onRefresh: function(chart) {
                chart.options.scales.y.max = 3.5;  // 縦軸の最大値
                chart.options.scales.y.min = -0.5;  // 縦軸の最小値
                chart.data.datasets.forEach(function(dataset) {
                  dataset.data.push({
                    x: Date.now(),
                    y: (data_x1)
                  });
                });
              }
            },
          },
        },
      },
    });


    var ws = new WebSocket("ws://192.168.1.100:8001/");

    // データ取得
    ws.onmessage = function(evt) {
      data_x0 = JSON.parse(evt.data)["msec"];
      data_x1 = JSON.parse(evt.data)["vol1"];
    };

    ws.onclose = function(evt) {
      console.log("ws: onclose");
      ws.close();
    };

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

実行結果

PCやスマホ等をESP32のアクセスポイントに接続し、Webブラウザから設定したIPアドレス(例: 192.168.123.45)にアクセスします。

今回はセンサー入力の代わりにボリュームをつなぎ、ぐりぐり回してみています。

100ms毎にデータ取得・更新していますが、それなりに描画されています。

 

おわりに

「Chart.js」とプラグイン「chartjs-plugin-streaming」で試してみて、ある程度希望した動作が実現できました。ただ、x軸のラベルがdatetimeで固定のようで、変更する方法がわかりませんでした。今回は検討までとなりましたが、手頃な案件があれば利用してみたいと思います。

その他、

「epoch.js」も試してみましたが、こちらは1秒毎の描画更新でありそれ以上早くする方法がわかりませんでした。

「smoothie.js」もリアルタイム表示できました。こちらはかなり高速化できそうですが、その分x,y軸ともラベルが表示されないなどの問題があります。

機会がありましたら、紹介してみたいと思います。