11.8. aioble モジュール

Bluetooth Core 仕様は、2つの MicroPython モジュールに対応する語彙を提供します。

  • bluetooth ― BLE コントローラへの低レベルバインディングです。同期的で、IRQ スタイルのコールバックを通じてイベント駆動され、バイトバッファ、ハンドル、そして素の GATT プリミティブを中心に構成されています。プロトコルを、Python アプリケーションが利用したい形ではなく、ありのままに公開します。

  • aioblebluetooth の上に Python で書かれた、より高レベルのラッパーです。あらゆるリモート操作を asyncio コルーチンに変え、あらゆる BLE オブジェクト(サービス、キャラクタリスティック、接続、スキャン結果、L2CAP チャネル)を扱いやすい Python クラスに変えます。スキャンは非同期イテレータに、接続は非同期コンテキストマネージャに、通知は待機可能なものになります。

11.8.1. 低レベルモジュールに手を伸ばすべきとき

bluetooth は、依然として次の2つの限られたケースでは正しい答えです。

  • aioble 自体が作られているのと同じ種類のコード、つまりプロトコルに対する IRQ レベルの制御を必要とする新しいパターンを書いている場合。

  • aioble パッケージが利用できないハードウェアターゲット上で動作させており、コントローラを薄くラップするシムが唯一の選択肢である場合。

あらゆるカメラアプリケーションにとって、aioble が正しい答えです。

11.8.2. aioble プログラムの構成要素

あらゆる aioble ベースのアプリケーションは、どのロールを担うかにかかわらず、いくつかの可動部分の小さな集合を持っています。

  • 長時間稼働する asyncio イベントループ。aioble のすべてはコルーチンであるため、アプリケーションは単一のイベントループ上の1つ以上のタスクとして構成されます。ループ、タスク、例外の詳細については Asyncio を参照してください。

  • オンになっている無線。aioble は最初の使用時に暗黙的に BLE 無線を起動しますが、aioble.config()(無線が起動していることを確認したうえで bluetooth.BLE.config() に転送します)で明示的に制御することも、aioble.stop() でシャットダウンすることもできます。

  • 同時に進行中の1つ以上のロール。ペリフェラル側では、登録された GATT サービスのセット(aioble.register_services() を参照)と、稼働中の aioble.advertise() コルーチンです。セントラル側では、稼働中の aioble.scan() イテレータ、または保留中の aioble.Device.connect() です。無線がその作業を多重化し、アプリケーションは各ロールを独立したタスクとして見ます。

11.8.3. 最小限のペリフェラル

最も小さく実用的な aioble プログラム、つまり単一の読み取り専用キャラクタリスティックをアドバタイズするペリフェラルは、短く書けます:

import aioble
import asyncio
import bluetooth

SERVICE_UUID = bluetooth.UUID(0x181A)            # Environmental Sensing
TEMP_UUID = bluetooth.UUID(0x2A6E)               # Temperature

service = aioble.Service(SERVICE_UUID)
temp = aioble.Characteristic(service, TEMP_UUID, read=True)
aioble.register_services(service)

async def main():
    while True:
        conn = await aioble.advertise(
            interval_us=250000,
            name="openmv-temp",
            services=[SERVICE_UUID],
        )
        async with conn:
            await conn.disconnected()

asyncio.run(main())

接続して一度読み取るだけのセントラルも、同様に短く書けます:

import aioble
import asyncio
import bluetooth

SERVICE_UUID = bluetooth.UUID(0x181A)
TEMP_UUID = bluetooth.UUID(0x2A6E)

async def main():
    device = None
    async with aioble.scan(duration_ms=5000, active=True) as scanner:
        async for result in scanner:
            if SERVICE_UUID in result.services():
                device = result.device
                break
    if device is None:
        return

    async with await device.connect() as conn:
        service = await conn.service(SERVICE_UUID)
        char = await service.characteristic(TEMP_UUID)
        print(await char.read())

asyncio.run(main())

どちらのプログラムも約15行で、「無線がオフ」から「有用な作業が完了」までの全フローをカバーしています。

11.8.4. 無線をオフにする

