11.4. アドバタイジングとスキャン

それまで一度も出会ったことのない2台のBLEデバイスは、まず互いを見つけなければなりません。ネットワーク接続は、共有プールから各デバイスにアドレスを割り当て、ルーターを介してどちらの側からも相手に到達できるようにすることでこれを解決します。BLEにはルーターも共有プールもなく、ほとんどのデバイス同士の間には事前の関係がまったく存在しません。Generic Access Profile(GAP)は、代わりにブロードキャストと受信のパターンで発見を解決します。一方の側がアドバタイズします。つまり、自分が何者であるかを記述した短いパケットを3つのアドバタイジングチャネルで一定間隔で送信します。もう一方の側がスキャンします。つまり、同じ3つのチャネルを掃引してそれらのパケットを受信します。

GAPはそのパターンを中心に4つの役割を定義しており、それぞれがアドバタイジングと受信の特定の組み合わせです。

11.4.1. 4つのGAPロール

2行2列のマトリックス。行には 「アドバタイズする」と「アドバタイズしない」のラベルが付いている。列には 「接続を受け入れる」と「接続を 受け入れない」のラベルが付いている。4つのセルには ロール名が入っている:Peripheral、Broadcaster、 Central、Observer。

4つのGAPロール。縦軸はデバイスがアドバタイズするかどうか、横軸は接続を受け入れる(または開始する)かどうかです。

  • peripheralは「私はここにいて、あなたは私に接続できます」と伝えるパケットをアドバタイズします。別のデバイスが接続を開くと、peripheralはアドバタイジングを停止し、GATTリクエストの処理を開始します。心拍数ストラップ、体温計、そしてセンサーとしての多くのカメラはperipheralとして動作します。

  • centralはperipheralをスキャンし、1つを選び、接続を開始します。接続後はクライアントとしてGATTを使用します。スマートフォン、ノートPC、そしてデータコレクターとして動作するカメラはcentralです。

  • broadcasterはアドバタイズしますが、接続を決して受け入れません。そのアドバタイジングペイロードそのものがデータであり、接続する対象は何もありません。iBeaconや店舗の在席ビーコンの多くはbroadcasterです。

  • observerはそれらのアドバタイズメントをスキャンしてペイロードを読み取りますが、やはり接続することは決してありません。近くのビーコンを受信し、聞き取った内容に基づいて動作するカメラはobserverです。

1台のデバイスは、同時に複数の役割を果たすことができます。カメラは、自身の状態を公開するperipheralであると同時に、近くのセンサーに接続するcentralにもなれます。無線がその作業を多重化します。

11.4.2. アドバタイジングパケットに含まれるもの

アドバタイジングパケットは小さく、ペイロードは31バイト、アドバタイザがスキャナーがその場で要求できるスキャンレスポンスも公開する場合は62バイトです。ペイロードは、短い型付きフィールドのリストです。

  • Flags。 接続可能かどうか、一般/限定発見可能。

  • Local name。 短く人間にわかりやすい文字列です。スマートフォンやノートPCのOSがBluetoothメニューに表示する名前です。

  • Service UUIDs。 デバイスがホストするGATTサービス識別子のリストで、スキャナーが先に接続することなく対応可能なperipheralを認識できるようにします。心拍数ストラップは0x180D(標準のHeart-RateサービスUUID)をアドバタイズし、スマートフォンの心拍数アプリはそれだけで、そのデバイスが接続する価値があると分かります。

  • Appearance。 Bluetoothのassigned-numbersリストからの16ビット値(sensor、generic media、generic watch、...)で、何を表示するかをcentralに伝えるヒントです。

  • Manufacturer-specific data。 会社IDが前置きされた自由形式のバイト列です。iBeaconはこのフィールドを使って自身のUUID、major、minorを運びます。カスタムアプリケーションはここに好きなものを入れられます。

アドバタイジングペイロードは余裕がありません。31バイトの制限により、何を含めるかの選択が実際の設計上の判断になります。長い人間可読の名前は、たちまちservice UUIDsの余地を残さなくなりかねません。aioble.advertise() APIはこれらを各キーワード引数として受け取り、バイト列を組み立て、メインパケットがいっぱいになると自動的にスキャンレスポンスにあふれさせます。

11.4.3. アクティブスキャンとパッシブスキャン

スキャナーは、アドバタイジングパケットを受信して届いた内容を解析するパッシブで動作することも、各アドバタイザにスキャンリクエストも送信して返ってくるスキャンレスポンスを解析するアクティブで動作することもできます。

パッシブスキャンは最初のアドバタイジングパケット(最大31バイト)のみを見ます。アクティブスキャンはそれを倍にします。スキャンレスポンスは、peripheralが収まりきらなかったフィールドに使える別の31バイトです。アクティブスキャンはまた、スキャナーが送信しアドバタイザが追加のパケットを送信するため両側で電力を消費します。したがって、デフォルトではなく選択肢です。

