はじめに
現在、方言を話すおしゃべり猫型ロボット「ミーア」を開発中。
現状のミーアはESP32とアプリの接続を、まずはBluetoothで接続した後に、Wi-Fi の接続をアプリから BLE 経由で行っている。
ただ、Wi-Fi接続のSSIDとパスワードを直書きする仕様になっていて、これだと、ユーザーからするととても面倒なため、接続可能なWi-Fiを探索して選べる形式に変更したい。
デバイス(ESP32)側実装
Wi-Fiネットワークの探索機能をESP32に実装
ESP32が利用可能なWi-Fiネットワークをスキャンしてリストを作成し、そのリストをBLE経由でアプリに送信するようにファームウェアを実装する。
現在のPlatformIOでのWifi接続部分のコードはこちら
// WifiConnection.h
#pragma once
#include <WiFi.h>
bool setupWiFi(const char *ssid, const char *password);
// WifiConnection.cpp
#include "WiFiConnection.h"
#include "SPIFFS.h"
#define WIFI_CONNECT_TIMEOUT 5000
bool setupWiFi(const char *ssid, const char *password) {
unsigned long startTime = millis();
if (ssid == nullptr || password == nullptr) {
Serial.println("Connecting to WiFi with previously stored credentials...");
WiFi.begin();
} else {
Serial.println("Connecting to WiFi with SSID and password from build flags...");
WiFi.begin(ssid, password);
}
while (WiFi.status() != WL_CONNECTED && millis() - startTime < WIFI_CONNECT_TIMEOUT) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("Connected to WiFi");
return true;
} else {
Serial.println("Not connected to WiFi");
return false;
}
}
ESP32でのWi-Fi機能に関する解説はこちらがわかりやすい
ESP32のWi-Fiライブラリは、WiFi.scanNetworks()という、利用可能なWi-Fiネットワークリスト表示してくれる関数がある。
今回は、ESP32をWi-Fiステーションモードに設定する。これにより、ESP32はWi-Fiアクセスポイントとして動作するのではなく、利用可能なWi-Fiネットワークをスキャンしたり、特定のネットワークに接続したりできるようになる。
WifiConnectionファイルを下記のように修正し、利用可能なWi-Fiネットワークリストを返す関数を追加する。
// WifiConnection.h
#pragma once
#include <WiFi.h>
#include <vector>
bool setupWiFi(const char *ssid, const char *password);
std::vector<String> scanNetworks();
// WifiConnection.cpp
std::vector<String> scanNetworks() {
std::vector<String> networkList;
// Wi-Fiモジュールをステーションモードに設定し、既存の接続を切断
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100); // 小さな遅延を挿入して安定性を高める
Serial.println("Scanning WiFi networks...");
int n = WiFi.scanNetworks();
Serial.println("Scan complete.");
if (n == 0) {
Serial.println("No networks found.");
} else {
for (int i = 0; i < n; ++i) {
// ネットワークリストにSSIDを追加
networkList.push_back(WiFi.SSID(i));
}
}
return networkList;
}
まずは、この段階でスキャンしたWi-Fiネットワークの結果をシリアルモニタに出力して確かめてみる。
// main.cpp
void setup() {
// ...既存の初期化コード...
// 利用可能なWi-Fiネットワークをスキャン
std::vector<String> networks = scanNetworks();
// スキャン結果の表示
Serial.println("Available Wi-Fi networks:");
for (const String& ssid : networks) {
Serial.println(ssid);
}
// ...その他の初期化コード...
}
ESP32を実行(platformio run
)すると、下記のようにシリアルモニタに、利用可能なWi-Fiネットワークリストが表示された。
Wi-Fi ネットワークのスキャン結果をBLE 経由でアプリに送信
次にスキャンしたWi-FiネットワークリストをBluetooth経由でFlutterアプリへ送信する機能を実装する。
スキャンしたWi-Fiネットワークリストをシリアル化して、BLE通信で送信可能なフォーマットにして、BLEキャラクタリスティックを通じてFlutterアプリに送信する。
BLEサービスとキャラクタリスティックの設定
既に BLEManager
にBLEサービスとCharacteristicを設定済み。
BLEキャラクタリスティック(BLE Characteristic)は、Bluetooth Low Energy (BLE) 通信におけるデータ交換の基本的な要素で、ユニークなUUID、プロパティ、値などを含む。
void BLEManager::initialize() {
Serial.printf("Initialize Bluetooth Low Energyn");
// Initialize the BLE Device
BLEDevice::init("Mia V1");
Serial.printf("BLE Device initializedn");
// Create the BLE Server
_pServer = std::unique_ptr<BLEServer>(BLEDevice::createServer());
Serial.printf("BLE Server createdn");
_pServer->setCallbacks(&serverCallbacks);
Serial.printf("BLE Server callbacks setn");
// Create the BLE Service
BLEService *pService = _pServer->createService(SERVICE_UUID);
Serial.printf("BLE Service createdn");
// 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));
_pCharacteristic->setCallbacks(&characteristicCallbacks);
Serial.printf("BLE Characteristic createdn");
// // Start the service
pService->start();
Serial.printf("BLE Service startedn");
}
Wi-Fiネットワークリストをシリアル化
Wi-FiネットワークリストをJSON形式にシリアル化するserializeNetworks
関数を実装する。
この関数を実装するために、ArduinoJsonライブラリを利用する。ArduinoJsonライブラリは、ESP32などのArduino互換デバイスでJSONデータを簡単に扱うためのライブラリ。
#include <ArduinoJson.h> // ArduinoJsonライブラリをインクルード
// Wi-FiネットワークリストをJSON形式の文字列にシリアル化する関数
String serializeNetworks(const std::vector<String>& networks) {
// JSONドキュメントを作成(サイズは適宜調整)
DynamicJsonDocument doc(1024);
// JSON配列を作成
JsonArray array = doc.to<JsonArray>();
// ネットワークリストをJSON配列に追加
for (const String& ssid : networks) {
array.add(ssid);
}
// JSON文字列にシリアル化
String result;
serializeJson(doc, result);
return result;
}
std::vector<String>
で提供されるWi-FiネットワークのSSIDのリストを受け取り、それらをJSON配列に変換し、最終的にはJSON配列を文字列としてシリアル化する。
serializeNetworks
関数を使用前のSSIDリスト
std::vector<String> networks = {
"Network_1",
"Network_2",
"Network_3",
"Network_4"
};
serializeNetworks
関数を使用した後のSSIDのJSON形式の表示:
["Network_1", "Network_2", "Network_3", "Network_4"]
BLEキャラクタリスティックを通じてデータを送信
シリアル化したWi-FiネットワークリストをBLEキャラクタリスティックを通じてFlutterアプリに送信する。
既存の BLEManager::CharacteristicCallbacks::onWrite
メソッド内にWi-Fiネットワークリストを送信する処理を追加する。
#include "WiFiConnection.h"
void BLEManager::CharacteristicCallbacks::onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue();
if (value.length() > 0) {
// Wi-Fiネットワークリストの要求を処理する
if (value == "request_wifi_list") {
// 利用可能なWi-Fiネットワークをスキャン
auto networks = scanNetworks();
// スキャン結果をシリアル化
String networkListJson = serializeNetworks(networks);
// シリアル化されたリストをBLE経由で送信
pCharacteristic->setValue(networkListJson.c_str());
pCharacteristic->notify();
Serial.println("WiFi network list sent via BLE");
} else if (value.find(':') != std::string::npos) {
// 既存のSSIDとパスワードによるWi-Fi接続処理
// ... 既存のWi-Fi接続処理 ...
}
// ... その他のコマンド処理 ...
}
}
アプリ側(Flutter)実装
Wi-Fiネットワークのリスト取得をESP32に要求(flutter_blue_plusライブラリ)
ESP32側のBLEManager.cppに実装したvalue == "request_wifi_list"
というコードは、ESP32のBLEキャラクタリスティックに書き込まれた値(value
)が特定のコマンド文字列(この場合は "request_wifi_list"
)と一致するかを判断するために使用される。
Flutterアプリ側で、ユーザーがWi-Fiネットワークリストの取得をリクエストした際に、アプリがESP32のBLEキャラクタリスティックに "request_wifi_list"
という文字列を書き込むコードを実装する必要がある。この書き込みが行われると、ESP32側で onWrite
コールバックがトリガーされ、受信した value
が "request_wifi_list"
と一致するかどうかが判断される。
指定された BluetoothDevice
に対して “request_wifi_list” コマンドを送信し、応答を受け取って Wi-Fi ネットワークのリストを List<String>
として返すrequestWifiList
メソッドをBLEService
クラスに追加する。
// ble_service.dart
class BLEService {
// ... 既存のコード ...
Future<List<String>> requestWifiList(BluetoothDevice device) async {
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);
// Wi-Fi リスト要求を送信
await characteristic.write(utf8.encode("request_wifi_list"));
// ESP32 からの応答を待つ
List<int> response = await characteristic.read().timeout(
Duration(seconds: timeoutSeconds),
onTimeout: () {
throw TimeoutException('Wi-Fi list not received within the timeout period');
},
);
// 応答をデコードしてリストに変換
String responseStr = utf8.decode(response);
final json = jsonDecode(responseStr);
List<String> wifiList = List<String>.from(json.map((model) => model.toString()));
return wifiList;
} catch (e) {
print('Error occurred while requesting Wi-Fi list: $e');
rethrow;
} finally {
await device.disconnect();
}
}
// ... 他のメソッド ...
}
requestWifiList メソッドの呼び出し
BLE接続後に表示される画面(BluetoothConnectionCompleteScreen
)の「ネットワーク接続へ進む」ボタンのonPressed
コールバック内で、BLEService
クラスのrequestWifiList
メソッドを呼び出す処理を追加する。
_requestWifiList
という新しいメソッドを作成して、その中でBLEService
のrequestWifiList
メソッドを非同期に呼び出し、Wi-Fiネットワークリストを要求。
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:clocky_app/services/ble_service.dart';
class BluetoothConnectionCompleteScreen extends StatefulWidget {
final BluetoothDevice device;
BluetoothConnectionCompleteScreen({required this.device});
@override
_BluetoothConnectionCompleteScreenState createState() =>
_BluetoothConnectionCompleteScreenState();
}
class _BluetoothConnectionCompleteScreenState
extends State<BluetoothConnectionCompleteScreen> {
// BLEServiceのインスタンスを作成
final bleService = BLEService();
void _requestWifiList() async {
try {
// Wi-Fiネットワークリストを要求する
List<String> wifiList = await bleService.requestWifiList(widget.device);
// コンソールにWi-Fiリストを表示
print('Wi-Fi List: $wifiList');
} catch (e) {
// エラー処理を行う
print(e);
}
}
@override
Widget build(BuildContext context) {
return BaseContainer(
children: <Widget>[
HeaderText("あなたの$productNameと接続しました"),
Spacing.h24(),
BodyText("引き続き$productNameをセットアップします"),
Spacing.h36(),
AppButton(
onPressed: () {
_requestWifiList();
},
text: 'ネットワーク接続へ進む',
),
],
);
}
}
一旦ここまでで、コンソールにWi-Fiリストが表示されるかどうか確認。
デバイス(ESP32)側
アプリ(Flutter)側
アプリでネットワーク接続ボタンを押す
→BLE経由でESP32にwifi scanのリクエストが飛ぶ(request_wifi_listという文字列で一致)
→BLE通信のレスポンスをBLE characteristic経由でアプリに返す
→アプリのコンソールに表示
までできていることを無事確認できた。あとは表示するだけ。
取得したWi-Fiネットワークのリストをリストビューとして表示
BluetoothConnectionCompleteScreen
ファイルの、_requestWifiList() 関数に、取得したWi-Fiリストとデバイス情報をWifiConnectionScreen
に渡して、その画面へナビゲートする処理を追加。
// bluetooth_connection_complete_screen.dart
void _requestWifiList() async {
try {
// Wi-Fiネットワークリストを要求する
List<String> wifiList = await bleService.requestWifiList(widget.device);
// コンソールにWi-Fiリストを表示
print('Wi-Fi List: $wifiList');
// 取得したWi-Fiリストを次の画面に渡してナビゲートする
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
WifiConnectionScreen(device: widget.device, wifiList: wifiList),
),
);
} catch (e) {
// エラー処理を行う
print(e);
}
}
// wifi_connection_screen.dart
class WifiConnectionScreen extends ConsumerStatefulWidget {
final BluetoothDevice device;
final List<String> wifiList;
WifiConnectionScreen({required this.device, required this.wifiList});
@override
_WifiConnectionScreenState createState() => _WifiConnectionScreenState();
}
class _WifiConnectionScreenState extends ConsumerState<WifiConnectionScreen> {
String? selectedSSID;
bool isLoading = false;
@override
Widget build(BuildContext context) {
return BaseContainer(
isLoading: isLoading,
children: <Widget>[
HeaderText('ネットワーク'),
Spacing.h36(),
BodyText('Wi-Fiネットワークを選択してください'),
Spacing.h16(),
Expanded(
child: ListView.builder(
itemCount: widget.wifiList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(widget.wifiList[index]),
onTap: () {
setState(() {
selectedSSID = widget.wifiList[index];
});
// パスワード入力画面に遷移
navigateToPasswordInputScreen(selectedSSID!);
},
selected: selectedSSID == widget.wifiList[index],
);
},
),
),
],
);
}
void navigateToPasswordInputScreen(String ssid) {
// パスワード入力画面に遷移する処理
// 例: Navigator.push(context, MaterialPageRoute(...));
}
}
このコードで一旦、今までの左の状態から、右の状態に変更できた!
ついでに下記の改修も行う。
- Wi-Fi接続でSSIDを選択するところとパスワードを入力する部分は画面遷移して別々に分離する
- ネットワークリスト表示のデザインを修正(Wi-Fiのセキュリティがある場合は鍵アイコンを表示し、Wi-Fiの信号強度に応じたアイコンを表示)
長くなったので、続きはこちらで
ではでは♪
コメント