バッテリー駆動のカメラでは、BLE 無線が予算の中で最も大きな随意の消費源です。重要なつまみが2つあります。

1つ目は暗黙的なものです。aioble は最初の使用時に無線を起動し、無線はスケジュールされたイベント(アドバタイズのバースト、接続イベント、スキャンウィンドウ)の合間に自動的にスリープします。aioble.advertise() / aioble.scan() でより長い間隔を選び、connect() の時点でより長い接続間隔に合意することで、それに比例して無線がオフになっている時間が増えます。アドバタイジングとスキャン のアドバタイズの表が、ここでの実用的なガイドになります。

2つ目は明示的なシャットダウンです:

import aioble

await do_burst_of_ble_work()
aioble.stop()                             # radio deactivated; in-flight tasks unwound
await asyncio.sleep(60)                   # sleep with the radio off
# ... next aioble call brings the radio back up automatically

aioble.stop() は基盤となる BLE 無線を非アクティブにし、進行中のあらゆるものを取り壊します。開いている接続は切断され、スキャナーとアドバタイザーはキャンセルされ、L2CAP チャネルはクローズされます。それらの操作を待機していたコルーチンは通常の例外(DeviceDisconnectedError など)を送出します。これは、周囲の async with ブロックが想定して書かれたクリーンアップの仕組みです。その後に aioble のコルーチンを呼び出すと、無線がコールドな状態から再び起動します。

周期的に動作するバッテリー駆動のセンサーカメラの典型的なパターンは次のとおりです。

  • スケジュール(タイマー、モーションセンサー、ボタン)に従って起動する。

  • BLE 作業のバーストを実行する。すなわち、アドバタイズし、接続を受け入れ、値をプッシュし、切断する。

  • aioble.stop() を呼び出し、次の起動までスリープする。

11.8.5. aioble が行わないこと

aioble は意図的に GATT、GAP、L2CAP、すなわちアプリケーションが使用する層をカバーしています。次の3つは対象外です。

  • リンク層より下のあらゆるもの。 チャネル選択、周波数ホッピング、パケットの確認応答、リンク層の暗号化は、すべて BLE ポートとコントローラのシリコン内部で行われます。aioble はそのレベルのフックを公開しません。

  • Classic Bluetooth。 aioble は BLE 専用です。オーディオリンク、RFCOMM、A2DP、その他の classic プロファイルの機能は API の一部ではありません。

  • Bluetooth Mesh。 Bluetooth SIG のメッシュネットワーキング層(BLE アドバタイズの上に構築される別個のスタック)は、カメラ上には実装されていません。カメラはアドバタイズと観測は行えますが、メッシュネットワークの relay / friend / proxy のロールには参加できません。

11.8.6. 例外

aioble からは4種類の例外型が送出されます。それぞれ、何か問題が起きたときに操作を待機していたコルーチンの内部から発火します。それらが伝播すると async with ブロックは正常にアンワインドされます。

  • aioble.DeviceDisconnectedError ― GATT 操作(readwritenotifiedindicatedsubscribeexchange_mtu など)が進行中に、ピアへの BLE リンクが切断されました。待機していたコルーチンの内部で送出されます。圧倒的に最も一般的な例外です。切断時に再接続すべきあらゆるコードで捕捉してください。

  • aioble.GattError ― GATT 操作はピアに到達しましたが、ゼロ以外の ATT ステータスで完了しました(応答付き書き込みの拒否、indicate が確認応答されない、読み取り不許可など)。ステータスコードは例外の _status 属性にあります。

  • aioble.L2CAPDisconnectedErrorsend()recvinto()、または flush() が進行中に L2CAP チャネルが切断されました。いずれかの側がチャネルをクローズしたか、基盤となる GAP 接続が失われた可能性があります。

  • aioble.L2CAPConnectionError ― リスナーが拒否したか、コントローラがチャネルのセットアップに失敗したときに l2cap_connect() から送出されます。Bluetooth のステータスコードが最初の位置引数です。

明示的な timeout_ms を取る操作(connect / discovery / read / write / pair の各呼び出し、およびラッパーとしての timeout())は、操作が完了する前に期限が経過すると、さらに asyncio から asyncio.TimeoutError を送出します。