記事の推し
・測定値を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.");
}