11.8. aioble 模組¶
Bluetooth 核心規格提供了一套詞彙,對應到兩個 MicroPython 模組。
bluetooth——與 BLE 控制器的低階綁定。它是同步的、透過 IRQ 風格的回呼函式以事件驅動,並圍繞著位元組緩衝區、控制代碼與最基本的 GATT 原語來建構。它將協定原貌地呈現出來,而非以 Python 應用程式想要使用的形式。aioble——一個較高階的包裝,以 Python 撰寫在bluetooth之上,將每個遠端操作轉化為asyncio協程,並將每個 BLE 物件(服務、特徵、連線、掃描結果、L2CAP 通道)轉化為符合人因工學的 Python 類別。掃描變成非同步迭代器;連線變成非同步情境管理器;通知變成可等待物件。
11.8.1. 何時該採用較低階的模組¶
對於兩種狹隘的情況,bluetooth 仍是正確的答案:
對於每一個相機應用程式,aioble 都是正確的答案。
11.8.2. aioble 程式的構成元件¶
每個以 aioble 為基礎的應用程式,無論扮演哪些角色,都有一小組可動的部件。
一個長時間執行的
asyncio事件迴圈。aioble中的一切都是協程,所以應用程式被建構成單一事件迴圈上的一個或多個任務。關於迴圈、任務與例外的詳細內容,請參見 Asyncio。一具已開啟的無線電。
aioble會在首次使用時隱式啟用 BLE 無線電,但也可以透過aioble.config()(它會在確保無線電已啟動後轉發給bluetooth.BLE.config())顯式控制,並以aioble.stop()將其關閉。同時進行一個或多個角色。在周邊端:一組已註冊的 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())
兩個程式都約十五行,涵蓋了從「無線電關閉」到「完成有用工作」的完整流程。
11.8.4. 關閉無線電¶
在電池供電的相機上,BLE 無線電是預算中最大的可裁量耗電項目。有兩個旋鈕很重要。
第一個是隱式的:aioble 會在首次使用時啟用無線電,而無線電會在排定的事件(廣播脈衝、連線事件、掃描視窗)之間自動進入睡眠。在 aioble.advertise() / aioble.scan() 上挑選較長的間隔,並在 connect() 時協商較長的連線間隔,能讓無線電按比例關閉更長的時間。廣告與掃描 中的廣播表格是這方面的實用指南。
第二個是顯式的關閉:
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——應用程式所使用的那幾層。有三個部分不在其範疇內:
11.8.6. 例外¶
aioble 會產生四種例外型別。每一種都從某個協程內部觸發——該協程在出錯時正等待著某個操作;當這些例外向外傳播時,async with 區塊會乾淨地解開。
aioble.DeviceDisconnectedError——在某個 GATT 操作(read、write、notified、indicated、subscribe、exchange_mtu等)進行中時,與對端的 BLE 鏈路中斷了。會在當時正在等待的那個協程內部引發。這是目前為止最常見的例外;任何應在連線遺失時重新連線的程式碼,都應捕捉它。aioble.GattError——某個 GATT 操作抵達了對端,但以非零的 ATT 狀態完成(帶回應的寫入遭拒、指示未被確認、讀取未獲許可等)。狀態碼位於例外的_status屬性上。aioble.L2CAPDisconnectedError——在某個send()、recvinto()或flush()進行中時,L2CAP 通道中斷了。可能是任一方關閉了通道,或底層的 GAP 連線消失了。aioble.L2CAPConnectionError——當監聽端拒絕,或控制器未能完成通道建立時,由l2cap_connect()引發。Bluetooth 狀態碼是第一個位置引數。
接受顯式 timeout_ms 的操作(連線/探索/讀取/寫入/配對等呼叫,加上作為包裝的 timeout()),在操作完成前期限屆滿時,還會額外從 asyncio 引發 asyncio.TimeoutError。