M5StackからWebページに値を送る方法 ~~WebSocket編~~

# 使用用途
- マイコンの値を手軽に確認できるシステムを構築したいとき
- ローカルネットワーク内で完結するとき
- あんまり長いコードを書かず、モジュールに頼りたいとき

# 実装に必要なもの
- ブラウザ
- M5Stack(ESP32)

# 今回のシステムの構成図
![system.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219558/1e08eef7-d9fa-4d48-bab2-737a4f6c1f0b.png)
WebページがWebSocketのクライアントとなり、ESP32がWebSocketのサーバーとなりWebSocketを使いデータのやり取りを行います。


# htmlファイルの配信方法について
Webページはお好みの方法で配信してもらって構いません。
以下の方法があると思います。
- そもそもhtmlファイルを配信せず、htmlファイルをPCから開くだけ
- M5Stack(ESP32)をWebサーバー化して、配信する
- Webサーバーを借りたり、ホスティングサービスを利用してそこから配信する

ただし一つ注意点があり、

:::note warn
HTTPSでWebページを配信してしまうと、混合コンテンツエラーになる可能性があります
:::

と言うのは、今回ESP32上に立ち上げるWebSocketのサーバーはHTTPなので。

また、

:::note warn
外部ネットワーク上のサーバーからhtmlファイルを配信しても動作しますが、**htmlファイルを開く場所が外部ネットワークになるとESP32とWebページが接続できません**
:::

**今回私はWebサーバーもESP32で立ち上げる方式をとりました。**  

Webサーバーの立ち上げについてはこの記事では解説しませんので、以下をご覧ください。

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

# 開発環境
今回開発環境として、Platform IOを用いました。  
Platform IOを使ったことがないかたは、以下の記事をお読みください。  

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

# 使用したライブラリ
```platformio.ini
lib_deps = 
	m5stack/M5Unified@^0.1.10
	links2004/WebSockets@^2.4.1
	me-no-dev/ESPAsyncTCP@^1.2.2
	me-no-dev/ESP Async WebServer@^1.2.3
```

# 実装

全体のコードは一番ページ下に置きます。以下は解説です。

## ESP32側のサーバーの立ち上げ
```cpp
const int webSocketPort = 81;
WebSocketsServer webSocket = WebSocketsServer(webSocketPort);

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()
{
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
}
```

`webSocketEvent`関数でWebSocket接続時などに実行する処理を設定しています。今回は、特に使用していませんが、クライアントから何かデータを受け取るときはお世話になると思います。

## サーバーからのデータ送信
```cpp
void loop()
{
  webSocket.loop();
  if (millis() % 1000 == 0)
  {
    String message = String(millis() / 1000);
    webSocket.broadcastTXT(message);
  }
}
```
今回はESP32にデータが立ち上がってからの時間をWebSocketで送信しています。  
もちろん、センサーから読み取った値も同様な方法で送信可能です。

## Webページクライアント側
### WebSocketのURLの自動取得
```js
const WebSocketUrl =
        window.location.href.replace("http", "ws").slice(0, -1) + ":81";
```
WebSocketのURLは今回のプログラムでは、以下のような形になります。
```
ws://${マイコンのipアドレス}:${WebSocketのポート番号}
```
また、マイコンでWebページを配信している場合は、このページは以下のURLになっているはずです。
```
http://${マイコンのipアドレス}/
```
`window.location.href`で現在アクセスしているページのurlが取得できるので、これを利用してWebSocketのURLを自動取得しています。  
上記のコードの通り、URLの`http`のところを`ws`に置き換え、末尾についている`/`を削除し、`:81`を追加すればWebSocketのURLが出来上がります。

### WebSocketクライアントのコード
めちゃくちゃシンプルで、メッセージを受け取ったらpタグの要素を書き換えるだけです。
```js
socket.onmessage = (event) => {
    const data = event.data;
    document.getElementById("data").textContent = data;
};
```

# 動作
WiFi接続に関しては、`esp smart config`を使用します。
WiFiの接続方法については、`esp smart config`で調べてください。
今回私が作成したコードでは、Webページの閲覧に必要なIPアドレスがシリアルターミナル上、もしくはM5Stackのディスプレイに表示されるQRコードに表示されます。お好きなやり方でWebページにアクセスしてみてください。
Webページの左上の数字がどんどん増えていくのが見られるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219558/57a85f04-6ba2-1c5f-4afe-d80548cd3e45.png)

# 全体のコード
```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);

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;
  }
}

String WifiSetup()
{
  Serial.println("Connecting...");
  WiFi.begin();
  while (WiFi.status() != WL_CONNECTED)
  {
    if (millis() > 10000)
    {
      Serial.println("Smart Config Start!");
      WiFi.beginSmartConfig();
      while (!WiFi.smartConfigDone())
      {
        if (millis() % 500 == 0)
        {
          Serial.print(".");
          delay(1);
        }
      }
      Serial.println("Smart Config Done!");
    }
    if (millis() % 500 == 0)
    {
      Serial.print(".");
      delay(1);
    }
  }

  Serial.println("WiFi Connected.");

  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
  return ("http://" + WiFi.localIP().toString());
}

void initSPIFFS()
{
  if (SPIFFS.begin())
  {
    Serial.println("SPIFFS initialized.");
  }
  else
  {
    Serial.println("Error initializing SPIFFS.");
  }
}

void setup()
{
  M5.begin();
  M5.Power.begin();
  M5.Lcd.fillScreen(BLACK);
  Serial.begin(115200);
  String url = WifiSetup();
  M5.Lcd.qrcode("http://" + WiFi.localIP().toString());
  initSPIFFS();

  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);
  }
}
```

```index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>esp32 WebSocket</title>
    <script>
      const WebSocketUrl =
        window.location.href.replace("http", "ws").slice(0, -1) + ":81";
      const socket = new WebSocket(WebSocketUrl); // M5StackのIPアドレスを指定

      socket.onmessage = (event) => {
        const data = event.data;
        document.getElementById("data").textContent = data;
      };
    </script>
  </head>
  <body>
    <p id="data"></p>
  </body>
</html>
```

# まとめ
WebSocket手軽に実装できるので、フロントエンドにマイコンから値を渡したい場合はこの方法がおすすめ。
ただし、
- ルーター越え(外部ネットワーク)が必要
- httpsでページを配信したいとき

のような場合には使えないので注意。

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

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

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