在同步程式裡呼叫 bleak 的 BLE 掃描,UI 會整個卡住;用 asyncio 的 event loop 橋接之後,掃描與連線期間 UI 可以正常回應。
我做的 BLE dashboard 需要跟裝置連線、讀寫 GATT。bleak 是 Python 常見的 BLE 模組,但它從設計上就是非同步的:
BleakScanner.discover() # 協程(coroutine)
BleakClient.connect() # 協程(coroutine)
這些函式回傳的是「協程物件」,不是直接執行結果。如果照一般同步寫法直接呼叫,會遇到兩個狀況:要嘛沒有 event loop 可以跑,要嘛整個執行流程停在那裡等,UI 就沒有回應了。
原因不難理解:BLE 掃描本質上是「等待」— 等裝置回廣播、等連線建立、等 GATT 回應。同步程式在等待期間什麼都不能做,畫面自然就凍住。
asyncio 適合 IO 密集的場景(等待網路、檔案、藍牙),它的做法是用一個 event loop 來調度任務:當某個任務在等 IO 時,loop 就去處理其他事情。
同步程式要執行協程,只要三個步驟:
import asyncio
from bleak import BleakScanner
# ① 建立新的 event loop
loop = asyncio.new_event_loop()
# ② 設定為當前執行緒的 event loop
asyncio.set_event_loop(loop)
# ③ 執行協程,直到它完成
devices = loop.run_until_complete(BleakScanner.discover(timeout=5.0))
包成函式,加上資源清理:
def scan_ble_devices():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
return loop.run_until_complete(
BleakScanner.discover(timeout=5.0)
)
finally:
loop.close()
連線並讀取 GATT 特徵值也是同樣的模式:
async def read_gatt(address, char_uuid):
async with BleakClient(address) as client:
if client.is_connected:
return await client.read_gatt_char(char_uuid)
def do_read(address, char_uuid):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
return loop.run_until_complete(read_gatt(address, char_uuid))
finally:
loop.close()
| 函式 | 作用 |
|---|---|
new_event_loop() | 建立一個新的事件迴圈(非同步任務的調度器) |
set_event_loop() | 設定為當前執行緒的預設迴圈 |
run_until_complete() | 執行協程直到完成,是同步與非同步之間的橋樑 |
run_until_complete() 本身也是阻塞的。它在 Flask 的請求處理執行緒裡沒問題,但如果放在 GUI 主執行緒直接呼叫,畫面還是會卡 — 這種情況要把整段放進獨立執行緒。close() 之後就不能再使用,需要重新 new_event_loop()。set_event_loop() 會覆蓋前一個。import threading
def run_async_in_thread(coro):
def worker():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(coro)
finally:
loop.close()
threading.Thread(target=worker, daemon=True).start()
改用 event loop 橋接之後,BLE 掃描與連線期間 UI 可以正常操作,掃描結果也能正確取回。Flask 的請求各自有執行緒,所以直接在 route 裡用這個模式就能運作。
asyncio.gather() 併發執行,不必一個一個等。new_event_loop() → set_event_loop() → run_until_complete()。run_until_complete() 是阻塞的:在請求執行緒裡沒問題,在 GUI 主執行緒要另開執行緒。