11.12. Współbieżne role i wiele połączeń¶
Strony dotyczące urządzenia peryferyjnego i centralnego pokazują pojedynczą rolę obsługującą w danym momencie jedno połączenie. Rzeczywiste aplikacje rzadko są tak proste. Kamera może publikować usługę sensora dla telefonu, jednocześnie odczytując wartości z paska do pomiaru tętna, albo przyjmować połączenia od dwóch jednocześnie sparowanych telefonów. API aioble obsługuje oba wzorce, ponieważ radio multipleksuje połączenia pod spodem, a każda operacja jest już korutyną – uruchom więcej korutyn, a praca będzie wykonywana równolegle na jednej pętli zdarzeń.
Ta strona zbiera wzorce, które się pojawiają.
11.12.1. Wielu klientów łączących się z jednym urządzeniem peryferyjnym¶
Prosta pętla urządzenia peryferyjnego na stronie Działanie jako urządzenie peryferyjne obsługuje w danym momencie jedno podłączone urządzenie centralne:
async def serve():
while True:
connection = await aioble.advertise(...)
async with connection:
await connection.disconnected()
Wzorzec, który pozwala mu przyjmować więcej niż jednego klienta, polega na uruchomieniu zadania dla każdego połączenia i natychmiastowym powrocie do pętli wywołującej aioble.advertise(), tak aby kolejny klient również mógł się połączyć:
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))
Każde połączenie działa we własnym zadaniu. Baza danych GATT jest współdzielona – wszyscy klienci widzą te same usługi i charakterystyki – ale stan właściwy dla każdego połączenia żyje wewnątrz zadania. Powiadomienia trafiają do każdego zasubskrybowanego klienta, gdy write() jest wywoływana z send_update=True; ukierunkowane wysyłki, które powinny dotrzeć tylko do jednego klienta, używają notify() / indicate() z konkretnym argumentem DeviceConnection.
Utrzymuj rozgałęzienie niewielkie. Każde podtrzymywane połączenie kosztuje czas radia, pamięć RAM i miejsce w tabeli połączeń kontrolera, a kamera nie jest zaprojektowana jako koncentrator dla dziesiątek klientów. Dwa lub trzy urządzenia centralne (telefon, tablet, towarzyszący mikrokontroler) są w pełni osiągalne; projekty wymagające większej liczby należą do właściwej bramy BLE, a nie do kamery.
11.12.2. Urządzenie peryferyjne i centralne jednocześnie¶
Kamera może rozgłaszać własną usługę telefonowi, jednocześnie działając jako urządzenie centralne wobec urządzenia noszonego. aioble nie ma przełącznika „trybu” – pętla rozgłaszania oraz pętla skanowania i łączenia to po prostu niezależne korutyny:
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())
Radio dzieli czas między dwie role – okno skanowania tutaj, impuls rozgłaszania tam, zdarzenie połączenia, gdy jedno z połączeń którejkolwiek strony jest aktywne. Przepustowość każdej roli spada, gdy obie są aktywne, ponieważ radio nie może dosłownie robić dwóch rzeczy naraz, ale w przypadku rozmów o niskiej przepustowości, do których BLE zostało zaprojektowane, koszt ten jest zwykle niezauważalny.
Dwie praktyczne rzeczy, o których warto pamiętać:
Obie role muszą znajdować się we własnej korutynie. Wywołanie
aioble.scan()z wnętrza zadania obsługującego podłączone urządzenie centralne dla danego klienta działa, ale blokuje powiadomienia tego klienta, dopóki skanowanie się nie zakończy – zamiast tego uruchom skanowanie we własnym zadaniu.W danym momencie działa tylko jedno skanowanie. Jeśli musisz skanować z dwóch różnych miejsc, współdziel iterator skanowania lub koordynuj dostęp; nie wchodź równolegle do dwóch menedżerów kontekstu
aioble.scan().
11.12.3. Koordynowanie wielu połączeń z jednego zadania¶
Gdy kilka połączeń trzeba połączyć w jedną logiczną operację – na przykład kamera rozmawia z dwoma sensorami naraz i raportuje wynik dopiero po tym, jak oba odpowiedzą – standardowe prymitywy asyncio mają bezpośrednie zastosowanie. asyncio.gather() uruchamia korutyny dla poszczególnych połączeń współbieżnie i zwraca, gdy wszystkie się zakończą; asyncio.wait_for() dodaje termin.
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
Ten sam wzorzec, którego rozdział o asyncio (Asyncio) używa do obsługi sieci – korutyny BLE podłączają się do gather / wait_for / Event / Lock w taki sam sposób jak te oparte na TCP.
11.12.4. Gdy jedna rola kończy się w każdym cyklu, a druga nie¶
Cykl w kamerze zasilanej z baterii może wyglądać następująco:
Wybudzenie.
Jako urządzenie centralne, odczytaj świeże wartości ze sparowanego paska sensorowego.
Jako urządzenie peryferyjne, rozgłaszaj, aby telefon mógł pobrać pomiary z danego dnia.
Gdy obie role są bezczynne, wywołaj
aioble.stop()i przejdź w stan uśpienia.
Sekwencjonowanie jest proste przy dwóch zadaniach i jednym 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