aioble APIでは、aioble.scan()active=Trueがモードを切り替え、各ScanResultは結合されたadv_dataresp_dataに加えて、バイトレベルの解析を隠すresult.name()result.services()などのヘルパーを公開します。

注釈

adv_dataresp_data 属性は、生のアドバタイジングおよびスキャンレスポンスのペイロード(bytes)です。ヘルパーであるname()services()manufacturer() は、一般的な標準フィールドをカバーし、99%のケースで正しい選択肢です。生のバイト列を使うのは、ヘルパーが解析しないベンダーフィールド(Eddystone URL、iBeaconのUUID/major/minor、カスタムアドバタイジングタイプ)が必要な場合のみにしてください。バイトレイアウトは標準のTLV形式で、各フィールドはlength, type, value...です。

11.4.4. アドバタイジング間隔

peripheralがどれくらいの頻度でブロードキャストするかは、電力と発見レイテンシのトレードオフです。20 msごとに送出されるアドバートはスキャナーにほぼ即座に拾われますが、無線を稼働させ続けバッテリーを消耗させます。1秒ごとのアドバートはほとんど電力を使いませんが、スキャナーがそのデバイスに気づくのが遅くなります。

aioble.advertise()interval_usはマイクロ秒単位で間隔を設定します。

  • 20,000~100,000 us(20 ms~100 ms)。高速なペアリング、アプリが素早い応答を期待する場合、電源に接続されたデバイス。

  • 250,000~1,000,000 us(250 ms~1 s)。電力を消費せずに発見可能でありたいバッテリー駆動のperipheralにとって妥当なデフォルトです。

  • 1,000,000 usより上。低速なバックグラウンドブロードキャスト、数秒ごとに位置の更新を送るビーコン。

スキャナー側には独自のつまみがあります。aioble.scan()interval_uswindow_us(スキャナーが無線を起動する頻度と、毎回どれくらいの時間受信するか)を受け取ります。デフォルトで問題ありません。一般的な変更としては、バッテリーが問題にならない場合に連続スキャンのために両方を等しく設定することくらいです。

11.4.5. 接続不要のパターン -- broadcasterとobserver

ペリフェラルとして動作するセントラルとして動作する のページでは、APIの接続可能な形態、つまりperipheralが接続を受け入れ、両者がGATTを通じてデータを交換する形態を順を追って説明しています。もう一方の形態は接続不要です。broadcasterはペイロードをアドバタイズメントとして送信し、範囲内のあらゆるobserverが接続することなくそれを読み取れます。ビーコン、在席センサー、一方向テレメトリはすべてここに属します。

broadcasterはconnectable=Falseを指定したaioble.advertise() です。Manufacturer-specific dataがペイロードを運びます:

import aioble
import asyncio
import struct

_COMPANY_ID = const(0xFFFF)                # 0xFFFF is "no specific vendor"

async def beacon():
    seq = 0
    while True:
        seq = (seq + 1) & 0xFFFF
        payload = struct.pack("<H", seq)
        await aioble.advertise(
            interval_us=500000,
            connectable=False,
            name="openmv-beacon",
            manufacturer=(_COMPANY_ID, payload),
            timeout_ms=1000,                # one cycle, then loop
        )

asyncio.run(beacon())

timeout_msキーワードは1秒後にadvertise呼び出しを終了させます。外側のループは次のシーケンス番号でそれを再発行するので、リスナーは新しいデータを見ます。connectable=Falseフラグが、そのアドバタイズメントをbroadcaster形式にするものです。カメラは接続リクエストが届いても応答しません。

observerは、それに対応する読み取り専用のスキャナーです。aioble.scan() を永続的に実行し、届いたアドバタイズメントを解析し、connect() を決して呼び出しません:

import aioble
import asyncio

_COMPANY_ID = const(0xFFFF)

async def watch():
    async with aioble.scan(duration_ms=0, active=False) as scanner:
        async for result in scanner:
            for company, data in result.manufacturer(filter=_COMPANY_ID):
                print(result.device.addr_hex(),
                      "rssi", result.rssi, "data", data)

asyncio.run(watch())

duration_ms=0はコンテキストマネージャが終了するまでスキャンします。active=Falseはobserver自身の無線を沈黙させ(スキャンレスポンスのリクエストなし)、最小の消費電力にします。manufacturer()filter=引数は、会社IDに一致しないすべてのアドバタイズメントを破棄するので、ループはbroadcasterのトラフィックに対してのみ発火します。

11.4.6. 発見から接続へ

centralが通信する相手のperipheralを選ぶと、受信を停止し、peripheralが最後に使用したアドバタイジングチャネルで接続リクエストを送信し、両者はリンク層のホッピングするデータチャネルへ移行します。peripheralはこの時点で通常アドバタイジングを停止します。次に何が起こるか(接続パラメータ、GATTディスカバリー、リンクの寿命)は接続 にあります。