1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

実践!ESP32とWindowsでBLE通信を「限界まで」高速化する

Posted at

はじめに:BLEは「遅い」のか?高速化への挑戦

Bluetooth Low Energy (BLE) は、その名の通り「低消費電力」を最大の強みとする無線通信技術です。IoTデバイスのバッテリー寿命を飛躍的に延ばし、ウェアラブル機器やスマートホームの普及に大きく貢献してきました。しかし、多くの開発者がBLEに対して抱く印象は、「便利だけど、通信速度はあまり期待できない」というものではないでしょうか。
確かに、BLEはWi-Fiのような高速通信を目的として設計されていません。しかし、BLEの通信速度を「限界まで」引き上げることができたら、どんな新しい可能性が生まれるでしょうか?リアルタイム性が求められるセンサーデータのストリーミング、高頻度な制御コマンドの送受信、あるいは短時間での大量データ転送など、その応用範囲は大きく広がるはずです。
本記事では、この問いに答えるべく、ESP32とWindows PC間でのBLE通信において、その速度をどこまで向上させられるのかを検証しました。BLEの「低消費電力」という設計思想を一時的に「無視」し、純粋な「高速化」に焦点を当てた、実践的な技術検証の記録です。
この記事は、以下のような方々におすすめです。

  • BLEの基本的な知識があり、さらに一歩踏み込んだ高速化に興味がある方
  • ESP32とWindows間のBLE通信で速度に課題を感じている方
  • 実験を通じて技術的なボトルネックを特定し、解決するプロセスに興味がある方

フェーズ1:手探りのBLE通信確立 - スマートフォンで「測定」

未知の領域に挑む際、私は常に「既知をベースに漸進的に進める」というアプローチを重視しています。ESP32とWindows間のBLE通信高速化という目標に対し、いきなりESPとWindowsクライアントを構築するのではなく、まずは「確実に通信ができる」という状態を確立し、BLEの基本的な挙動を理解することから始めました。そのために選んだのが、手軽に利用できるスマートフォンとNordic社の「nRF Connect」アプリです。

ESP32側の実装:Notifyの選択

ESP32はBLEペリフェラルとして動作させ、定期的にデータを送信する設定にしました。ここで重要なのは、データ送信に「Notify(通知)」を選択した点です。Notifyは、セントラル側からの要求なしに、ペリフェラル側からデータ更新を能動的に通知できるため、高速なデータストリーミングに適しています。今回は、初期検証として4バイトのデータを1000msec(1秒)間隔でNotifyするシンプルな実装を行いました。

ESP32側のサンプルコード(抜粋)
// BLEの初期化、サービス・キャラクタリスティックの定義は省略しますが、
// Notify属性を持つキャラクタリスティックを設定することが重要です。

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// 定義したUUIDは、後続のWindowsクライアントでも使用します
#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

BLECharacteristic *pCharacteristic;
uint8_t value[1] = {0x01, 0x02, 0x03, 0x04}; // 送信するデータ

void setup() {
  Serial.begin(115200);
  BLEDevice::init("ESP32_BLE"); // デバイス名を「ESP32_BLE」に設定
  BLEServer *pServer = BLEDevice::createServer();
  BLEService *pService = pServer->createService(SERVICE_UUID);
  pCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID,
                      BLECharacteristic::PROPERTY_READ |
                      BLECharacteristic::PROPERTY_WRITE |
                      BLECharacteristic::PROPERTY_NOTIFY // Notifyを有効化
                    );
  pCharacteristic->addDescriptor(new BLE2902()); // Notifyを有効にするためのディスクリプタ

  pService->start();
  pServer->getAdvertising()->start(); // アドバタイズ開始
  Serial.println("Waiting a client connection to notify...");
}

void loop() {
  delay(1000); // 1秒ごとにデータを更新
  value++;
  pCharacteristic->setValue(value, 4);
  pCharacteristic->notify(); // Notifyを送信
  Serial.print("Notified: ");
  Serial.print(value);
  Serial.print(", ");
  Serial.print(value[2]);
  Serial.print(", ");
  Serial.print(value[3]);
  Serial.print(", ");
  Serial.println(value[4]);
}

