11.12. Ruoli concorrenti e connessioni multiple

Le pagine dedicate al peripheral e al central mostrano ciascuna un singolo ruolo che serve una singola connessione alla volta. Le applicazioni reali sono raramente cosi semplici. Una camera potrebbe pubblicare un servizio sensore verso uno smartphone mentre allo stesso tempo legge valori da una fascia cardiaca, oppure accettare connessioni da due smartphone accoppiati simultaneamente. L’API aioble supporta entrambi i pattern perche la radio multiplexa al di sotto e ogni operazione e gia una coroutine: basta eseguire piu coroutine e il lavoro avviene in parallelo su un unico event loop.

Questa pagina raccoglie i pattern che ricorrono piu spesso.

11.12.1. Piu client che si connettono a un solo peripheral

Il semplice loop del peripheral in Agire come periferica serve un central connesso alla volta:

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

Il pattern che gli permette di accettare piu di un client consiste nell’avviare un task per connessione e tornare immediatamente al loop su aioble.advertise(), cosi che anche il client successivo possa connettersi:

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

Ogni connessione viene eseguita nel proprio task. Il database GATT e condiviso – tutti i client vedono gli stessi servizi e le stesse caratteristiche – ma lo stato per ogni connessione vive all’interno del task. Le notifiche raggiungono ogni client iscritto quando write() viene chiamato con send_update=True; i push diretti che devono raggiungere un solo client usano notify() / indicate() con l’argomento DeviceConnection specifico.

Mantieni piccolo il fan-out. Ogni connessione mantenuta costa tempo radio, RAM e uno slot nella tabella delle connessioni del controller, e la camera non e progettata per essere un hub per decine di client. Due o tre central (uno smartphone, un tablet, un microcontrollore companion) sono ampiamente alla portata; i progetti che ne richiedono di piu appartengono a un vero gateway BLE piuttosto che alla cam.

11.12.2. Peripheral e central contemporaneamente

Una camera puo pubblicizzare il proprio servizio a uno smartphone mentre allo stesso tempo agisce da central verso un dispositivo indossabile. aioble non ha un interruttore di «modalita»: il loop di advertising e il loop di scan-and-connect sono semplicemente coroutine indipendenti:

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

La radio condivide il tempo tra i due ruoli: una finestra di scan qui, una raffica di advertising la, un evento di connessione quando una delle connessioni di uno dei due lati e attiva. Il throughput di ogni ruolo cala quando entrambi sono attivi, perche la radio non puo letteralmente fare due cose contemporaneamente, ma per le conversazioni a bassa larghezza di banda per cui BLE e stato progettato, il costo e di solito invisibile.

Due aspetti pratici da tenere a mente:

  • Entrambi i ruoli devono trovarsi nella propria coroutine. Chiamare aioble.scan() dall’interno del task per client che gestisce un central connesso funziona, ma blocca le notifiche di quel client finche lo scan non termina: esegui invece la scansione in un task dedicato.

  • Una sola scansione viene eseguita alla volta. Se devi effettuare scansioni da due punti diversi, condividi l’iteratore di scan o coordina l’accesso; non entrare in due context manager aioble.scan() in parallelo.

11.12.3. Coordinare connessioni multiple da un solo task

Quando piu connessioni devono essere combinate in un’unica operazione logica – per esempio la camera dialoga con due sensori contemporaneamente e riporta il risultato solo dopo che entrambi hanno risposto – si applicano direttamente le primitive standard di asyncio. asyncio.gather() esegue concorrentemente le coroutine per connessione e ritorna quando tutte sono terminate; asyncio.wait_for() aggiunge una scadenza.

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

E lo stesso pattern che il capitolo asyncio (Asyncio) usa per il networking: le coroutine BLE si collegano a gather / wait_for / Event / Lock esattamente come fanno quelle TCP.

11.12.4. Quando un ruolo termina a ogni ciclo e l’altro no

Un ciclo in una cam alimentata a batteria potrebbe assomigliare a questo:

  • Risveglio.

  • Come central, leggere valori freschi da una fascia sensore accoppiata.

  • Come peripheral, pubblicizzare per far scaricare a uno smartphone le misurazioni della giornata.

  • Quando entrambi sono inattivi, chiamare aioble.stop() e andare in sleep.

Il sequenziamento e diretto con due task e un 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