🌟

【ESP32 × Flutter】Wi-Fiスキャン結果をBLEでチャンク分割送信する方法

2024/04/26に公開

はじめに

以前、猫型おしゃべりロボット「ミーア」に関する開発記事で、「Wi-Fiネットワークの探索機能をESP32に実装し、Bluetooth経由でFlutterアプリに送信する機能」について記載した。

https://kazulog.fun/dev/mia-wifi-connection-candidate-list/

最初はエラーなくスキャンしたWi-Fiリストが表示されていたが、ある時下記のエラーが出現した。

PlatformIO(ESP32)のログを見ると、下記のエラーが表示されていた。

設定しようとしているデータのサイズは724バイトだが、ESP32のBLE特性に許可されている最大サイズは600バイトであり、データのサイズが大きすぎるためBLE送信できないというエラー。

BLEServer connected
Scanning WiFi networks...
Scan complete.
Available networks:
Size of data to send: 724 bytes
[ 68429][E][BLECharacteristic.cpp:662] setValue(): Size 724 too large, must be no bigger than 600

現状のコード確認

現状、ESP32からWi-Fiスキャン結果をBluetooth通信でFlutterアプリに送信しているコードがこちら。Wi-Fiスキャン結果をJSON形式の文字列にシリアル化して、一括送信している。

// src/BLEManager.cpp

// Wi-FiネットワークリストをJSON形式の文字列にシリアル化
String serializeNetworks(const std::vector<NetworkInfo> &networks) {
  DynamicJsonDocument doc(1024);
  JsonArray array = doc.to<JsonArray>();

  for (const auto &network : networks) {
    JsonObject obj = array.createNestedObject();
    obj["ssid"] = network.ssid;
    obj["rssi"] = network.rssi;
    obj["encryptionType"] = network.encryptionType;
  }

  String result;
  serializeJson(doc, result);
  return result;
}

void BLEManager::CharacteristicCallbacks::onWrite(
    BLECharacteristic *pCharacteristic) {
  std::string value = pCharacteristic->getValue();

  if (value.length() > 0) {
    std::string ssid;
    std::string password;
    std::string response;

    // Wi-Fiネットワークリストの要求を処理する
    if (value == "request_wifi_list") {
      // 利用可能なWi-Fiネットワークをスキャン
      std::vector<NetworkInfo> networks = scanNetworks();

      // スキャン結果をシリアル化
      String networkListJson = serializeNetworks(networks);
      Serial.printf("networkListJson: %s\n", networkListJson.c_str());

      // 送信データのサイズをログ出力
      Serial.printf("Size of data to send: %d bytes\n", networkListJson.length());

      // シリアル化されたリストをBLE経由で送信
      pCharacteristic->setValue(networkListJson.c_str());
      pCharacteristic->notify();
      Serial.println("WiFi network list sent via BLE");

      return;
    }
  }
}

受け取り側のFlutterでは、fluter_blue_plusプラグインを使用しており、AndroidデバイスでのデフォルトMTU(Maximum Transmission Unit:BLEデバイス間で交換される最大データサイズを定義する値)は512バイトとして自動的にリクエストされ、iOSおよびmacOSではMTUが自動的に交渉される。

https://pub.dev/packages/flutter_blue_plus

というわけで、今回の場合2つの対応策があると思う。

1)大きなデータを複数のメッセージに分割して送信する

2)キー名を短くすることでJSONのサイズを削減する

どちらも行おうと思う。まずは、簡易な2から。

JSONキー名を短くしてJSONサイズを削減

現状、スキャンしたWifiリストをBLE送信する際は、下記のようなリスト構造になっていてキー名がやや長い。

{"ssid":"XXXXXXX-X-XXXXX","rssi":-56,"encryptionType":3}

Wi-FiネットワークリストをJSON形式の文字列にシリアル化する部分で、**ssidsに、rssirに、encryptionTypee**にするなどして、キー名を短縮する。