nRF Connectによる検証:BLE通信の「見える化」

nRF Connectアプリは、BLEデバイスのGATTサービス構造を視覚的に表示し、Notifyデータの受信をリアルタイムで確認できるため、初期デバッグに非常に有用です。ESP32が正しくアドバタイズし、接続後Notifyが機能していることを確認できました。この段階で、BLE通信の基本的な「開通」が確認できたことは、次のステップへ進むための自信となりました。
image.png

フェーズ2:Windowsクライアントへの移行 - Python/BLEAKで「計測の足場」を築く

スマートフォンでの通信確認ができたところで、いよいよ本命のWindowsクライアント開発に着手しました。最終的な高速化の検証はWindows環境で行うため、nRF Connectの代替となるアプリケーションが必要です。

PythonとBLEAKの選定理由

開発言語には、その手軽さと豊富なライブラリエコシステムからPythonを選択しました。BLEフレームワークとしては、Windows (WinRT)、macOS (Core Bluetooth)、Linux (BlueZ) など複数のプラットフォームに対応し、非同期処理をサポートするBleakが最適と判断しました。これにより、将来的なクロスプラットフォーム展開の可能性も視野に入れつつ、Windows環境でのBLE通信を効率的に実装できます。

受信データ測定ロジックの構築

単にデータを受信するだけでなく、通信速度を正確に測定するためのロジックを組み込むことが重要です。以下の要件を満たすように設計しました。

  • コマンドラインインターフェースで動作
  • 特定のデバイス名("ESP32_BLE")をスキャンし、自動接続
  • Notify属性を持つキャラクタリスティックを特定し、購読
  • 受信したデータ量、受信回数、経過時間を計測し、1秒あたりの受信ボーレート(bps)を算出
Windowsクライアント側のPythonサンプルコード(抜粋)
import asyncio
from bleak import BleakClient, BleakScanner
import time

SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
CHARACTERISTIC_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8"

# 測定用グローバル変数
received_data_count = 0
received_bytes_total = 0
start_time = 0

def notification_handler(sender, data):
    """Notify受信時のコールバック関数:ここでデータと時間を計測"""
    global received_data_count, received_bytes_total, start_time
    if start_time == 0:
        start_time = time.time() # 最初のデータ受信時に計測開始

    received_data_count += 1
    received_bytes_total += len(data)
    current_time = time.time()
    elapsed_time = current_time - start_time

    # 1秒ごとに統計情報を表示し、リセット
    if elapsed_time >= 1.0:
        print(f"--- 1秒間の統計情報 ---")
        print(f"経過時間: {elapsed_time:.2f}")
        print(f"受信回数: {received_data_count}")
        print(f"受信バイト数: {received_bytes_total}バイト")
        print(f"平均ボーレート: {received_bytes_total * 8 / elapsed_time:.2f} bps")
        print(f"平均受信間隔: {elapsed_time / received_data_count * 1000:.2f} msec/回")
        print("------------------------")
        received_data_count = 0
        received_bytes_total = 0
        start_time = 0

async def main():
    print("ESP32_BLEデバイスをスキャン中...")
    device = await BleakScanner.find_device_by_name("ESP32_BLE")
    if not device:
        print("ESP32_BLEデバイスが見つかりませんでした。")
        return

    print(f"デバイスが見つかりました: {device.name} ({device.address})")

    async with BleakClient(device.address) as client:
        if client.is_connected:
            print(f"接続成功! {device.name}")
            await client.start_notify(CHARACTERISTIC_UUID, notification_handler)
            print("Notify受信を開始しました。Ctrl+Cで終了します。")
            while True:
                await asyncio.sleep(1) # メインループを維持
        else:
            print("接続に失敗しました。")

if __name__ == "__main__":
    asyncio.run(main())

