こんにちは、ENGかぴです。
Arduino UNO R4 WiFi の ROM に HTML を保存し、ブラウザーからのアクセスに応答する WebServer を構築できます。続けて、そのページ上でセンサーの値をキャンバスを使ってグラフを描画する手順をまとめました。
下記記事では SDカードを使用してグラフ表示する方法をまとめています。
Arduino UNO R4 WiFiでセンサーの値をグラフ表示
本記事ではSDカードを使用せず、内部の ROM に HTML と JavaScript を保存し、キャンバスに自作でグラフ描写して動作確認を行いました。
センサーは MAKER SOIL MOISTURE SENSOR(Cytron製:秋月電子で購入)を使用しています。以下ではセンサーを土壌センサー、 Arduino UNO R4 WiFiをUNOR4-WiFiと表記します。Arduinoライブラリを使用して動作確認したことをまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
WebServerの全体構成

スマホは UNOR4-WiFi で実装したアクセスポイントに接続し、Google Chrome などのブラウザーを使用して「192.168.11.1」にアクセスします。
ブラウザーはクライアントとして動作し、最初に GET / HTTPを送信します。UNOR4-WiFi はこの文字列を識別して ROM に保存した index_html[] を読み込んで応答します。
次にブラウザーは UNOR4-WiFi から取得した HTML 内のスクリプトの読み込みのため、GET /main.jsを送信します。UNOR4-WiFi はこの文字列を識別して ROM に保存した main_Script[] を読み込んで応答します。その後、一定周期で GET /data が送信され、UNOR4-WiFi はセンサーのデータを JSON 形式で応答します。
以下では WiFiClient クラスを赤太文字で記載し、その他のメンバー関数は太文字で記載します。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
クライアントの接続と応答
WiFiClient client = server.available();
if (!client) return; //接続なしで抜ける
String req = client.readStringUntil('\r');
client.readStringUntil('\n');
if (req.startsWith("GET / HTTP") ) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println("Connection: close");
client.println();
client.print(index_html);
}
client.stop();
1行目の available() でクライアント(ブラウザー)からの接続を待機します。接続要求があると4行目に進みます。
最初にクライアントから HTTP データのリクエストが GET / HTTP/1.1 のようにバージョン付きで送信されます。
HTTP の行末は改行コードの CRLF(\r\n)が付くため、readStringUntil() で文字列の先頭から\r\nまで読み込んでリクエストの内容を判断します。readStringUntil() は1文字しか指定できないため、CRLF(\r\n)の2文字を一度に処理できません。そのため先に CR(\r)までを読み、5行目で LF(\n)を読んで LF が残らないようにしています。
7行目はリクエストから取得した文字列が GET / HTTP から始まっているかを startsWith() で判定します。
ブラウザーに最初にリクエストが正常に処理できたことを通知するため、HTTP のバージョン(HTTP/1.1)、ステータス(200 OK)を送ります。 200 OK はリクエストが正常に処理されたことを通知する応答です。
「Content-Type: text/html」は送るデータが HTML データであることを通知するヘッダーです。「Connection: close」はファイルを送信した後サーバー(UNOR4-WiFi)の接続を閉じる通知です。
UNOR4-WiFi で最後に stop() で切断をしますが、事前に接続を閉じる通知をしておくことでブラウザー側も切断処理にスムーズに進むため通信が安定します。ヘッダーの後は空行をいれてから ROM に保存した index_html[] を送信します。
送信後は close() でファイルを閉じ、クライアントの接続を stop() で切断します。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
ROMに保存するデータ