// src/BLEManager.cpp
// Wi-FiネットワークリストをJSON形式の文字列にシリアル化
String serializeNetworks(const std::vector<NetworkInfo> &networks) {
  DynamicJsonDocument doc(1024);
  JsonArray array = doc.to<JsonArray>();

  for (const auto &network : networks) {
    JsonObject obj = array.createNestedObject();
    obj["s"] = network.ssid;
    obj["r"] = network.rssi;
    obj["e"] = network.encryptionType;
  }

  String result;
  serializeJson(doc, result);
  return result;
}

JSONキー名を変更したので、Flutter側(アプリ)のコードもそれに合わせて変更する必要がある。具体的には、**WifiNetwork**クラスのファクトリメソッド fromJson とそれを使用している部分で、JSONのキー名を新しい短いキーに合わせて更新する。

// lib/services/ble_service.dart
// Wi-Fiネットワーク情報を保持するクラス
class WifiNetwork {
  final String ssid;
  final int rssi;
  final int encryptionType;

  WifiNetwork(
      {required this.ssid, required this.rssi, required this.encryptionType});

  factory WifiNetwork.fromJson(Map<String, dynamic> json) {
    return WifiNetwork(
      ssid: json['ssid'] as String,
      rssi: json['rssi'] as int,
      encryptionType: json['encryptionType'] as int,
    );
  }
}

動作確認

600バイトを超えていてBLE送信エラーとなっていたWifiリストが、JSONキーの短縮により479バイトまで抑えられて無事送信できた。

execute 1 minute action
Scanning WiFi networks...
12
Scan complete.
Available networks:
SSID / RSSI / Encryption
Size of data to send: 479 bytes
WiFi network list sent via BLE

しかし、これは根本的な解決策にはなっておらず、仮に商業施設など多くのWi-Fiが飛び交っている状況ではWi-Fiスキャン結果が100近くになることも想定されるため、その場合は、BLE送信サイズ超過エラーは解消できない。

なので、BLE分割送信を実装する必要がある。

データ分割による複数メッセージでの送信

PlatformIO (ESP32) :チャンクに分けて送信

WiFiネットワークリストをチャンクに分けて送信する関数を作成する。

データがBLEの最大ペイロードサイズ(通常は512バイト)に達するたびにBLEを通じてデータを送信し、新しいチャンクを開始する。

// BLEManager.hに追加
void sendDataInChunks(const std::string& data, unsigned int chunkSize);

// BLEManager.cppに実装を追加
void BLEManager::sendDataInChunks(const std::string& data, unsigned int chunkSize) {
    size_t numChunks = data.length() / chunkSize + (data.length() % chunkSize != 0);
    for (size_t i = 0; i < numChunks; i++) {
        size_t start = i * chunkSize;
        std::string chunk = data.substr(start, chunkSize);
        _pCharacteristic->setValue(chunk);
        _pCharacteristic->notify();
        delay(100);
    }
}

void BLEManager::CharacteristicCallbacks::onWrite(BLECharacteristic *pCharacteristic) {
    std::string value = pCharacteristic->getValue();

    if (value == "request_wifi_list") {
        std::vector<NetworkInfo> networks = scanNetworks();
        String networkListJson = serializeNetworks(networks);
        Serial.printf("networkListJson: %s\n", networkListJson.c_str());
        Serial.printf("Size of data to send: %d bytes\n", networkListJson.length());

        // データをチャンクに分けて送信。512はBLEのペイロードサイズ
        sendDataInChunks(networkListJson.c_str(), 512);
        Serial.println("WiFi network list sent via BLE in chunks");
    }
}

BLEの特性の一部として定義されるDescriptorにCCCDを追加

分割送信を行う場合、BLEの通知(Notify)機能を利用することで、、、

続きは、こちらで記載しています。
https://kazulog.fun/dev/esp32-flutter-ble-chunk-transmission/

Discussion