初めに
作ったもの
Reactを使って、リアルタイムにマイコン内の変数を表示するアプリ作ってみました。
動作はこんな感じ。
PCの画面に Counter Value: と書かれていますが、この数字が動いているのがReactアプリです。
Reactで作っているのでブラウザを常に更新しなくても、Javascriptが動的に値を変更してくれます。
ラグを測るために、マイコンのディスプレイにも同じ数字を表示しています。
作ったシステムの図
マイコンがwebサーバーとして、htmlファイルを配布し、それを受け取ったブラウザはそのhtmlファイルに埋め込まれたReactを動かします。
そのReactはマイコンからWebSocketで送られてきた値を受け取り、その値に応じてブラウザのDOM(ブラウザのレンダリング結果)を更新します。
詳しくはあとで説明します。
「簡単に」とは
ESP32でReactアプリを動かす方法は、結構他のサイトでも紹介されています。
しかし、gzipで圧縮などとすこし難しそうなことをしていたので、簡単にできないかと思い、
一つのhtmlファイルにReactを埋め込むことで実装してみました。
基本的なコードはReactでhtmlファイルを配信する方法と変わりません。
環境
マイコン:M5Stack TOUGH
開発環境:PlatformIO IDE
使用したPC: Windows11
Reactを動かしたブラウザ:Google Chrome(Windows11)
コードの解説
htmlページの配信
まずはhtmlページの配信をします。
Webサーバーについての解説はこの記事にお任せします。
今回私はこんな感じのコードで実装しました。
ESPAsyncWebserverというモジュールを使っています。
#include <Arduino.h>
#include <M5Unified.h>
#include <WiFi.h>
#include "ESPAsyncWebServer.h"
#include <SPIFFS.h> // SPIFFS(ファイルシステム)用のライブラリをインクルード
// WebServer server(80); // ポート80でWebサーバーを開始
AsyncWebServer server(80);
void setup()
{
M5.begin();
M5.Power.begin();
int i = 1;
Serial.begin(115200);
Serial.print("Connecting");
while (WiFi.status() != WL_CONNECTED)
{
if (i++ > 1)
{ // 10 seconds
Serial.println("Smart Config Start!");
WiFi.beginSmartConfig();
while (!WiFi.smartConfigDone())
{
delay(500);
Serial.print(".");
}
Serial.println("Smart Config Done!");
}
delay(500);
Serial.print(".");
}
Serial.println("WiFi Connected.");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
// SPIFFS(ファイルシステム)を初期化
if (SPIFFS.begin())
{
Serial.println("SPIFFS initialized.");
}
else
{
Serial.println("Error initializing SPIFFS.");
}
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(SPIFFS, "/index.html", String(), false); });
server.begin();
}
void loop()
{
}
htmlファイルはテキトーで構いません。
もはやこれ一行でも良いです。
<h1>Hello World!</h1>
他のファイルも見たい方のために、ここにソースコードを置いておきます。
esp32内のフラッシュメモリーにhtmlファイルを置いています。
PlatformIOでフラッシュメモリーを使うには、以下の記事をお読みください。
ちなみに、Upload Filesystem Image
というのはvscodeのアリさんマークを開いたときに出現するウィンドウの上側、
PROJECT TASKS/m5stack-core2/Platform/Upload Filesystem Image にあります。
Wifiの接続には smartConfig を使っています。
smartConfigとはなんぞやという方は以下のurlをご覧ください。
ソースコードを書きこめて、wifiに繋がるとシリアルモニタに以下のように表示されると思います。
このIP Address:
の後に表示されている値を、同じwifiに繋がっているスマホやPCのブラウザに打ち込んでアクセスすると、htmlファイルが確認できます。
Reactアプリの配信
今回はReactアプリをhtmlファイルに埋め込んで配信したいと思います。
普通、ReactアプリはexpressとかNext.jsとかnode.jsを使ったサーバーで配信するのですが、
実はhtmlに埋め込んで配信することもできます。
埋め込む代わりに機能も制限されますがね。
以下のページを参考にしてください。
ちょっと情報が古いのですが、まだ使えました。
今回はTypeScriptを使いましたが、Javascriptでも構いません。
この方法なら、main.cppを触る必要はないので、フラッシュメモリーに書き込んだhtmlファイルのみを編集していきます。
私はテキトーにこんなコードを書きました。
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>React tsx-esm-standalone</title>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script>
// typescript用のpresetを登録する
Babel.registerPreset('tsx', {
presets: [
[Babel.availablePresets['typescript'], // preset-typescriptを指定
{allExtensions: true, isTSX: true} // allExtensionsは、isTSX利用時に必要なためセット
]],
},
);
</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
import React from "https://cdn.skypack.dev/react@17";
import ReactDOM from "https://cdn.skypack.dev/react-dom@17";
const Button = () => {
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
ReactDOM.render(<Button />, document.getElementById('app'));
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>
ボタンを設置し、ボタンを押すと「You Clicked me!」というポップアップが出るという、Reactのチュートリアルにあるコードです。
全体のソースコードもここに置いて置きます。
これを先ほど同様にフラッシュメモリーに書き込むと、main.cppをいじらずともreactアプリを動かせます。
WebSocketでの値渡し
解説記事を書いておいてお恥ずかしいのですが、実は私あまりWebSocketは詳しくありません。
とあるツールをnode.jsで作ったときに一回触ったことがある程度です。
ましてや、esp32では一回もないです。
ということで、ChatGPT君に仕様を指示して書いてもらいました。なので、あまりきれいなコードではないことをお許しください。
以下が作ったコードです。
#include <Arduino.h>
#include <M5Unified.h>
#include <WiFi.h>
#include <WebSocketsServer.h>
#include "ESPAsyncWebServer.h"
#include <SPIFFS.h> // SPIFFS(ファイルシステム)用のライブラリをインクルード
const int webSocketPort = 81;
WebSocketsServer webSocket = WebSocketsServer(webSocketPort);
// WebServer server(80); // ポート80でWebサーバーを開始
AsyncWebServer server(80);
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
{
switch (type)
{
case WStype_CONNECTED:
Serial.printf("WebSocket client connected: %u\n", num);
break;
case WStype_TEXT:
// Handle text messages from WebSocket clients if needed
break;
case WStype_DISCONNECTED:
Serial.printf("WebSocket client disconnected: %u\n", num);
break;
}
}
void setup()
{
M5.begin();
M5.Power.begin();
int i = 1;
Serial.begin(115200);
Serial.print("Connecting");
while (WiFi.status() != WL_CONNECTED)
{
if (i++ > 1)
{ // 10 seconds
Serial.println("Smart Config Start!");
WiFi.beginSmartConfig();
while (!WiFi.smartConfigDone())
{
delay(500);
Serial.print(".");
}
Serial.println("Smart Config Done!");
}
delay(500);
Serial.print(".");
}
Serial.println("WiFi Connected.");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
// SPIFFS(ファイルシステム)を初期化
if (SPIFFS.begin())
{
Serial.println("SPIFFS initialized.");
}
else
{
Serial.println("Error initializing SPIFFS.");
}
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(SPIFFS, "/index.html", String(), false); });
server.begin();
webSocket.begin();
webSocket.onEvent(webSocketEvent);
}
void loop()
{
webSocket.loop();
if (millis() % 1000 == 0)
{
String message = String(millis() / 1000);
webSocket.broadcastTXT(message);
}
}
今回はmills()
という関数で、プログラムが実行された時間を取得し、1秒ごとにその数字をWebSocketでブラウザに送信しています。
WebSocketの通信を受け取るhtmlはこちらです。
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>React tsx-esm-standalone</title>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script>
const WebSocketUrl = window.location.href.replace("http", "ws").slice(0,-1) + ":81";
</script>
<script>
// typescript用のpresetを登録する
Babel.registerPreset('tsx', {
presets: [
[Babel.availablePresets['typescript'], // preset-typescriptを指定
{allExtensions: true, isTSX: true} // allExtensionsは、isTSX利用時に必要なためセット
]],
},
);
</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
import React, { useState } from "https://cdn.skypack.dev/react@17";
import ReactDOM from "https://cdn.skypack.dev/react-dom@17";
const Count = () => {
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
const socket = new WebSocket(WebSocketUrl); // M5StackのIPアドレスを指定
socket.onmessage = (event) => {
const data = event.data;
setCounter(data);
};
return () => {
socket.close();
};
}, []);
return (
<div>
<h1>Counter Value: {counter}</h1>
</div>
);
};
ReactDOM.render(<Count />, document.getElementById('app'));
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>
WebSocketのurlですが、ws://{マイコンのIPアドレス}:{main.cppで設定したポート番号}
となっています。
これを環境によらず動的に取得するために、window.location.href
という変数でそのページのURLを取得しています。具体的にはwindow.location.href
で、http://{マイコンのIPアドレス}/
という情報が取得できます。ここから、WebSocketのURLに変更するために、http
をws
に置き換え、末尾の/
を消して、:81
を追加しています。
これが、ソースコード内の
const WebSocketUrl = window.location.href.replace("http", "ws").slice(0,-1) + ":81";
です。
あとはWebSocketからの値をReactを使って、動的に変更するコードです。この部分の解説は省略します。
マイコンに表示する処理が追加されていたり、wifi接続周りのコードが微妙に変わっていますが、全体のソースコードはここに置いておきます。
フラッシュメモリーにhtmlファイルを書き込み、main.cppをマイコンに書き込み、wifiに接続して、表示されたIPアドレスをブラウザに打ち込むと、値が1秒に一回更新されます。
ラグについて
マイコンに値を表示する処理を追加して、wifi経由で値を渡すことによってどのくらいラグが発生するのか検証しましたが、今回は目で分かるようなラグは発生しませんでした。
まとめ
自分のスマホとかPCから遠隔でセンサー情報を見れるのは夢があるよね。
宣伝
現在新潟大学学生フォーミュラプロジェクトでは、スポンサーになっていただける企業様や個人を募集しています。
プログラミングから溶接まで多種多様なスキルを持った学生が在籍しています。
下記のメールまでご連絡をお待ちしております。
next-fp@eng.niigata-u.ac.jp