11.12. Gelijktijdige rollen en meerdere verbindingen

De pagina’s over peripheral en central tonen elk een enkele rol die op een bepaald moment een enkele verbinding bedient. Echte toepassingen zijn zelden zo eenvoudig. Een camera kan een sensorservice publiceren naar een telefoon terwijl hij ook waarden uitleest van een hartslagband, of verbindingen accepteren van twee tegelijk gekoppelde telefoons. De aioble-API ondersteunt beide patronen omdat de radio er onderhuids tussen multiplext en elke bewerking al een coroutine is – voer meer coroutines uit en het werk gebeurt parallel op één event-loop.

Deze pagina verzamelt de patronen die hierbij naar voren komen.

11.12.1. Meerdere clients verbinden met één peripheral

De eenvoudige peripheral-loop op Optreden als randapparaat bedient telkens één verbonden central:

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

Het patroon waarmee hij meer dan één client kan accepteren, is het afvuren van een taak per verbinding en onmiddellijk terugkeren naar aioble.advertise() zodat de volgende client ook verbinding kan maken:

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

Elke verbinding draait in zijn eigen taak. De GATT-database wordt gedeeld – alle clients zien dezelfde services en characteristics – maar de toestand per verbinding leeft binnen de taak. Notificaties gaan naar elke geabonneerde client wanneer write() wordt aangeroepen met send_update=True; gerichte pushes die slechts één client mogen bereiken, gebruiken notify() / indicate() met het specifieke DeviceConnection-argument.

Houd de fan-out klein. Elke vastgehouden verbinding kost radiotijd, RAM en een plek in de verbindingstabel van de controller, en de camera is niet ontworpen om een hub voor tientallen clients te zijn. Twee of drie centrals (een telefoon, een tablet, een begeleidende microcontroller) liggen ruim binnen bereik; ontwerpen die meer nodig hebben, horen thuis op een echte BLE-gateway in plaats van op de camera.

11.12.2. Peripheral en central tegelijkertijd

Een camera kan zijn eigen service adverteren naar een telefoon terwijl hij ook als central optreedt naar een wearable. aioble heeft geen “modus”-schakelaar – de advertise-loop en de scan-en-verbind-loop zijn gewoon onafhankelijke coroutines:

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

De radio verdeelt de tijd tussen de twee rollen – hier een scanvenster, daar een advertentieburst, een verbindingsgebeurtenis wanneer een van beide verbindingen actief is. De doorvoer van elke rol daalt wanneer beide actief zijn, omdat de radio niet letterlijk twee dingen tegelijk kan doen, maar voor de conversaties met lage bandbreedte waarvoor BLE is ontworpen, zijn de kosten meestal onzichtbaar.

Twee praktische dingen om in gedachten te houden:

  • Beide rollen moeten in hun eigen coroutine zitten. aioble.scan() aanroepen vanuit de taak per client die een verbonden central afhandelt werkt wel, maar blokkeert de notificaties van die client totdat de scan klaar is – voer het scannen in plaats daarvan uit op een eigen taak.

  • Er draait maar één scan tegelijk. Als je vanuit twee verschillende plaatsen moet scannen, deel dan de scan-iterator of coördineer de toegang; ga niet parallel twee aioble.scan()-contextmanagers binnen.

11.12.3. Meerdere verbindingen vanuit één taak coördineren

Wanneer meerdere verbindingen moeten worden gecombineerd tot één logische bewerking – bijvoorbeeld de camera praat met twee sensoren tegelijk en rapporteert het resultaat pas nadat beide hebben geantwoord – zijn de standaard asyncio-primitieven direct toepasbaar. asyncio.gather() voert de coroutines per verbinding gelijktijdig uit en keert terug wanneer ze allemaal klaar zijn; asyncio.wait_for() voegt een deadline toe.

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

Hetzelfde patroon dat het asyncio-hoofdstuk (Asyncio) gebruikt voor netwerken – BLE-coroutines koppelen op dezelfde manier in gather / wait_for / Event / Lock als TCP-coroutines.

11.12.4. Wanneer de ene rol per cyclus klaar is en de andere niet

Een cyclus in een camera op batterijvoeding zou er als volgt uit kunnen zien:

  • Ontwaken.

  • Als central, lees verse waarden uit een gekoppelde sensorband.

  • Als peripheral, adverteer zodat een telefoon de metingen van die dag kan downloaden.

  • Wanneer beide inactief zijn, roep aioble.stop() aan en ga slapen.

De volgorde is eenvoudig met twee taken en een 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