Arduino ファイルと HTML 関係のデータを区別して管理するためヘッダーファイルを生成し、ROM に保存する HTML と JavaScript を追加します。ヘッダーファイルを追加するかは任意ですが、ヘッダーファイルを生成しない場合は Arduino ファイルに ROM に保存するデータの配列を生成します。
「・・・」をクリックし、「新しいタブを追加」を選択すると新しいファイルの名前の入力するダイヤログが表示されるので html.h(任意のファイル名でよいですが、拡張子を.hにする)を入力し、OKを選択するとファイルが生成されます。拡張子を指定しない場合は Arduino ファイル(.ino)が生成されます。
const char index_html[] =
R"(
//ブラウザーで表示する内容を追加(ファイルの内容をそのまま貼り付け)
)";
const char index_html[] = R”( … )”; のように const を付けて定義すると、HTML データを ROM に格納できます。ブラウザーに返す HTML は、この index_html[] にそのまま貼り付けて保存します。
HTML や JavaScript をエスケープせずに記述するために、C++ の Raw String Literal(R”(…)”)を使用します。R”(…)” は括弧内の文字列をそのままの形で扱う構文で、ダブルクォート(”)をエスケープしたり、改行コード(CRLF)を意識したりする必要がありません。
そのため、動作確認済みの HTML や JavaScript ファイルの内容を、そのまま貼り付けて使うことができます。
HTML と JavaScript を作成する
HTML・JavaScript・JSON の生成方法は下記記事と同様なのでポイントを絞って説明します。
Arduino UNO R4 WiFiでセンサーの値をグラフ表示
HTML は JavaScript ファイルを読み込んでキャンバスにグラフを表示します。
<head>
<script src="main.js" defer></script>
</head>
<body>
<div style="height: 400px; width: 800px; margin: auto;">
<canvas id="soil-chart" width="800" height="400"></canvas>
</div>
</body>
2行目は HTML が JavaScript を読み込み、グラフ描画を行います。
5~7行目は、幅800px・高さ400pxの中央配置されたコンテナを作り、その中にグラフ描画用のキャンバスを配置しています。JavaScript で生成したグラフをid=”sail-chart” のキャンバスに描写します。
async function update() {
const res = await fetch('/data');
const obj = await res.json();
const times = obj.times;
const values = obj.values;
document.getElementById("latest-time").innerHTML = times[times.length -1];
document.getElementById("latest-value").innerHTML = values[values.length -1];
draw(values, times);
}
update()はfetch(“/data”)で UNOR4-WiFi に接続し、JSON を取得します。取得した JSON の times と values の値を HTML 内の id=”latest-time” と “latest-value” の要素に表示します。(7~8行目)
fetch() は JavaScript の標準関数で、指定した URL に GET リクエストを送り、UNOR4-WiFi(サーバー)から応答(レスポンス)を受け取るまでを自動で行います。2行目の await fetch(“/data”) は通信で JSON を取得するのを待って次の行に進みます。
3行目の await res.json() で JSON を取得し、4~5行目で JSON を構成している times と values に展開しています。times はセンサーから取得したときの RTC 時刻で values はセンサーの値です。これらのデータは最新のデータの更新及びグラフ描写に使用します。
9行目の draw() はグラフの描写を行います。引数に times(RTC時刻)と values(センサーの値)を指定しています。
キャンバスにグラフを描写する

