11.12. 並行角色與多重連線¶
周邊與中央兩個頁面各自示範了單一角色在同一時間服務單一連線的情況。實際應用很少這麼單純。一台相機可能在向手機發布感測器服務的同時,也從心率帶讀取數值,或者同時接受兩支已配對手機的連線。aioble API 同時支援這兩種模式,因為底層的無線電會進行多工處理,而且每個操作本身就已經是協程——只要執行更多協程,這些工作就會在同一個事件迴圈上並行進行。
本頁彙整了會出現的各種模式。
11.12.1. 多個用戶端連線至同一個周邊裝置¶
作為周邊裝置 上的簡易周邊迴圈在同一時間只服務一個已連線的中央裝置:
async def serve():
while True:
connection = await aioble.advertise(...)
async with connection:
await connection.disconnected()
讓它能接受多於一個用戶端的做法,是為每條連線啟動一個專屬任務,然後立刻回到迴圈呼叫 aioble.advertise(),以便下一個用戶端也能連線:
async def handle_client(connection):
async with connection:
# ... per-client work: subscribe their CCCDs,
# push notifications, await writes ...
await connection.disconnected()
async def serve():
while True:
connection = await aioble.advertise(
interval_us=250000,
name="openmv-env",
services=[ENV_SERVICE],
)
asyncio.create_task(handle_client(connection))
每條連線都在自己的任務中執行。GATT 資料庫是共用的——所有用戶端看到的服務與特徵都相同——但每條連線各自的狀態則保存在其任務內部。當以 send_update=True 呼叫 write() 時,通知會送往每一個已訂閱的用戶端;若是只應送達單一用戶端的定向推送,則使用 notify() / indicate() 並帶上特定的 DeviceConnection 引數。
請讓扇出維持在小規模。每一條持有的連線都會耗用無線電時間、RAM,以及控制器連線表中的一個位置,而相機並非設計來作為服務數十個用戶端的集線器。兩三個中央裝置(一支手機、一台平板、一顆搭配用的微控制器)完全在能力範圍內;需要更多連線的設計應該交給真正的 BLE 閘道器,而非相機。
11.12.2. 同時擔任周邊與中央¶
一台相機可以向手機廣播自己的服務,同時又對某個穿戴裝置擔任中央角色。aioble 沒有「模式」開關——廣播迴圈與掃描連線迴圈不過是兩個各自獨立的協程:
async def be_peripheral():
while True:
connection = await aioble.advertise(
interval_us=250000,
name="openmv-hub",
services=[ENV_SERVICE],
)
asyncio.create_task(handle_client(connection))
async def be_central():
while True:
sensor = await find_sensor()
if sensor is None:
await asyncio.sleep(5)
continue
try:
async with await sensor.connect() as conn:
await stream_from_sensor(conn)
except aioble.DeviceDisconnectedError:
pass
async def main():
await asyncio.gather(be_peripheral(), be_central())
asyncio.run(main())
無線電會在這兩個角色之間分時運作——這裡一個掃描視窗、那裡一陣廣播,當任一方的連線正在進行時則安排連線事件。當兩者都啟用時,每個角色的吞吐量都會下降,因為無線電實際上無法同時做兩件事,但對於 BLE 原本所設計的低頻寬通訊而言,這個代價通常察覺不出來。
有兩件實務上要謹記的事:
兩個角色都需要各自的協程。 從處理某個已連線中央裝置的「每用戶端任務」內部呼叫
aioble.scan()是可行的,但會在掃描完成前阻擋該用戶端的通知——應改為在獨立的任務上執行掃描。同一時間只能執行一次掃描。 如果你需要從兩個不同的地方掃描,請共用該掃描迭代器或協調存取;不要並行進入兩個
aioble.scan()情境管理器。
11.12.3. 從單一任務協調多重連線¶
當數條連線需要結合成一個邏輯操作時——例如相機同時與兩個感測器通訊,並且只在兩者都回應後才回報結果——標準的 asyncio 原語可直接套用。asyncio.gather() 會並行執行各連線的協程,並在全部完成後回傳;:func:asyncio.wait_for 則加上一個期限。
async def read_pair():
async with await sensor_a.connect() as a:
async with await sensor_b.connect() as b:
value_a, value_b = await asyncio.gather(
read_value(a, A_SERVICE, A_CHAR),
read_value(b, B_SERVICE, B_CHAR),
)
return value_a, value_b
這與 asyncio 章節(Asyncio)用於網路通訊的模式相同——BLE 協程接入 gather / wait_for / Event / Lock 的方式,與 TCP 協程完全一樣。
11.12.4. 當其中一個角色每週期就結束,而另一個不會時¶
一台電池供電相機的週期可能像這樣:
喚醒。
作為中央,從已配對的感測器帶讀取最新數值。
作為周邊,進行廣播以供手機下載當天的量測資料。
當兩者都閒置時,呼叫
aioble.stop()並進入睡眠。
用兩個任務搭配一個 asyncio.Event,這個排序就能直截了當地完成:
phone_done = asyncio.Event()
async def serve_phone():
connection = await aioble.advertise(
interval_us=250000,
name="openmv-hub",
services=[ENV_SERVICE],
)
async with connection:
await stream_measurements(connection)
phone_done.set()
async def read_strap():
async with await strap.connect() as conn:
await pull_fresh_values(conn)
async def cycle():
await asyncio.gather(read_strap(), serve_phone())
aioble.stop() # radio off until next wake