この段階で、ESP32からのNotifyがWindowsクライアントで正確に受信され、測定ロジックも期待通りに動作することを確認しました。これで、いよいよ本格的な速度検証の「足場」が整いました。

フェーズ3:通信速度の「壁」 - ESP32 vs Windows、ボトルネックはどちらか

いよいよ、通信速度のボトルネックがどこにあるのかを特定するための実験です。当初の仮説は「ESP32側の処理能力がボトルネックになるだろう」というものでした。この仮説を検証するため、以下の2つの実験を設計しました。

実験1:1回あたりの送信データ量を変化させる

まず、ESP32から送信する1回あたりのデータ量を4バイトから512バイトまで段階的に増やし、Windows側での受信ボーレート(bps)がどのように変化するかを測定しました。送信間隔は1000msec(1秒)に固定しています。
[グラフ1: 送信データ量と受信ボーレートの関係(最適化前)]
image.png
グラフを見ると、送信データ量が増えるにつれて受信ボーレートも増加する傾向が見られます。これは直感的にも理解できます。しかし、興味深いのは、通信開始直後のボーレートが送信ボーレートよりも一時的に高くなる点です。これは、Windows側のBLEスタックがデータをバッファリングし、まとめてアプリケーションに渡している可能性を示しています。

実験2:送信間隔を変化させる

次に、1回あたりの送信データ量を512バイトに固定し、ESP32からの送信間隔を1000msecから7.5msecまで徐々に短くしていきました。
[グラフ2: 送信間隔と受信ボーレートの関係(最適化前)]
image.png
この実験結果は「まさかの結果」であり、当初の仮説を覆すものでした。送信間隔が37.5msecまでは比較的安定していましたが、20msec、15msec、7.5msecと間隔を短くしていくと、受信ボーレートが不安定になり、特に測定開始直後の変動が激しくなりました。
そして最も重要な発見は、ESP32側には処理の遅れがほとんど見られなかったことです。これは、通信のボトルネックがESP32の処理能力ではなく、Windows側のBLEスタックやアプリケーション層でのデータ処理能力にある可能性が高いことを示唆しています。つまり、ESP32はもっと速くデータを送れるのに、Windows側がそれを受け止めきれていない、という状況が浮き彫りになったのです。

フェーズ4:Windowsの「隠れた設定」を解放する - ThroughputOptimizedの効果

ボトルネックがWindows側にあると判明した以上、次なる課題は「どうすればWindowsのBLE通信性能を最大限に引き出せるか」です。そこで、マイクロソフトの公式ドキュメントを掘り下げた結果、BLEの接続パラメータを最適化する「隠れた設定」を発見しました。
Windows 10以降のWinRT APIには、BluetoothLEDevice.RequestPreferredConnectionParametersメソッドが存在し、これにBluetoothLEPreferredConnectionParameters.ThroughputOptimizedを指定することで、スループットを優先した接続パラメータを要求できることが分かりました。これは、BLEの接続インターバルやスレーブレイテンシなどのパラメータを、OSが自動的に高速通信に適した値に調整してくれることを意味します。
Bleakは内部的にWinRT APIを使用しているため、このオプションを適用したカスタムクライアントクラスを実装しました。

Windowsクライアント側のPythonサンプルコード(接続パラメータ最適化版)
import asyncio
from bleak import BleakClient, BleakScanner
import time
from winrt.windows.devices.bluetooth import BluetoothLEDevice
from winrt.windows.devices.bluetooth.background import BluetoothLEPreferredConnectionParameters

#... (UUIDの定義やnotification_handler関数はフェーズ2と同じ)...

