はじめに
以前、猫型おしゃべりロボット「ミーア」に関する開発記事で、「Wi-Fiネットワークの探索機能をESP32に実装し、Bluetooth経由でFlutterアプリに送信する機能」について記載した。
最初はエラーなくスキャンした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: %sn", networkListJson.c_str());
// 送信データのサイズをログ出力
Serial.printf("Size of data to send: %d bytesn", 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形式の文字列にシリアル化する部分で、ssid
をs
に、rssi
をr
に、encryptionType
をe
にするなどして、キー名を短縮する。
// 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: %sn", networkListJson.c_str());
Serial.printf("Size of data to send: %d bytesn", networkListJson.length());
// データをチャンクに分けて送信。512はBLEのペイロードサイズ
sendDataInChunks(networkListJson.c_str(), 512);
Serial.println("WiFi network list sent via BLE in chunks");
}
}
BLEの特性の一部として定義されるDescriptorにCCCDを追加
分割送信を行う場合、BLEの通知(Notify)機能を利用することで、ESP32がデータの準備ができた時点で即座にアプリにデータを送信できる。これを実現するためには、特性に対して「Client Characteristic Configuration Descriptor(CCCD)」を設定する必要がある。
http://marchan.e5.valueserver.jp/cabin/comp/jbox/arc212/doc21201.html
// BLEManager.hに追加
#include <BLE2902.h>
// BLEManager.cppに実装を追加
void BLEManager::initialize() {
// Initialize the BLE Device
BLEDevice::init("ミーア V1");
// Create the BLE Server
_pServer = std::unique_ptr<BLEServer>(BLEDevice::createServer());
_pServer->setCallbacks(&serverCallbacks);
// Create the BLE Service
BLEService *pService = _pServer->createService(SERVICE_UUID);
// Create a BLE Characteristic
_pCharacteristic = std::unique_ptr<BLECharacteristic>(
pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_NOTIFY |
BLECharacteristic::PROPERTY_INDICATE));
// Create a BLE Descriptor (CCCD)
_pCharacteristic->addDescriptor(new BLE2902());
_pCharacteristic->setCallbacks(&characteristicCallbacks);
pService->start();
}
Flutter (アプリ):分割されたデータから再構築
通知の設定
- 特性に対して
setNotifyValue(true)
を呼び出し、BLEデバイスからのデータを非同期に受信できるようにする。これにより、デバイスが新しいデータを送信するたびにFlutterアプリに通知され、即座にデータを受け取ることができる。 - 特性の通知を有効にした後(
setNotifyValue(true)
をコールした後)、lastValueStream
を購読することで、その特性から送られてくるデータの変更をリアルタイムで受け取れる。
flutter_blue_plusパッケージの、Last Value Streamのセクション参考
https://pub.dev/packages/flutter_blue_plus
分割送信の再構成方法
- 受信したデータを
StringBuffer
に追加し、データがJSONの開始([
)で始まり、終了(]
)で終わるかをチェックする。全てのチャンクが受信され、有効なJSON文字列が形成された場合、それを解析しWifiNetwork
オブジェクトのリストに変換する。
// lib/services/ble_service.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:your_app/models/wifi_network.dart';
class BLEService {
FlutterBluePlus flutterBlue = FlutterBluePlus.instance;
final String serviceUUID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx";
final String characteristicUUID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx";
Future<List<WifiNetwork>> requestWifiList(BluetoothDevice device) async {
final completer = Completer<List<WifiNetwork>>();
List<WifiNetwork> wifiList = [];
StringBuffer buffer = StringBuffer();
bool isStartChunk = true;
bool isEndChunk = false;
try {
// デバイスに接続
await device.connect();
// サービスを探索
List<BluetoothService> services = await device.discoverServices();
BluetoothService service =
services.firstWhere((s) => s.uuid == clockyServiceUUID);
BluetoothCharacteristic characteristic =
service.characteristics.firstWhere((c) => c.uuid == wifiUuid);
// 通知を有効にする
await characteristic.setNotifyValue(true);
// Wi-Fi リスト要求を送信
await characteristic.write(utf8.encode("request_wifi_list"));
// ストリームを購読し、受信したデータをバッファリング
Stream<List<int>> stream = characteristic.lastValueStream;
stream.listen((data) {
String decodedData = utf8.decode(data);
print('Received chunk: $decodedData');
if (isStartChunk) {
if (decodedData.startsWith('[')) {
buffer.write(decodedData);
isStartChunk = false;
isEndChunk = false;
}
} else {
if (decodedData.endsWith(']')) {
buffer.write(decodedData);
isEndChunk = true;
} else {
buffer.write(decodedData);
}
}
if (isEndChunk) {
String jsonData = buffer.toString();
print("Received JSON data: $jsonData");
try {
final jsonResponse = jsonDecode(jsonData) as List<dynamic>;
wifiList = jsonResponse.map((networkJson) {
final networkMap = networkJson as Map<String, dynamic>;
return WifiNetwork.fromJson(networkMap);
}).toList();
if (!completer.isCompleted) {
buffer.clear();
completer.complete(wifiList);
}
} catch (e) {
print('Error parsing JSON data: $e');
}
isStartChunk = true;
isEndChunk = false;
}
});
// ストリームが完了したらリストを返す
return completer.future;
} catch (e) {
print('Error occurred while requesting Wi-Fi list: $e');
rethrow;
}
}
}
Completerのと使用と、複数回のリスキャン時のエラー回避
Completerの目的
Completer
は非同期操作が完了したことを外部に通知するために使う。このケースでは、全てのデータが正しく受信・解析された後に、Future
を完成させ、Wi-Fiネットワークリストを呼び出し元に返すために使用。Completer
がない場合、すべてのデータが到着していると確認する方法がなく、不完全なデータで処理を進めてしまい、アプリにWifiスキャン結果のリストが表示されない。
複数回リスキャン時のエラー回避
Completer
が一度完成された後、もう一度「Wi-Fiを探す」ボタンを押すと、すでに完了したCompleter
に対してまた.complete()
を呼び出そうとしてエラーが発生する。一度Completer
が完了すると(つまり、.complete()
メソッドが呼び出されると)、再度同じCompleter
で.complete()
を呼び出すことはできないため。- これを防ぐために、
Completer
の状態をチェックし、未完成の場合のみ完成させるようにする。また、各スキャン操作の開始時に新しいCompleter
インスタンスを作成することで、前の操作の状態から独立させる。
Future<List<WifiNetwork>> requestWifiList(BluetoothDevice device) async {
// スキャンごとに新しいCompleterを作成
final completer = Completer<List<WifiNetwork>>();
try {
// ... (デバイスへの接続コードなど)
// ストリームリスナーコールバック
Stream<List<int>> stream = characteristic.lastValueStream;
stream.listen((data) {
String decodedData = utf8.decode(data);
// ... (チャンク処理とJSONデータ解析のコード)
if (isEndChunk) {
// 完全なJSONデータを処理
// 解析前にJSONデータを検証
try {
// Completerが完了していないことを確認
if (!completer.isCompleted) {
buffer.clear();
completer.complete(wifiList);
}
} catch (e) {
print('Error parsing JSON data: $e');
}
}
});
// CompleterのFutureを返す
return completer.future;
} catch (e) {
print('Error occurred while requesting Wi-Fi list: $e');
rethrow;
}
}
動作確認
PlatformIO(ESP32)
512バイトを超えるデータの場合でも無事送信できている
Scanning WiFi networks...
13
Scan complete.
Available networks:
SSID / RSSI / Encryption
Size of data to send: 720 bytes
WiFi network list sent via BLE in chunks
Flutter
ESP32から分割送信されるWi-Fiスキャン結果に対して、有効なJSON文字列が形成された場合にのみ、JSON dataとしてWi-Fiリストを再構成できていることが分かる。
flutter: Received chunk: P-337H-G","rssi":-87,"encryptionType":4},{"ssid":"********-W","rssi":-88,"encryptionType":3},<br>flutter: Received chunk: [{"ssid":"********-W","rssi":-64,"encryptionType":3},{"ssid":"********-W","rssi":-71,"encryptionType":3},{"ssid":"AP_sta<br>flutter: Received chunk: ff_3F_2.4","rssi":-88,"encryptionType":4},{"ssid":"********-W","rssi":-89,"encryptionType":4}]<br>flutter: Received JSON data: [{"ssid":"********-W","rssi":-64,"encryptionType":3},{"ssid":"********-W","rssi":-71,"encryptionType":3},{"ssid":"********-W","rssi":-79,"encryptionType":3}]
アプリ画面にもWifiリストが無事表示された。
スキャン結果Wi-Fi数が画面縦幅を超える場合には、スクロールして表示している。「もう一度探す」ボタンをクリックした時も無事表示されることを確認できた。
コメント