方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【ESP32】Wi-Fi接続を直接入力ではなく、候補リストから選べるようにする。

この記事は約22分で読めます。

はじめに

現在、方言を話すおしゃべり猫型ロボット「ミーア」を開発中。

https://mia-cat.com

現状のミーアはESP32とアプリの接続を、まずはBluetoothで接続した後に、Wi-Fi の接続をアプリから BLE 経由で行っている。

ただ、Wi-Fi接続のSSIDとパスワードを直書きする仕様になっていて、これだと、ユーザーからするととても面倒なため、接続可能なWi-Fiを探索して選べる形式に変更したい。

デバイス(ESP32)側実装

Wi-Fiネットワークの探索機能をESP32に実装

ESP32が利用可能なWi-Fiネットワークをスキャンしてリストを作成し、そのリストをBLE経由でアプリに送信するようにファームウェアを実装する。

現在のPlatformIOでのWifi接続部分のコードはこちら

C++
// WifiConnection.h
#pragma once

#include <WiFi.h>

bool setupWiFi(const char *ssid, const char *password);
C++
// 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機能に関する解説はこちらがわかりやすい

https://randomnerdtutorials.com/esp32-useful-wi-fi-functions-arduino/#:~:text=The%20ESP32%20can%20scan%20nearby,range%20of%20your%20ESP32%20board

ESP32のWi-Fiライブラリは、WiFi.scanNetworks()という、利用可能なWi-Fiネットワークリスト表示してくれる関数がある。

今回は、ESP32をWi-Fiステーションモードに設定する。これにより、ESP32はWi-Fiアクセスポイントとして動作するのではなく、利用可能なWi-Fiネットワークをスキャンしたり、特定のネットワークに接続したりできるようになる。

WifiConnectionファイルを下記のように修正し、利用可能なWi-Fiネットワークリストを返す関数を追加する。

C++
// WifiConnection.h
#pragma once

#include <WiFi.h>
#include <vector>

bool setupWiFi(const char *ssid, const char *password);
std::vector<String> scanNetworks();
C++
// 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ネットワークの結果をシリアルモニタに出力して確かめてみる。

C++
// 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、プロパティ、値などを含む。

C++
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データを簡単に扱うためのライブラリ。

C++
#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リスト

C++
std::vector<String> networks = {
    "Network_1",
    "Network_2",
    "Network_3",
    "Network_4"
};

serializeNetworks関数を使用した後のSSIDのJSON形式の表示:

C++
["Network_1", "Network_2", "Network_3", "Network_4"]

BLEキャラクタリスティックを通じてデータを送信

シリアル化したWi-FiネットワークリストをBLEキャラクタリスティックを通じてFlutterアプリに送信する。

既存の BLEManager::CharacteristicCallbacks::onWrite メソッド内にWi-Fiネットワークリストを送信する処理を追加する。

C++
#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 クラスに追加する。

Dart
// 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という新しいメソッドを作成して、その中でBLEServicerequestWifiListメソッドを非同期に呼び出し、Wi-Fiネットワークリストを要求。

Dart
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に渡して、その画面へナビゲートする処理を追加。

Dart
// 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);
    }
  }
Dart
// 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の信号強度に応じたアイコンを表示)

長くなったので、続きはこちらで

ではでは♪

コメント

タイトルとURLをコピーしました