class HighSpeedBleakClient(BleakClient):
    """スループット最適化された接続パラメータを要求するBleakClientの拡張"""
    async def __aenter__(self):
        await super().__aenter__()
        # Windows環境でのみ適用されるWinRT固有の設定
        if self.is_connected and self.address.startswith("XX:XX:XX"): # WindowsのMACアドレス形式で判定
            # BleakClientの内部にある_deviceオブジェクトにアクセスし、接続パラメータを要求
            # この部分はBleakの内部実装に依存するため、将来的な互換性に注意が必要です。
            if hasattr(self, "_device") and isinstance(self._device, BluetoothLEDevice):
                print("接続パラメータをスループット最適化に設定中...")
                self._device.request_preferred_connection_parameters(
                    BluetoothLEPreferredConnectionParameters.throughput_optimized
                )
                print("接続パラメータ設定完了。")
        return self

async def main_optimized():
    print("ESP32_BLEデバイスをスキャン中...")
    device = await BleakScanner.find_device_by_name("ESP32_BLE")
    if not device:
        print("ESP32_BLEデバイスが見つかりませんでした。")
        return

    print(f"デバイスが見つかりました: {device.name} ({device.address})")

    # カスタムクライアントを使用
    async with HighSpeedBleakClient(device.address) as client:
        if client.is_connected:
            print(f"接続成功! {device.name}")
            await client.start_notify(CHARACTERISTIC_UUID, notification_handler)
            print("Notify受信を開始しました。Ctrl+Cで終了します。")
            while True:
                await asyncio.sleep(1)
        else:
            print("接続に失敗しました。")

if __name__ == "__main__":
    asyncio.run(main_optimized())

補足: 上記のHighSpeedBleakClientの実装はBleakの内部実装に依存する部分があるためBleakのバージョンアップによって動作しなくなる可能性がありますあくまで概念を示すためのサンプルとしてご参照ください

最適化の結果:BLE高速化の「ブレイクスルー」

この最適化を適用した結果、通信速度は劇的に向上し、検討当初の速度からはまさに「ブレイクスルー」と呼べるものでした。
[グラフ3: 接続パラメータ最適化前後の通信速度比較]
image.png
グラフが示す通り、特に送信間隔が短い領域(20msec、15msec)において、受信ボーレートが安定し、かつ大幅に向上しました。これにより、Windows側がボトルネックであったという仮説が完全に裏付けられました。
最終的に、安定した通信速度として、** 20msecで512バイトのデータを送信した場合、約25600バイト/秒(約204800bps)**を達成することができました。これは、BLEの「低消費電力」という特性を維持しつつも、特定のユースケースにおいては十分な高速通信が実現可能であることを示しています。

結論:BLE高速化の「実践的」知見と次なる挑戦

本記事を通じて、ESP32とWindows間のBLE通信高速化という挑戦的なテーマに取り組み、その過程と結果を詳細に共有しました。
今回の検証で得られた最も「実践的」知見は以下の通りです。

  • BLE通信のボトルネックは、必ずしもハードウェアやデバイス側の処理能力だけにあるわけではなく、OSのBLEスタックやアプリケーション層の最適化が極めて重要である
  • Windows環境においては、WinRT APIのThroughputOptimizedオプションが、BLE通信速度向上に絶大な効果を発揮する
  • 体系的な実験とデータに基づいた分析は、見えないボトルネックを特定し、効果的な解決策を見出すための強力な手段となる

今回の検証は、BLEの「低消費電力」という特性を一時的に脇に置いたものでしたが、この知見は、リアルタイムセンサーデータの収集、高頻度な制御信号の送受信、あるいは短時間での大量データ転送を必要とするIoTアプリケーション開発において、非常に有用な示唆を与えるものです。例えば、スマートホームにおける瞬時のデバイス連携や、ヘルスケア機器からのバイタルデータ連続取得など、その応用範囲は広がります。
この検証結果が、あなたのBLE開発における「常識」を打ち破り、新たな可能性を切り開く一助となれば幸いです。

編集後記

この記事は元記事を生成AIに入力し、内容はそのままに読者への伝わりやすさを重視して整えるよう依頼、過度な表現を筆者が調整したものです。読みやすさが上がったと感じたので、出稿前に必ず生成AIに整えさせるプロセスを取り入れるのは今後必須かも思わせるな経験となりました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?