HTML のキャンバスは、左端の座標が(0, 0)になります。黒線で示すようにX座標は右へ行くほど+Xになり、Y座標は下へ行くほど+Yになります。いつもロガーで見ているようなグラフと違ってY座標の向きが反対になります。
グラフは赤枠の部分に描写していきます。グラフの座標はグラフの原点を基準にするとX座標の向きはキャンバスと同様ですが、Y座標の向きは上へ行くほど+Yになるようにします。
グラフ描写領域を生成する
グラフの軸や文字がはみ出さないようにキャンバスの内側に余白(LEFT / RIGHT / TOP / BOTTOM)をとり描写領域とします。赤い枠線が描写領域になります。
赤枠の左下の原点の座標は(LEFT, H – BOTTOM)になり、これがグラフの原点の( 0, 0)に相当します。
const c = document.getElementById("soil-chart");
const ctx = c.getContext("2d");
ctx.clearRect(0, 0, W, H);
// ---- 枠 ----
ctx.strokeStyle = "#000";
ctx.strokeRect(LEFT, TOP, W - LEFT - RIGHT, H - TOP - BOTTOM);
ctx.fillStyle = "#000";
ctx.font = "20px sans-serif";
1行目は HTML 内にある id=”soil-chart”のキャンバスを取得します。 この戻り値を使って描画コンテキスト(ctx)を取得し、グラフの描画を行います。
2行目はキャンバスに 2D 描画を行うためのコンテキストを取得します。 戻り値の ctx を使って線を描いたり文字を書いたりします。
clearRect() は左上 (0,0) から幅 W、高さ H のキャンバス全体を一度クリアして前回の描画をクリアします。
strokeStyle は線を描くときの色を指定するプロパティです。”#000″ は黒を表す16信カラーコードで、枠線や軸などを描く際に使用します。ブラウザーで依存になりますが “black” でも黒色になります。
strokeRect() は指定した位置から枠線の四角形を書くメソッドです。第1引数と第2引数で指定した座標から第3引数に幅、第4引数に高さを指定します。例では LEFT / RIGHT / TOP / BOTTOM の余白を除いた範囲の枠を描写しています。
fillStyle は文字の色を指定するプロパティです。例では黒色を指定しています。font は文字のフォントとサイズを指定します。例では20pxの大きさでサンセリフ体を指定しています。サンセリフ体を指定しているのはブラウザーに最適なフォントを選択させるためです。この 2行は、軸ラベルや値を描く際の文字に使用されます。
X座標の変換(データ数で均等割り)
const N = times.length;
for (let i = 0; i < N; i += Math.floor(N / 5)) {
const x = LEFT + i * ((W - LEFT - RIGHT) / (N - 1));
ctx.fillStyle = "#000";
ctx.fillText(times[i], x - 20, H - 10);
}
最初に times に入っているデータ数を取得し、N に保存します。 グラフの X 軸の時刻を表示するための座標を計算する際に使用します。
時刻をデータ数だけ表示すると入らないので N/5(floorで切り捨て)だけ時刻を表示します。今回はデータ数60個に対して等間隔で5箇所の目盛りを描くように調整しています。
X 軸方向はデータ数 N に対してプロットが均等になるよう並べます。描画領域の幅は(W – LEFT – RIGHT)になるのでこれを(N – 1)で割り、1データあたりの X 方向の間隔を求めます。LEFT + i * 1データあたりの X 方向の間隔で i 番目のデータが均等になる X 座標を算出しています。
fillStyle プロパティで文字の色を黒にし、fillText() で時刻を表示します。第1引数に時刻の文字列を指定し、第2引数に X 座標、第3引数に Y 座標を指定します。例で x – 20 にしているのは文字幅のぶんだけ左にずらすためです。同様に H- 10 はキャンバス下側にずらすためです。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
Y座標の変換
const maxV = 1000;
const minV = 0;
const steps = 5;
for (let i = 0; i <= steps; i++) {
const v = minV + (maxV - minV) * (i / steps);
const y = H - BOTTOM - (i / steps) * (H - TOP - BOTTOM);
ctx.fillText(v.toFixed(0), LEFT - 50, y + 7);
}
5行目は 最小値から最大値の値を steps で分割し、均等な間隔で目盛りを配置するためのループです。今回は 0 ~1000 までを等間隔で目盛りを描くように調整しています。
6行目の ( i / steps ) によって 0〜1 の割合を作り、 その割合に応じて minV から maxV までを線形に補間した値 v を求めています。 これが目盛りに表示する数値になります。
7行目は描画領域の高さ(H – TOP – BOTTOM )を steps で均等に割り、 i の割合に応じて上から下へ均等に並ぶ Y 座標を計算しています。H – BOTTOM は描画領域の下端で、そこから上方向へ目盛りを配置します。
fillText() で目盛りを表示します。計算した値 v を toFixed(0) で整数に丸めて、 目盛りの左側に描画します。例で LEFT – 50 と y + 7 としているのは、文字の高さのぶんだけ位置を微調整するためです。
折れ線を描写する
ctx.strokeStyle = "#00f";
ctx.beginPath();
for (let i = 0; i < N; i++) {
const x = LEFT + i * ((W - LEFT - RIGHT) / (N - 1));
const y = H - BOTTOM - ((values[i] - minV) / (maxV - minV)) * (H - TOP - BOTTOM);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
折れ線を青色で描くため strokeStyle を青(#00f)にしています。beginPath() は描画パスをクリアして次の線を書き始めるときに使用します。これにより前に描いた枠線やグリッド線など混ざらず折れ線専用の描写ができます。
データ数 N 回ループし、各データ点をキャンバスの座標に変換して線でつないで折れ線グラフを描写します。
5行目は X 座標を計算しています。これは X 座標の変換(データ数で均等割り)で説明した内容と同じです。6行目は Y 座標を計算しています。これは Y 座標の変換で説明した内容と同じです。
8~9行目で折れ線グラフを描写します。最初の点(i = 0)は moveTo() で線の開始位置を指定します。2点目以降は lineTo() で前の点から線を引きます。これを繰り返すことで折れ線として連続的につながるようになります。
最後に stroke() でこれまでのパス( moveTo() 及び lineTo() の結果)をキャンバスに描写します。
動作確認

土壌センサーを UNOR4-WiFi の A0 ピンと7ピンに配線しています。DIS ピンはセンサーの測定を操作するもので、LOW にすると測定が有効になり HIGH にすると測定を停止します。IOT モジュールなど省電力で動作させたい場合に使用すると効果的です。本記事では常に測定できるように LOW にしています。
UNOR4-WiFi の電源を入れると土壌センサーの測定とクライアントからの接続を待機します。
RTC 時刻は Arduino UNO R4 WiFiのRTCを使用する で作成した自作の電文で変更できます。例えば RTC2026-05-25-01-20-30:30 の文字列を送信すると 2026年5月25日(月)20時30分30秒 になります。
スマホでアクセスポイントに接続し、Chrome などのブラウザーで「192.168.11.1」を入力して接続します。この様子をシリアルモニターで確認しました。

ブラウザーから要求を受けると HTTP/1.1 を受信します。その後 main.js HTTP/1.1 を受信しています。/data HTTP/1.1 は JSON の送信ですが、HTML データの送信から JSON までは 約500ms で表示できていることが分かります。

UNOR4-WiFi で生成した JSON が展開されてブラウザーに最新のデータとグラフが表示されています。
グラフが更新される様子を確認するため、土壌センサーのプローブに濡れたスポンジを当てると Value が500付近の値になりスポンジを外すと700付近の値に戻っている様子が確認できました。
グラフの時間を確認すると15秒から18秒と誤差が出ています。これは1秒ごとにセンサーからデータを取得して計算していますが JSON の処理やWiFi通信などのタスクが重なると測定タイミングにずれが生じたためです。
本記事ではグラフの更新を確認するため、土壌センサーの測定タイミングを1秒周期にしています。土壌の水分は頻繁に変化しないことを考慮すると、測定タイミングは10分や1時間で十分だと思います。測定タイミングの周期が長くなると時間の誤差は気にならなくなります。
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
UNOR4-WiFiのソースコード:
#include "RTC.h"
#include "WiFiS3.h"
#include "html.h"
#define SOIL_DIS 7
#define MEAS_MAX 8
#define TIM_MEAS 100
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define TIM_RTC 100
#define RTC_READ 100
#define RING_SZ 128
#define TIM_RX_WAIT 5 //50ms
#define OFFSET 3
#define CHART_SZ 60
struct DIS_MEAN{
uint8_t wp;
uint32_t buf[MEAS_MAX];
uint32_t humid;
};
struct RING_MNG{
uint8_t wp;
uint8_t rp;
uint8_t buf[RING_SZ];
};
typedef struct{
uint16_t wp;
uint16_t rp;
RTCTime t[CHART_SZ];
uint32_t time[CHART_SZ];
uint32_t value[CHART_SZ];
}DATA_RING;
uint32_t beforetimCnt = millis();
int16_t timdataget;
int32_t timsave;
DIS_MEAN soil;
int16_t timrtc;
int16_t timrcv = TIME_OFF;
char datestr[32];
char str[] = {"2025/09/22-00-21:30:30"};
char week[] = {"sun"};
uint8_t Rcvdata[sizeof(str) + OFFSET];
RING_MNG rcv;
RTCTime now;
DATA_RING meas;
int status = WL_IDLE_STATUS;
WiFiServer server(80);
const char *ssid = "EngKapi1"; //SSID
const char *pass = "22223333"; //password
const IPAddress ip(192,168,11,1); //IPアドレス
void mainTimer(void);
void mainApp(void);
void mainRtc(void);
void maiWifi(void);
void rcvdatechk(void);
void ReadPointerAdd(void);
bool chkdata(void);
void sendJson(WiFiClient &client);
void setup() {
Serial.begin(115200);
pinMode(SOIL_DIS,OUTPUT);
RTC.begin();
RTCTime init(3, Month::MAY, 2026, 9, 0, 30, DayOfWeek::SUNDAY, SaveLight::SAVING_TIME_INACTIVE );
RTC.setTime(init);
delay(500);
Serial.println("Init Start!");
for( uint8_t i=0; i < 10; i++ ){
timdataget = TIME_UP;
timrtc = TIME_UP;
mainRtc();
mainApp();
delay(1000);
}
WiFi.config(ip);
status = WiFi.beginAP(ssid, pass);
if (status != WL_AP_LISTENING) {
Serial.println("Creating access point failed");
// don't continue
while (true);
}
server.begin();
Serial.println("Init End!");
}
void loop() {
mainTimer();
mainApp();
mainWifi();
mainRtc();
rcvdatechk();
}
void mainWifi(void){
WiFiClient client = server.available();
if (!client) return;
String req = client.readStringUntil('\r');
client.readStringUntil('\n');
Serial.println(req);
if (req.startsWith("GET / HTTP")) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println("Connection: close");
client.println();
client.print(index_html);
}
else if( req.startsWith("GET /data") ){
//JSONファイルを送信
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: application/json");
client.println("Connection: close");
client.println();
sendJson(client);
}
else if (req.startsWith("GET /main.js") ) {
//JavaScriptファイルの読み出し
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: application/javascript");
client.println("Connection: close");
client.println();
client.print(main_Script);
}
else {
client.println("HTTP/1.1 404 Not Found\r\n");
}
client.stop();
}
/* JSONデータの生成 */
void sendJson(WiFiClient &client){
int16_t rp;
uint16_t i;
char buf[6];
String str="";
rp = meas.wp; //wp更新後なので最古のデータとなる
client.print("{\"times\":[");
for( i = 0; i < CHART_SZ; i++){
sprintf(buf, "%02d:%02d", meas.t[rp].getMinutes(), meas.t[rp].getSeconds());
str = "\"" + String(buf) + "\"";
client.print(str);
if( ++rp >= CHART_SZ ){
rp = 0;
}
if (i < CHART_SZ - 1) client.print(",");
}
rp = meas.wp; //wp更新後なので最古のデータとなる
client.print("],\"values\":[");
for( i = 0; i < CHART_SZ; i++){
client.print(meas.value[rp]);
if( ++rp >= CHART_SZ ){
rp = 0;
}
if (i < CHART_SZ - 1) client.print(",");
}
client.print("]}");
}
void mainApp(void){
uint16_t sum;
uint16_t i;
if( timdataget == TIME_UP){
timdataget = TIM_MEAS;
soil.buf[soil.wp] = analogRead(A0);
if(++soil.wp >= MEAS_MAX){
soil.wp = 0;
}
sum = 0;
for( i =0; i < MEAS_MAX; i++){
sum += soil.buf[i];
}
soil.humid = sum >> 3;
meas.t[meas.wp] = now;
//meas.time[meas.wp] = now.getUnixTime();
meas.value[meas.wp] = soil.humid;
if( ++meas.wp >= CHART_SZ ){
meas.wp = 0;
}
}
}
/* タイマ管理 */
void mainTimer(void){
if ( millis() - beforetimCnt >= BASE_CNT ){
beforetimCnt = millis();
if( timdataget > TIME_UP ){
--timdataget;
}
if( timrtc > TIME_UP ){
--timrtc;
}
if( timrcv > TIME_UP ){
timrcv--;
}
}
}
/* メイン処理関数 */
void mainRtc(void){
while(Serial.available()){
rcv.buf[rcv.wp] = Serial.read();
if( ++rcv.wp >= RING_SZ ){
rcv.wp = 0;
}
}
if( timrtc == TIME_UP ){
timrtc = TIM_RTC;
RTC.getTime(now);
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
}
/* 受信データから時刻データを生成 */
void rcvdatechk(void){
int8_t rxsz;
uint8_t allsz;
uint8_t rp = rcv.rp;
uint8_t i;
uint8_t dat[3];
if( timrcv == TIME_UP){
timrcv = TIME_OFF;
ReadPointerAdd();
}
rxsz = rcv.wp - rcv.rp; //受信データ数の算出
if( rxsz < 0 ){
rxsz = rxsz + sizeof(rcv.buf);
}
if( rxsz == 0 ){
timrcv = TIME_OFF;
}
else{
if( timrcv == TIME_OFF ){
timrcv = TIM_RX_WAIT;
}
if( rxsz >= 3 ){
for( i = 0; i < 3; i++){//データサイズ算出のため仮おき
dat[i] = rcv.buf[ rp ];
if(++rp >= sizeof(rcv.buf) ) rp = 0;
}
if( dat[0] == 'R' && dat[1] == 'T' && dat[2] == 'C'){
allsz = sizeof(str) + OFFSET - 1;
if( rxsz >= allsz){
timrcv = TIME_OFF;
for(i=0; i < sizeof(Rcvdata); i++){
Rcvdata[i] = 0;
}
for( i=0; i < allsz; i++ ){
Rcvdata[i] = rcv.buf[rcv.rp];
ReadPointerAdd();
}
if( chkdata() == false ){
Serial.println("RTCSET NG!");
}
}
}
}
}
}
/* 読み込み位置の更新 */
void ReadPointerAdd(void){
if(++rcv.rp >= sizeof(rcv.buf) ){
rcv.rp = 0;
}
}
/* 時刻のチェック */
bool chkdata(void){
int d;
Month m;
int y;
int h;
int min;
int s;
DayOfWeek dotw;
SaveLight su;
bool ret = false;
//RTC2025-08-10-00-12-00-00
y = strtol((const char*)&Rcvdata[3], NULL, 10);
m = (Month)(strtol((const char*)&Rcvdata[8], NULL, 10)-1);
d = strtol((const char*)&Rcvdata[11], NULL, 10);
dotw = (DayOfWeek)strtol((const char*)&Rcvdata[14], NULL, 10);
h = strtol((const char*)&Rcvdata[17], NULL, 10);
min = strtol((const char*)&Rcvdata[20], NULL, 10);
s = strtol((const char*)&Rcvdata[23], NULL, 10);
if( y >= 2000 && y < 2100 ){
if( m >= Month::JANUARY && m <= Month::DECEMBER ){
if( d >= 1 && d <= 31 ){
if( h >= 0 && h <= 23 ){
if( min >= 0 && min <=59 ){
if( s >= 0 && s <= 59 ){
if( dotw >= DayOfWeek::SUNDAY && dotw <= DayOfWeek::SATURDAY ){
RTCTime setT(d, m, y, h, min, s, dotw, SaveLight::SAVING_TIME_INACTIVE);
RTC.setTime(setT);
Serial.println("RTCSET OK!");
ret = true;
}
}
}
}
}
}
}
return ret;
}
html.hのソースコード:
const char index_html[] =
R"(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>土壌センサーの動作確認</title>
<!-- 自作の JavaScript(fetch + Chart.js 設定) -->
<script src="main.js" defer></script>
</head>
<body>
<h1>Maker soil Moisture sensor</h1>
<h2>最新のデータ
<br>時刻:
<code id="latest-time">----</code>
<br>Value:
<code id="latest-value">----</code>
</h2>
<div style="height: 400px; width: 800px; margin: auto;">
<canvas id="soil-chart" width="800" height="400"></canvas>
</div>
</body>
</html>
)";
const char main_Script[] =
R"(
async function update() {
const res = await fetch('/data');
const obj = await res.json();
const times = obj.times;
const values = obj.values;
document.getElementById("latest-time").innerHTML = times[times.length -1];
document.getElementById("latest-value").innerHTML = values[values.length -1];
draw(times, values);
}
function draw(times, values) {
const c = document.getElementById("soil-chart");
const ctx = c.getContext("2d");
const W = c.width;
const H = c.height;
// ---- 余白設定(フォント20px対応) ----
const LEFT = 60;
const RIGHT = 10;
const TOP = 10;
const BOTTOM = 50;
ctx.clearRect(0, 0, W, H);
// ---- 枠 ----
ctx.strokeStyle = "#000";
ctx.strokeRect(LEFT, TOP, W - LEFT - RIGHT, H - TOP - BOTTOM);
ctx.fillStyle = "#000";
ctx.font = "20px sans-serif";
// ---- X軸(時間) ----
const N = times.length;
for (let i = 0; i < N; i += Math.floor(N / 5)) {
const x = LEFT + i * ((W - LEFT - RIGHT) / (N - 1));
// グリッド線(縦)
ctx.strokeStyle = "#ccc";
ctx.beginPath();
ctx.moveTo(x, TOP);
ctx.lineTo(x, H - BOTTOM);
ctx.stroke();
// 時刻ラベル
ctx.fillStyle = "#000";
ctx.fillText(times[i], x - 20, H - 10);
}
// ---- 右端の時刻 ----
ctx.fillStyle = "#000";
ctx.fillText(times[N - 1], W - RIGHT - 40, H - 10);
// ---- Y軸目盛り ----
const maxV = 1000;
const minV = 0;
const steps = 5;
ctx.fillStyle = "#000";
ctx.font = "20px sans-serif";
for (let i = 0; i <= steps; i++) {
const v = minV + (maxV - minV) * (i / steps);
const y = H - BOTTOM - (i / steps) * (H - TOP - BOTTOM);
//グリッド線
ctx.strokeStyle = "#ccc";
ctx.beginPath();
ctx.moveTo(LEFT, y);
ctx.lineTo(W - RIGHT, y);
ctx.stroke();
// 目盛り値
ctx.fillText(v.toFixed(0), LEFT - 50, y + 7);
}
// ---- 折れ線 ----
ctx.strokeStyle = "#00f";
ctx.beginPath();
for (let i = 0; i < N; i++) {
const x = LEFT + i * ((W - LEFT - RIGHT) / (N - 1));
const y = H - BOTTOM - ((values[i] - minV) / (maxV - minV)) * (H - TOP - BOTTOM);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
}
update();
setInterval(update, 5000);
)";
Arduinoファイルにhtml.hを追加して貼り付けて使用してください。Arduinoファイルでhtml.hをインクルードすると使用できます。
html.hを作成しない場合はArduinoファイルに配列を張り付けて使用してください。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:外資系・IT業界などハイクラスの転職に強い【AXIS Agent】
PR:キャリア形成でお悩みのあなたへ!初回無料相談で相談できる[coachee]
最後まで、読んでいただきありがとうございました。

Chart.jsを使用すると最初のライブラリの読み込みに時間が必要ですが、グラフの表示の自由度が高く様々なグラフを描写できます。今回はグラフの描写は最低限ですが読み込みがスムーズになるメリットがあります。また、ROM に HTML や JavaScript をいれているので SDカードの動作不良の影響を受けないメリットがあります。