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

# 使用用途
基本的には下の記事の方法がおすすめです。

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

理由は、Bluetooth LEはWebSocketに比べてやらなければならないことが多く面倒くさいからです。
なので、以下の制約でWebSocketが使えない場合に限られると思います。

- M5Stackがネットワークに接続できないという条件(WiFi環境がない場所で使用 かつ WiFiのAPモードでの運用も禁止)のとき
- どうしてもWebページをHTTPSで配信しなければならないとき
- Bluetoothを使いたいとき

また、滅茶苦茶当たり前ですが

:::note warn
Webページを開く場所はM5StackとBluetoothで通信できる範囲内である必要があります。
:::

# 実装に必要なもの
- Google Chrome(IphoneのChromeでは動きませんでした。Macは未確認)
- M5Stack(ESP32)

# できたもの
![WebBluetooth.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219558/01f094fc-8178-b9fc-af89-401bd7ed4222.gif)

`connect-ble`というボタンを押すと、BluetoohでM5Stackと接続でき、接続が完了したあとは2秒ごとにデータが受信され、下のエリアにそのデータが描画されます。
また、接続完了したあとに、もう一度`connect-ble`ボタンを押すと、シリアルモニター上に`Hello, BLE`というメッセージが流れます。

# htmlの配信方法
Webページはお好みの方法で配信してもらって構いません。
以下の方法があると思います。

- そもそもhtmlファイルを配信せず、htmlファイルをPCから開くだけ
- M5Stack(ESP32)をWebサーバー化して、配信する
- Webサーバーを借りたり、ホスティングサービスを利用してそこから配信する

自分の用途に合わせて選択してください。

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

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

# 実装
## Bluetooth LEについて
今回のコードは長いですが、Bluetooth LEで物をつくったことがある方にとってはあまり難しくないと思います。  
今回の記事は、Bluetooth LEについての解説はしません。代わりに私が勉強に使用したドキュメントを貼っておきます。

https://www.musen-connect.co.jp/blog/course/trial-production/ble-beginner-1/

https://www.musen-connect.co.jp/blog/course/trial-production/ble-beginner-2/

## サーバー(M5Stack)のコード
```main.cpp
#include <Arduino.h>
#include <M5Unified.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>

#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID_TX "beb5483f-36e1-4688-b7f5-ea07361b26a8"
#define CHARACTERISTIC_UUID_RX "beb5483e-36e1-4688-b7f5-ea07361b26a9"

BLEServer *pServer = NULL;
BLECharacteristic *pTxCharacteristic;
BLECharacteristic *pRxCharacteristic;
bool deviceConnected = false;

class MyServerCallbacks : public BLEServerCallbacks
{
  void onConnect(BLEServer *pServer)
  {
    deviceConnected = true;
    Serial.println("Connected");
  };

  void onDisconnect(BLEServer *pServer)
  {
    deviceConnected = false;
    Serial.println("Disconnected");
  }
};

class MyCallbacks : public BLECharacteristicCallbacks
{
  void onWrite(BLECharacteristic *pCharacteristic)
  {
    std::string value = pCharacteristic->getValue();

    if (value.length() > 0)
    {
      String message = value.c_str();
      Serial.println(message);
    }
  }
};

void setup()
{
  M5.begin();
  Serial.begin(115200);
  BLEDevice::init("ESP32_BLE_SERVER"); // この名前がスマホなどに表示される
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());
  BLEService *pService = pServer->createService(SERVICE_UUID);
  pTxCharacteristic = pService->createCharacteristic(
      CHARACTERISTIC_UUID_TX,
      BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ);
  pTxCharacteristic->addDescriptor(new BLE2902());
  pRxCharacteristic = pService->createCharacteristic(
      CHARACTERISTIC_UUID_RX,
      BLECharacteristic::PROPERTY_READ |
          BLECharacteristic::PROPERTY_WRITE); // 読み書き可能にする

  pRxCharacteristic->setCallbacks(new MyCallbacks());
  pService->start();
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06); // iPhone接続の問題に役立つ
  pAdvertising->setMinPreferred(0x12);
  BLEDevice::startAdvertising();
  Serial.println("Characteristic defined! Now you can read it in your phone!");
}

void loop()
{
  delay(2000);
  if (deviceConnected)
  {
    // データを送信
    Serial.println("Sending data");
    String str(millis() / 1000);
    pTxCharacteristic->setValue(str.c_str());
    pTxCharacteristic->notify();
  }
}
```

今回は、送信するデータとして`mills`で取得したマイコンの起動時間を送信しています。

## クライアント(Webページ)のコード
可読性低いです。ごめんなさい🙇
```index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>SensorLess GPS FrontEND</title>
    <style>
      #messageLog {
        height: 200px; /* 高さを固定 */
        overflow-y: auto; /* 縦方向のオーバーフロー時にスクロールバーを表示 */
        margin-top: 0;
      }
      #messageArea {
        border: 2px solid #000;
        border-radius: 15px;
        padding: 0px 10px;
        margin-top: 20px;
      }
    </style>
  </head>
  <body>
    <button type="button" id="connect-ble">connect-ble</button>

    <div id="messageArea">
      <div id="messageLog"></div>
    </div>

    <script>
      const kUARTServiceUUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
      const kTXCharacteristicUUID = "beb5483e-36e1-4688-b7f5-ea07361b26a9";
      const kRXCharacteristicUUID = "beb5483f-36e1-4688-b7f5-ea07361b26a8";

      let device;
      let server;
      let service;
      let characteristicRx;
      let characteristicTx;
      let messageLog = [];

      const readData = async (characteristic) => {
        const ary = [];
        const value = await characteristic.readValue();
        ary.push(new TextDecoder().decode(value));

        // 特性値が変更されたときのイベントリスナーを追加
        characteristic.addEventListener(
          "characteristicvaluechanged",
          (event) => {
            messageLog.push(new TextDecoder().decode(event.target.value));
            document.getElementById("messageLog").innerText =
              messageLog.join("\n");
          }
        );

        // 通知を開始
        await characteristic.startNotifications();
      };

      document.getElementById("connect-ble").addEventListener("click", () => {
        sendData("Hello, BLE!");
      });

      const sendData = async (message) => {
        if (!device) {
          try {
            device = await navigator.bluetooth.requestDevice({
              filters: [{ services: [kUARTServiceUUID] }],
            });
            device.addEventListener("gattserverdisconnected", async () => {
              server = undefined;
              service = undefined;
              characteristicRx = undefined;
              characteristicTx = undefined;
              await initBle();
            });
            initBle();
          } catch (error) {
            console.log("Error: " + error);
            return;
          }
        }

        let encoder = new TextEncoder();
        let data = encoder.encode(message);

        try {
          await characteristicTx.writeValue(data);
        } catch (error) {
          console.log("Error: " + error);
        }
      };

      const initBle = async () => {
        server = await device.gatt.connect();
        service = await server.getPrimaryService(kUARTServiceUUID);
        characteristicTx = await service.getCharacteristic(
          kTXCharacteristicUUID
        );
        characteristicRx = await service.getCharacteristic(
          kRXCharacteristicUUID
        );
        readData(characteristicRx);
      };
    </script>
  </body>
</html>
```

# コードを書く上で参考にさせていただいた記事

https://qiita.com/omitsuhashi/items/0547f26a604603896433

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

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

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