【お手軽】M5stackでReactアプリをホストし、マイコン内の変数を閲覧してみた。

(2024/6/27 編集:記事タイトルを分かりやすくなるように変更し、記事の文章も分かりやすくしました。)

# 初めに
## 作ったもの
Reactを使って、リアルタイムにマイコン内の変数を表示するアプリ作ってみました。
動作はこんな感じ。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219558/ad582803-ed72-5708-b058-91dd5af27a8b.gif)



PCの画面に `Counter Value:` と書かれていますが、この数字が動いているのがReactアプリです。
Reactで作っているのでブラウザを常に更新しなくても、Reactが動的に値を変更してくれます。
ラグを測るために、マイコンのディスプレイにも同じ数字を表示しています。

## 作ったシステムの図
センスがなさすぎる図ですいません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219558/4bb2c768-44f4-2d02-2e02-ddb21d6c194e.png)


マイコンがwebサーバーとして、htmlファイルを配布し、それを受け取ったブラウザはそのhtmlファイルに埋め込まれたReactを動かします。
ReactはマイコンからWebSocketで送られてきた値を受け取り、その値に応じてブラウザのDOM(ブラウザのレンダリング結果)を更新します。

まとめると、

:::note info
マイコンは2つのデータをそれぞれ異なる経路でブラウザに情報を渡します。
- htmlファイルデータ: Webサーバーで渡す
- マイコンの内部変数: WebSocketで渡す
:::

詳しくはあとで説明しますが、システムの大まかな構成をつかんでもらえると幸いです。

## 「簡単に」とは
ESP32でReactアプリを動かす方法は、結構他のサイトでも紹介されています。
しかし、gzipで圧縮などとすこし難しそうなことをしていたので、簡単にできないかと思い、

:::note info
htmlファイルにCDNのReactを埋め込むことで実装してみました。
:::
**基本的なコードはWebサーバーをマイコンで立ち上げてhtmlファイルを配信する方法と変わりません。**

# 環境
マイコン:`M5Stack TOUGH`
開発環境:`PlatformIO IDE`
使用したPC: `Windows11`
Reactを動かしたブラウザ:`Google Chrome(Windows11)`

# 実装
## htmlページの配信
まずはhtmlページの配信から説明をします。

### ESP32でWebサーバーの立ち上げ
Webサーバーを立ち上げたことがない方は以下の記事をご覧ください。

https://it-evo.jp/blog/blog-1441/

### ESP32のフラッシュメモリーについて
今回はesp32内のフラッシュメモリーにhtmlファイルを置いています。

PlatformIOでフラッシュメモリーを使うには、以下の記事をお読みください。

https://labo.mycabin.net/electronics-programming/arduino-esp32/1268/

ちなみに、`Upload Filesystem Image`というのはvscodeのアリさんマークを開いたときに出現するウィンドウの上側、
`PROJECT TASKS/m5stack-core2/Platform/Upload Filesystem Image` にあります。

### WiFi Smart Configについて
今回私はSmart Configを使用しました。
smartConfigとはなんぞやという方は以下のurlをご覧ください。

https://lang-ship.com/blog/work/esp32-wi-fi-setting/#toc9

### ソースコード

```main.cpp
#include <Arduino.h>
#include <M5Unified.h>
#include <WiFi.h>
#include "ESPAsyncWebServer.h"
#include <SPIFFS.h> // SPIFFS（ファイルシステム）用のライブラリをインクルード

AsyncWebServer server(80);  // ポート80でWebサーバーを開始

void setup()
{
  M5.begin();
  M5.Power.begin();
  Serial.begin(115200);
  Serial.print("Connecting");
  // Wifi接続作業開始
  int i = 1;
  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());
  // WiFi接続作業ここまで

  // SPIFFS（ファイルシステム）を初期化ここから
  if (SPIFFS.begin())
  {
    Serial.println("SPIFFS initialized.");
  }
  else
  {
    Serial.println("Error initializing SPIFFS.");
  }
  // SPIFFS（ファイルシステム）を初期化ここまで
  
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(SPIFFS, "/index.html", String(), false); });
  server.begin();
}

void loop()
{
}
```
htmlファイルはテキトーで構いません。
もはやこれ一行でも良いです。
```index.html
<h1>Hello World!</h1>
```

また、コードを動かすのにライブラリが必要ですので、`platformio.ini`に以下を追加してください。
```platformio.ini
lib_deps = 
	m5stack/M5Unified@^0.1.10
    me-no-dev/ESPAsyncTCP@^1.2.2
	me-no-dev/ESP Async WebServer@^1.2.3
 ```
### 実行結果
ソースコードを書きこめて、wifiに繋がるとシリアルモニタに以下のように表示されると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219558/8c8620cc-f1ec-b538-8234-ada17cca64f2.png)

この`IP Address:`の後に表示されている値を、同じwifiに繋がっているスマホやPCのブラウザに打ち込んでアクセスすると、htmlファイルが確認できます。

## Reactアプリの配信
### ソースコード
今回はTypeScriptを使いましたが、JavaScriptでも構いません。

この方法なら、main.cppを触る必要はないので、フラッシュメモリーに書き込んだhtmlファイルのみを編集していきます。

```index.html
<!DOCTYPE html>
<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での値渡し
(追記) この部分の解説記事書きました。
解説については以下をご覧ください。

https://qiita.com/nextfp/items/ffac9f3a76d5725f056f

### ソースコード
```main.cpp
#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);

AsyncWebServer server(80);　// ポート80でWebサーバーを開始

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:
    // メッセージの受け取りをしたいときに使う
    break;
  case WStype_DISCONNECTED:
    Serial.printf("WebSocket client disconnected: %u\n", num);
    break;
  }
}

void setup()
{
  M5.begin();
  M5.Power.begin();
  Serial.begin(115200);
  Serial.print("Connecting");
  
  // WiFI接続処理ここから
  int i = 1;
  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());
  // WiFi接続処理ここまで

  // SPIFFS（ファイルシステム）を初期化ここから
  if (SPIFFS.begin())
  {
    Serial.println("SPIFFS initialized.");
  }
  else
  {
    Serial.println("Error initializing SPIFFS.");
  }
  // 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はこちらです。
```index.html
<!DOCTYPE html>
<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>
```

### 実行結果
フラッシュメモリーにhtmlファイルを書き込み、main.cppをマイコンに書き込み、wifiに接続して、表示されたIPアドレスをブラウザに打ち込むと、値が1秒に一回更新されます。

# ラグについて
マイコンに値を表示する処理を追加して、wifi経由で値を渡すことによってどのくらいラグが発生するのか検証しましたが、今回は目で分かるようなラグは発生しませんでした。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219558/ad582803-ed72-5708-b058-91dd5af27a8b.gif)

# 本記事を書く上で参考にした記事

https://qiita.com/murasuke/items/e0600497a8900ca8401b

# まとめ
自分のスマホとかPCから遠隔でセンサー情報を見れるのは夢があるよね。

# 宣伝
現在新潟大学学生フォーミュラプロジェクトでは、スポンサーになっていただける企業様や個人を募集しています。

プログラミングから溶接まで多種多様なスキルを持った学生が在籍しています。

下記のメールまでご連絡をお待ちしております。
next-fp@eng.niigata-u.ac.jp