11.4. 廣告與掃描

兩個從未碰過面的 BLE 裝置必須先找到彼此。網路是透過從共享位址池中配發位址給每個裝置、並讓任一端能透過路由器觸及另一端來解決這個問題。BLE 沒有路由器、沒有共享位址池,而且——在大多數成對的裝置之間——根本沒有任何先前的關係。Generic Access Profile(GAP)改以廣播並聆聽的模式來解決探索問題。一端 廣告(advertises)——它以固定間隔在三個廣告頻道上發送一個簡短封包,描述自己的身分。另一端 掃描(scans)——它在這相同的三個頻道上來回聆聽那些封包。

GAP 圍繞這個模式定義了四種角色,每一種都是廣告與聆聽的特定組合。

11.4.1. 四種 GAP 角色

一個二乘二的矩陣。列分別標示為 「廣告」與「不廣告」。欄分別標示 為「接受連線」與「不接受連線」。 這四個儲存格內含角色名稱:周邊(Peripheral)、 廣播者(Broadcaster)、中心(Central)、觀察者(Observer)。

四種 GAP 角色。縱軸代表裝置是否廣告;橫軸代表裝置是否接受(或發起)連線。

  • 周邊裝置(peripheral) 廣告那些表示「我在這裡,你可以連接我」的封包。當另一個裝置開啟連線時,周邊裝置停止廣告並開始處理 GATT 請求。心率帶、溫度計,以及大多數作為感測器的相機,都扮演周邊裝置的角色。

  • 中心裝置(central) 掃描周邊裝置、挑選一個並發起連線。連接之後,它以用戶端身分使用 GATT。手機、筆記型電腦,以及作為資料收集者的相機,都是中心裝置。

  • 廣播者(broadcaster) 進行廣告,但從不接受連線。它的廣告酬載 本身就是 資料——根本沒有東西可供連接。iBeacon 與大多數賣場到場偵測信標都是廣播者。

  • 觀察者(observer) 掃描那些廣告並讀取其酬載,同樣完全不進行連接。一台聆聽附近信標並根據所聽到的內容做出動作的相機,就是觀察者。

單一裝置可以同時扮演不只一種角色——一台相機可以既是發佈自身狀態的周邊裝置,也是 連接附近感測器的中心裝置。無線電會多工處理這些工作。

11.4.2. 廣告封包包含什麼

廣告封包很小:31 位元組的酬載,若廣告者還發佈了掃描者可即時請求的 掃描回應(scan response),則為 62 位元組。酬載是一份簡短的具型別欄位清單:

  • 旗標(Flags)。 是否可連接、一般/有限可探索。

  • 本機名稱(Local name)。 一個簡短、易於人類辨識的字串——手機或筆記型電腦上的作業系統在其藍牙選單中顯示的名稱。

  • 服務 UUID(Service UUIDs)。 該裝置所代管的 GATT 服務識別碼清單,使掃描者不必先連接即可辨識出具備所需功能的周邊裝置。心率帶廣告 0x180D——標準的心率服務 UUID——光憑這一點,手機上的心率應用程式便知道該裝置值得連接。

  • 外觀(Appearance)。 一個取自藍牙指定編號清單的 16 位元值(感測器、通用媒體、通用手錶……)——給中心裝置一個關於顯示內容的提示。

  • 製造商專屬資料(Manufacturer-specific data)。 以公司 ID 為前綴的自由格式位元組。iBeacon 使用這個欄位來攜帶其 UUID、major 與 minor;自訂應用程式則可在這裡放入任何想放的內容。

廣告酬載很緊湊。31 位元組的限制使得選擇要包含哪些內容成了一個實際的設計決策——一個冗長的人類可讀名稱很快就會讓服務 UUID 無空間可容。aioble.advertise() API 將上述每一項作為關鍵字引數接收,並為你組裝這些位元組,當主封包填滿時,會自動溢出至掃描回應中。

11.4.3. 主動與被動掃描

掃描者可以 被動(passive) 執行,僅聆聽廣告封包並解析所收到的內容;也可以 主動(active) 執行,此時它還會向每個廣告者發送一個 掃描請求(scan request),並解析回傳的掃描回應。

被動掃描只會看到最初的廣告封包(最多 31 位元組)。主動掃描則使其加倍——掃描回應是另外 31 位元組,周邊裝置可用於放置未能容納的欄位。主動掃描也會在兩端耗費功率,因為掃描者要發送、廣告者要多發送一個封包,因此它是一項選擇而非預設行為。

aioble API 中,aioble.scan() 上的 active=True 可切換模式,而每個 ScanResult 都會公開合併後的 adv_data 加上 resp_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. 廣告間隔

周邊裝置多久廣播一次,是功率與探索延遲之間的權衡。每隔 20 ms 發出的廣告幾乎會立即被掃描者擷取,但會讓無線電保持忙碌並耗盡電池;每隔一秒發出的廣告幾乎不耗電,但會讓掃描者要花更久才會掃描到該裝置。

aioble.advertise() 上的 interval_us 以微秒為單位設定間隔:

  • 20,000 至 100,000 us(20 ms - 100 ms)——快速配對、應用程式預期快速回應、有外接電源的裝置。

  • 250,000 至 1,000,000 us(250 ms - 1 s)——對於想要保持可被探索、又不想耗盡電量的電池供電周邊裝置而言,這是一個合理的預設值。

  • 高於 1,000,000 us——緩慢的背景廣播,每隔幾秒才送出一次位置更新的信標。

掃描者這一側有它自己的旋鈕——aioble.scan() 接收 interval_uswindow_us(掃描者多久喚醒一次其無線電,以及每次聆聽多久)。預設值已足夠;唯一常見的變更是在不在意電池消耗時,將兩者設為相等以進行連續掃描。

11.4.5. 無連線模式——廣播者與觀察者

作為周邊裝置作為中央裝置(central) 這幾頁逐步說明了 API 的 可連線(connectable) 形態——周邊裝置接受連線,雙方透過 GATT 交換資料。另一種形態是 無連線(connectionless):廣播者將酬載作為廣告來發送,範圍內任何觀察者都能讀取它,完全不需連接。信標、到場偵測感測器,以及單向遙測,全都屬於這一類。

廣播者就是帶有 connectable=Falseaioble.advertise()。製造商專屬資料攜帶酬載:

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 關鍵字會在一秒後結束 advertise 呼叫;外層迴圈再以下一個序號重新發出,使聆聽者能看到最新的資料。connectable=False 旗標使該廣告成為廣播者形式——即使有連線請求送達,相機也不會回應。

觀察者就是相對應的唯讀掃描者。它永遠執行 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 使觀察者自身的無線電保持靜默(不發出掃描回應請求),以達到最低的功耗。manufacturer() 上的 filter= 引數會捨棄每一個不符合公司 ID 的廣告,因此該迴圈只會對廣播者的流量觸發。

11.4.6. 從探索到連線

一旦中心裝置挑選了一個周邊裝置來通訊,它便停止聆聽,在周邊裝置上次使用的廣告頻道上發送一個 連線請求(connect request),雙方隨即進入鏈路層的跳頻資料頻道。此時周邊裝置通常會停止廣告。接下來會發生什麼——連線參數、GATT 探索、鏈路的生命週期——詳見 連線