11.12. Egyidejű szerepek és több kapcsolat

A peripheral és a central oldalak mindegyike egyetlen szerepet mutat be, amely egyszerre egyetlen kapcsolatot szolgál ki. A valós alkalmazások ritkán ennyire egyszerűek. Egy kamera közzétehet egy érzékelőszolgáltatást egy telefonnak, miközben egy pulzusmérő pánttól is olvas értékeket, vagy elfogadhatja két egyidejűleg párosított telefon kapcsolatát. Az aioble API mindkét mintát támogatja, mert a rádió alatta multiplexel, és minden művelet eleve coroutine – futtass több coroutine-t, és a munka párhuzamosan zajlik egyetlen eseményhurokon.

Ez az oldal összegyűjti az előforduló mintákat.

11.12.1. Több kliens csatlakozása egyetlen peripheralhoz

Az egyszerű peripheral hurok a Periféria szerepkörben oldalon egyszerre egy csatlakozott centralt szolgál ki:

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

A minta, amely lehetővé teszi, hogy egynél több klienst fogadjon el, az, hogy kapcsolatonként elindít egy feladatot, és azonnal visszatér az aioble.advertise() hívásra, hogy a következő kliens is csatlakozhasson:

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

Minden kapcsolat a saját feladatában fut. A GATT-adatbázis közös – minden kliens ugyanazokat a szolgáltatásokat és karakterisztikákat látja –, de a kapcsolatonkénti állapot a feladaton belül él. Az értesítések minden feliratkozott klienshez eljutnak, amikor a write() hívás send_update=True paraméterrel történik; az olyan irányított továbbítások, amelyeknek csak egy klienst kell elérniük, a notify() / indicate() hívást használják a konkrét DeviceConnection argumentummal.

Tartsd kicsiben a szétosztást. Minden fenntartott kapcsolat rádióidőbe, RAM-ba és a vezérlő kapcsolati táblájában egy helybe kerül, és a kamera nem arra tervezték, hogy több tucat kliens hubjaként szolgáljon. Két vagy három central (egy telefon, egy tablet, egy társ mikrokontroller) bőven elérhető; az ennél többet igénylő tervek egy megfelelő BLE-átjáróra valók, nem pedig a kamerára.

11.12.2. Peripheral és central egyszerre

Egy kamera hirdetheti a saját szolgáltatását egy telefonnak, miközben centralként is működik egy viselhető eszköz felé. Az aioble nem rendelkezik „mód” kapcsolóval – a hirdetési hurok és a beolvasó-és-csatlakozó hurok csupán független coroutine-ok:

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

A rádió időosztásban dolgozik a két szerep között – egy beolvasási ablak itt, egy hirdetési kitörés ott, egy kapcsolati esemény, amikor bármelyik oldal kapcsolata él. Mindkét szerep átviteli teljesítménye csökken, amikor mindkettő aktív, mert a rádió szó szerint nem tud egyszerre két dolgot csinálni, de azokhoz az alacsony sávszélességű beszélgetésekhez, amelyekhez a BLE-t tervezték, a költség általában láthatatlan.

Két gyakorlati dolog, amit érdemes szem előtt tartani:

  • Mindkét szerepnek a saját coroutine-jában kell lennie. Az aioble.scan() hívása egy csatlakozott centralt kezelő, kapcsolatonkénti feladaton belülről működik, de blokkolja az adott kliens értesítéseit, amíg a beolvasás be nem fejeződik – futtasd a beolvasást inkább a saját feladatában.

  • Egyszerre csak egy beolvasás fut. Ha két különböző helyről kell beolvasnod, oszd meg a beolvasó iterátort, vagy koordináld a hozzáférést; ne lépj be két aioble.scan() kontextuskezelőbe párhuzamosan.

11.12.3. Több kapcsolat koordinálása egyetlen feladatból

Amikor több kapcsolatot egyetlen logikai műveletbe kell egyesíteni – például a kamera egyszerre két érzékelővel beszél, és csak akkor jelenti az eredményt, miután mindkettő válaszolt –, a standard asyncio primitívek közvetlenül alkalmazhatók. Az asyncio.gather() egyidejűleg futtatja a kapcsolatonkénti coroutine-okat, és akkor tér vissza, amikor mindegyik befejeződött; az asyncio.wait_for() határidőt ad hozzá.

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

Ugyanaz a minta, amelyet az asyncio fejezet (Asyncio) használ a hálózatkezeléshez – a BLE-coroutine-ok ugyanúgy illeszkednek a gather / wait_for / Event / Lock mechanizmusokhoz, mint a TCP-sek.

11.12.4. Amikor az egyik szerep ciklusonként befejeződik, a másik viszont nem

Egy ciklus egy elemmel táplált kamerában így nézhet ki:

  • Ébredés.

  • Centralként friss értékeket olvas egy párosított érzékelőpántról.

  • Peripheralként hirdet, hogy egy telefon letölthesse a napi méréseket.

  • Amikor mindkettő tétlen, meghívja az aioble.stop() hívást, és alszik.

A sorrendiség egyszerű két feladattal és egy asyncio.Event objektummal:

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