11.12. Souběžné role a více připojení

Stránky o periferii a centrálním zařízení vždy ukazují jedinou roli obsluhující v daném okamžiku jediné připojení. Reálné aplikace bývají jen zřídka tak jednoduché. Kamera může publikovat senzorovou službu do telefonu a zároveň číst hodnoty z hrudního pásu pro měření tepu, nebo přijímat připojení od dvou současně spárovaných telefonů. API aioble podporuje oba vzory, protože rádio se na pozadí multiplexuje a každá operace už je korutina – stačí spustit více korutin a práce probíhá paralelně v jedné smyčce událostí.

Tato stránka shromažďuje vzory, které se v praxi objevují.

11.12.1. Více klientů připojených k jedné periferii

Jednoduchá smyčka periferie na Vystupování jako periferie obsluhuje v daném okamžiku jediné připojené centrální zařízení:

async def serve():
    while True:
        connection = await aioble.advertise(...)
        async with connection:
            await connection.disconnected()

Vzor, který jí umožňuje přijmout více než jednoho klienta, spočívá v tom, že se pro každé připojení spustí samostatná úloha a okamžitě se vrátí zpět k aioble.advertise(), aby se mohl připojit i další klient:

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ždé připojení běží ve své vlastní úloze. Databáze GATT je sdílená – všichni klienti vidí stejné služby a charakteristiky – ale stav specifický pro jednotlivé připojení žije uvnitř úlohy. Oznámení jdou každému přihlášenému klientovi, když je write() voláno s send_update=True; cílené odeslání, které má dosáhnout pouze jednoho klienta, používá notify() / indicate() s konkrétním argumentem DeviceConnection.

Udržujte rozvětvení malé. Každé udržované připojení stojí čas rádia, RAM a slot v tabulce připojení řadiče a kamera není navržena jako rozbočovač pro desítky klientů. Dvě nebo tři centrální zařízení (telefon, tablet, doprovodný mikrokontrolér) jsou snadno na dosah; návrhy, které potřebují více, patří na řádnou BLE bránu, nikoli na kameru.

11.12.2. Periferie a centrální zařízení současně

Kamera může inzerovat svoji vlastní službu do telefonu, a zároveň fungovat jako centrální zařízení vůči nositelnému zařízení. aioble nemá přepínač „režimu“ – inzertní smyčka a smyčka skenování a připojování jsou jen nezávislé korutiny:

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())

Rádio se časově sdílí mezi oběma rolemi – skenovací okno zde, inzertní dávka tam, událost připojení, když je některé z připojení na obou stranách aktivní. Propustnost (throughput) každé role klesá, když jsou obě aktivní, protože rádio nemůže doslova dělat dvě věci najednou, ale u nízkokapacitní komunikace, pro kterou byl BLE navržen, je tento náklad obvykle neviditelný.

Dvě praktické věci, které je třeba mít na paměti:

  • Obě role musí být ve své vlastní korutině. Volání aioble.scan() zevnitř úlohy pro jednotlivého klienta, která obsluhuje připojené centrální zařízení, funguje, ale blokuje oznámení tohoto klienta, dokud skenování neskončí – skenování raději spusťte v jeho vlastní úloze.

  • Najednou běží pouze jedno skenování. Pokud potřebujete skenovat ze dvou různých míst, sdílejte iterátor skenování nebo koordinujte přístup; nevstupujte do dvou kontextových správců aioble.scan() paralelně.

11.12.3. Koordinace více připojení z jedné úlohy

Když je třeba zkombinovat několik připojení do jedné logické operace – například kamera komunikuje se dvěma senzory najednou a výsledek hlásí teprve poté, co oba odpověděly – uplatní se přímo standardní primitiva asyncio. asyncio.gather() spouští korutiny pro jednotlivá připojení souběžně a vrací se, až všechny dokončí; asyncio.wait_for() přidává časový limit.

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

Stejný vzor, jaký pro síťování používá kapitola o asyncio (Asyncio) – BLE korutiny se zapojují do gather / wait_for / Event / Lock stejným způsobem jako ty pro TCP.

11.12.4. Když jedna role dokončí svůj cyklus a druhá ne

Cyklus u kamery napájené z baterie může vypadat takto:

  • Probuzení.

  • Jako centrální zařízení přečíst čerstvé hodnoty ze spárovaného senzorového pásu.

  • Jako periferie inzerovat, aby si telefon mohl stáhnout denní měření.

  • Když jsou obě nečinné, zavolat aioble.stop() a uspat se.

Sekvencování je s dvěma úlohami a asyncio.Event přímočaré:

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