記事の内容
- ESP32にブラウザからアクセスして値を表示させる方法
- センサーの値を用いてChart.jsでのグラフ化
- 詰まったところ解説
こんな人向け
- センサーで取得した値をブラウザで表示させたい
- センサーの値をブラウザ上でリアルタイムにグラフ化したい
目次
つくったもの
外観、ブラウザ表示
パーツの総額は50元くらい
外観
ブラウザ上の表示 グラフの外観は追求の余地あり
リンク
リンク
リンク
回路図
コード
main.cpp
#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
// sensor
#include "SSD1306.h" //SSD1306 OELD
#include "DFRobot_SHT20.h" //SHT20 temp + humd sensor
#include "HCSR04.h" //HC-SR04 distance sensor
//GIPOpin setting
int sadPin = 4;
int sclPin = 15;
int triggerPin = 12; //for HC-SR04
int echoPin = 13; //for HC-SR04
//instance
SSD1306 display(0x3c, sadPin, sclPin);
DFRobot_SHT20 sht20;
UltraSonicDistanceSensor distanceSensor(triggerPin, echoPin);
// WebServer
WebServer webServer(80); // 80番ポート
WebSocketsServer webSocket = WebSocketsServer(81); // 81番ポート
const char *ssid = "SSID";
const char *password = "Password";
const IPAddress ip(192, 168, 1, 155); //fixed IP adress
const IPAddress gateway(192, 168, 1, 1);
const IPAddress subnet(255, 255, 255, 0);
// サンプリング周期
elapsedMillis sensorElapsed;
const unsigned long DELAY = 1000; // ms
// Webコンテンツのイベントハンドラ
// https://garretlab.web.fc2.com/arduino/esp32/examples/WebServer/HelloServer.html
// root webServer.on("/", handleRoot);
// othre handleNotFound で処理する.
// URI で指定されるファイルがあればクライアントに転送.なければ404 エラー
// MIME type
String getContentType(String filename)
{
if (filename.endsWith(".html") || filename.endsWith(".htm"))
return "text/html";
else if (filename.endsWith(".css"))
return "text/css";
else if (filename.endsWith(".js"))
return "application/javascript";
else if (filename.endsWith(".png"))
return "image/png";
else if (filename.endsWith(".gif"))
return "image/gif";
else if (filename.endsWith(".jpg"))
return "image/jpeg";
else if (filename.endsWith(".csv"))
return "text/csv";
else
return "text/plain";
}
// transport SPIFSS file
bool handleFileRead(String path)
{
Serial.println("handleFileRead: trying to read " + path);
// パス指定されたファイルがあればクライアントに送信する
if (path.endsWith("/"))
path += "index.html";
String contentType = getContentType(path);
if (SPIFFS.exists(path))
{
Serial.println("handleFileRead: sending " + path);
File file = SPIFFS.open(path, "r");
webServer.streamFile(file, contentType);
file.close();
Serial.println("handleFileRead: sent " + path);
return true;
}
else
{
Serial.println("handleFileRead: 404 not found");
webServer.send(404, "text/plain", "ESP: 404 not found");
return false;
}
}
void handleRoot()
{
String s = INDEX_HTML; // index_html.hより読み込み
webServer.send(200, "text/html", s);
}
void handleNotFound()
{
if (!handleFileRead(webServer.uri()))
{
Serial.println("404 not found");
webServer.send(404, "text/plain", "File not found");
}
}
String ipToString(uint32_t ip)
{
/*
WIFIのIPを変換する
192.168.0.13の場合
String(WIFI.localIP)=218147008
2進数変換:00001101 00000000 10101000 11000000
8bitずつ10進数変換:13 0 168 192
ipのポインタから該当箇所を抽出し、シフト演算で除去。ゴリラ技。
*/
String result = "";
result += String((ip & 0xFF), 10);
result += ".";
result += String((ip & 0xFF00) >> 8, 10);
result += ".";
result += String((ip & 0xFF0000) >> 16, 10);
result += ".";
result += String((ip & 0xFF000000) >> 24, 10);
return result;
}
void sensor_init()
{
//SSD1306 Setup
display.init(); //初期化
display.setFont(ArialMT_Plain_16); //フォント設定
display.drawString(0, 0, "IP:" + ipToString(WiFi.localIP())); //IP表示
display.display(); //描写
//SHT20 setup
sht20.initSHT20();
delay(100);
sht20.checkSHT20();
}
// センサのデータ(JSON形式)
// 生文字列R"***(この中に生文字列が書ける)***"
// https://cpprefjp.github.io/lang/cpp11/raw_string_literals.html
const char SENSOR_JSON[] PROGMEM = R"=====({"temp":%.1f,"humd":%.1f,"dist":%.1f})=====";
// データの更新
void sensor_loop()
{
//SHT20
float humd = sht20.readHumidity(); // Read Humidity
float temp = sht20.readTemperature(); // Read Temperature
//HC-SR04
double distance = distanceSensor.measureDistanceCm();
display.clear();
//display.setFont(ArialMT_Plain_16); //フォント設定
display.drawString(0, 0, "IP :" + ipToString(WiFi.localIP())); //1行目
display.drawString(0, 16, "Temp :" + String(temp)); //2行目
display.drawString(0, 32, "Humd :" + String(humd)); //3行目
display.drawString(0, 48, "Dist :" + String(distance)); //4行目
display.display();
// 送信データ作成
char payload[100];
snprintf_P(payload, sizeof(payload), SENSOR_JSON, temp, humd, distance);
// WebSocketでデータ送信
webSocket.broadcastTXT(payload, strlen(payload));
Serial.println(payload);
}
void setup()
{
// シリアル通信設定
Serial.begin(115200);
delay(100);
Serial.println("Start setup");
// Wi-Fi設定
Serial.println("wifi setup start");
WiFi.disconnect(true);
WiFi.config(ip, gateway, subnet);
WiFi.begin(ssid, password);
Serial.println("wifi setup end");
// センサの初期化
Serial.println("sensor setup start");
sensor_init();
Serial.println("sensor setup end");
// Webサーバーのコンテンツ設定
// favicon.ico, Chart.min.jsは dataフォルダ内に配置
Serial.println("webserver setup start");
SPIFFS.begin();
webServer.serveStatic("/favicon.ico", SPIFFS, "/favicon.ico");
webServer.serveStatic("/Chart.min.js", SPIFFS, "/Chart.min.js");
webServer.on("/", handleRoot);
webServer.onNotFound(handleNotFound);
webServer.begin();
Serial.println("webserver setup end");
// WebSocketサーバー開始
webSocket.begin();
Serial.println("websoket setup end");
}
void loop(void)
{
webSocket.loop();
webServer.handleClient();
// 一定の周期でセンシング
if (sensorElapsed > DELAY)
{
sensorElapsed = 0;
sensor_loop();
}
}
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 (time)
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 dist
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/');
// JSON data example {"temp":26.9,"humd":64.9,"dist":42.2}
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)["dist"];
// 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>
)=====";
dataフォルダに設置するファイル
Chart.min.js:下記URLの最下部にあります。
GitHub
Release Version 2.8.0 · chartjs/Chart.js
Documentation: http://www.chartjs.org/docs/2.8.0/
Deprecations
#5868 Deprecate Chart.{Type} classes
#6022 Deprecate configMerge and scaleMerge helpers
Enhance...
favicon.ico:下記URLで適当に作って下さい。
ファビコン作成。favicon.ico 無料...
ファビコン作成。favicon.ico 無料で半透過マルチアイコンが作れます。
ファビコン(ファブアイコン)faviconを作ろう!。16x16と32x32と48x48ピクセルのマルチアイコンが簡単に作れます。作成したファビコンの画像はプレビューで確認できます。プ...
dataフォルダの書き込み方法
Arduino IDEを使用する場合
あわせて読みたい
ESP32でHTMLをファイルシステムに保存する - テクノベインズ ブログ
ESP32(ESP-WROOM-02)のSPIFFS機能を使ってHTML,CSS,JSファイルをファイルシステムに保存する方法
PratformIOを使用する場合
* いしのなかにいる * :工兵のラ...
PlatformIOでSPIFFSにファイルアップロードできる模様 | * いしのなかにいる * :工兵のラボ
なんかPlatformIOにSPIFFSへのファイルアップロード機能があるみたいなのでちょっとググってみました。 ESP32 with VS Code and PlatformI...
コードの挙動
- Web ServerとWeb Sockets Serverを立ち上げる。
- Web Sockets Serverにてセンサーの値を1秒ごとに送信。(OELDにも表示させる)
- ブラウザでアクセス時WebServerにて、「index.html.c」に記載しているhtmlを表示させる。
- ブラウザ上にてWeb Socketsで受け取ったデータを用い、Chart.jsでグラフをリアルタイムで描写。
参考サイト様
AI / IoT センサのしくみを知ろう
ESP32: Webサーバ上でリアルタイムグラフ表示(Chart.js) - AI / IoT センサのしくみを知ろう
ESP32:Chart.jsを使ったグラフ表示 公開講座(春夏)「AI/IoTセンサのしくみを知ろう」の補足です。 ESP32のウェブサーバ上で,センサで取得したデータをグラフ表示します。...
詰まったところ
C++
他言語と比べ、記述ルールが厳密
上からしか読み込まないので、関数の宣言は使う前に行う必要あり。
ポインタや変数の使い回しは理解に時間がかかりそうだったので、今回は回避した。
R”===()===”
エスケープ文字を不要とする「生文字列」を表現。
htmlやJSON等、記号を多く含む文字列を用いる場合に用いる。
===の箇所は左右対称になっていればOKなので、 「R”***()***”」 とかでもOK。
cpprefjp - C++日本語リファレンス
生文字列リテラル - cpprefjp C++日本語リファレンス
`R`プレフィックスを付けた文字列リテラル内の丸カッコ`( )`で囲まれた部分は、エスケープシーケンスが無視される。この機能を「生文字列リテラル (Raw string literals)」...
snprintf_P
snprintf_P (変数名, MAXの文字数, フォーマットの文字列, 代入する変数1[, 代入する変数2…])
第3引数からはpythonでいうformatとだいたい同じ。
PROGMEM
変数を保管するメモリには限りがあるので、比較的広い領域のメモリを使う際に使用する。らしい。
jumbleat
PROGMEMを使う
Arduinoでは、変数の数値はSRAMにキープされます。
PlatformIO
platformio-ide.showhome not found
PratformIOのVerを2.3.3から2.3.2に戻すことで解決。
自動アップデートによりバージョンアップが行なわれたのが原因でした。
異常が出た場合、まずはGithub等の本家を確認する癖をつけたほうがいいね。
GitHub
GitHub - platformio/platformio-vscode-ide: PlatformIO IDE for VSCode: The next generation integrated...
PlatformIO IDE for VSCode: The next generation integrated development environment for IoT - GitHub - platformio/platformio-vscode-ide: PlatformIO IDE for VSCode...
これを機に、開発環境のアップデートを手動に変更。
マゴトログ シュミニイキル
VisualStudioCodeの自動更新を止める方法
「Visual Studio Code」の自動更新をキャンセルする手順を備忘録的に投稿しておきます。
dataフォルダの書き込みボタン
選択項目が何処にあるのか理解できなかった。
わかりにくいね😥
まとめ
先人のネット記事や知見、ライブラリを組み合わせるだけで作れてしまった。知の巨人に感謝。
次は複数のWeb Serverから、各センサーのデータをcsv形式で収拾するロガーを作成予定。
データ取得時間の同期、データ保存領域、言語、画面出力等を考えると、RaspberryPiで作るのが良いかな?