11.8. Il modulo aioble

La specifica Bluetooth Core fornisce un vocabolario che si mappa su due moduli MicroPython.

  • bluetooth – il binding di basso livello al controller BLE. Sincrono, guidato da eventi attraverso una callback in stile IRQ e strutturato attorno a buffer di byte, handle e le primitive GATT grezze. Espone il protocollo cosi com’e, non come le applicazioni Python vorrebbero consumarlo.

  • aioble – un wrapper di piu alto livello, scritto in Python sopra bluetooth, che trasforma ogni operazione remota in una coroutine asyncio e ogni oggetto BLE (servizi, caratteristiche, connessioni, risultati di scan, canali L2CAP) in un’ergonomica classe Python. Gli scan diventano iteratori asincroni; le connessioni diventano context manager asincroni; le notifiche diventano awaitable.

11.8.1. Quando ricorrere al modulo di livello piu basso

bluetooth rimane la risposta giusta per due casi ristretti:

  • Stai scrivendo il tipo di codice di cui aioble stesso e fatto – un nuovo pattern che richiede controllo a livello IRQ sul protocollo.

  • Stai operando su un target hardware in cui il pacchetto aioble non e disponibile, e un sottile shim attorno al controller e l’unica opzione.

Per ogni applicazione della camera, aioble e la risposta giusta.

11.8.2. Componenti di un programma aioble

Ogni applicazione basata su aioble ha un piccolo insieme di parti mobili, indipendentemente dai ruoli che svolge.

  • Un event loop asyncio a lunga durata. Tutto in aioble e una coroutine, quindi l’applicazione e strutturata come uno o piu task su un singolo event loop. Vedi Asyncio per i dettagli su loop, task ed eccezioni.

  • Una radio accesa. aioble attiva la radio BLE implicitamente al primo utilizzo, ma puo anche essere controllata esplicitamente con aioble.config() (che inoltra a bluetooth.BLE.config() dopo essersi assicurato che la radio sia attiva) e spenta con aioble.stop().

  • Uno o piu ruoli attivi contemporaneamente. Sul lato peripheral: un insieme registrato di servizi GATT (vedi aioble.register_services()) e una coroutine aioble.advertise() in esecuzione. Sul lato central: un iteratore aioble.scan() in esecuzione o una aioble.Device.connect() in sospeso. La radio multiplexa il lavoro; l’applicazione vede ogni ruolo come un task indipendente.

11.8.3. Un peripheral minimale

Il piu piccolo programma aioble utile – un peripheral che pubblicizza una singola caratteristica in sola lettura – e breve:

import aioble
import asyncio
import bluetooth

SERVICE_UUID = bluetooth.UUID(0x181A)            # Environmental Sensing
TEMP_UUID = bluetooth.UUID(0x2A6E)               # Temperature

service = aioble.Service(SERVICE_UUID)
temp = aioble.Characteristic(service, TEMP_UUID, read=True)
aioble.register_services(service)

async def main():
    while True:
        conn = await aioble.advertise(
            interval_us=250000,
            name="openmv-temp",
            services=[SERVICE_UUID],
        )
        async with conn:
            await conn.disconnected()

asyncio.run(main())

Un central che non fa altro che connettersi e leggere una volta e altrettanto breve:

import aioble
import asyncio
import bluetooth

SERVICE_UUID = bluetooth.UUID(0x181A)
TEMP_UUID = bluetooth.UUID(0x2A6E)

async def main():
    device = None
    async with aioble.scan(duration_ms=5000, active=True) as scanner:
        async for result in scanner:
            if SERVICE_UUID in result.services():
                device = result.device
                break
    if device is None:
        return

    async with await device.connect() as conn:
        service = await conn.service(SERVICE_UUID)
        char = await service.characteristic(TEMP_UUID)
        print(await char.read())

asyncio.run(main())

Entrambi i programmi sono di circa quindici righe e coprono l’intero flusso da «radio spenta» a «lavoro utile svolto».

11.8.4. Spegnere la radio

Su una camera alimentata a batteria la radio BLE e il maggior consumo discrezionale sul budget. Contano due manopole.

La prima e implicita: aioble attiva la radio al primo utilizzo, e la radio dorme automaticamente tra gli eventi pianificati (raffiche di advertising, eventi di connessione, finestre di scan). Scegliere intervalli piu lunghi su aioble.advertise() / aioble.scan() e concordare un intervallo di connessione piu lungo al momento di connect() mantiene la radio spenta proporzionalmente piu a lungo. La tabella di advertising in Advertising e scanning e la guida pratica in questo caso.

La seconda e lo spegnimento esplicito

import aioble

await do_burst_of_ble_work()
aioble.stop()                             # radio deactivated; in-flight tasks unwound
await asyncio.sleep(60)                   # sleep with the radio off
# ... next aioble call brings the radio back up automatically

aioble.stop() disattiva la radio BLE sottostante e smantella tutto cio che e attivo – le connessioni aperte cadono, scanner e advertiser vengono annullati, i canali L2CAP si chiudono. Le coroutine in attesa su quelle operazioni sollevano le loro consuete eccezioni (DeviceDisconnectedError e simili), che e il meccanismo di pulizia per cui i blocchi async with circostanti sono stati scritti. Chiamare in seguito una qualsiasi coroutine aioble riattiva la radio da fredda.

Il pattern tipico per una cam sensore periodica alimentata a batteria e:

  • Risvegliarsi su una pianificazione (timer, sensore di movimento, pulsante).

  • Eseguire la raffica di lavoro BLE – pubblicizzare, accettare una connessione, inviare il valore, disconnettere.

  • Chiamare aioble.stop() e andare in sleep fino al risveglio successivo.

11.8.5. Cosa aioble non fa

aioble copre deliberatamente GATT, GAP e L2CAP – i livelli che un’applicazione utilizza. Tre componenti sono fuori dall’ambito:

  • Tutto cio che e al di sotto del link layer. Selezione del canale, frequency hopping, acknowledgement dei pacchetti e cifratura a livello di link avvengono tutti all’interno della porta BLE e del silicio del controller; aioble non espone hook a quel livello.

  • Il Bluetooth classico. aioble e solo BLE. Collegamenti audio, RFCOMM, A2DP e altre funzionalita dei profili classici non fanno parte dell’API.

  • Bluetooth Mesh. Il livello di rete mesh del Bluetooth SIG (uno stack separato sopra l’advertising BLE) non e implementato sulla camera. La cam puo pubblicizzare e osservare, ma non puo partecipare ai ruoli relay / friend / proxy di una rete mesh.

11.8.6. Eccezioni

Da aioble escono quattro tipi di eccezione. Ognuna scatta dall’interno di una coroutine che era in attesa di un’operazione quando qualcosa e andato storto; i blocchi async with si smontano in modo pulito quando si propagano.

  • aioble.DeviceDisconnectedError – il collegamento BLE al peer e caduto mentre un’operazione GATT (read, write, notified, indicated, subscribe, exchange_mtu, …) era attiva. Sollevata all’interno della coroutine che era in attesa. Di gran lunga l’eccezione piu comune; intercettala in qualsiasi codice che debba riconnettersi in caso di perdita.

  • aioble.GattError – un’operazione GATT ha raggiunto il peer ma si e completata con uno stato ATT diverso da zero (write-with-response rifiutata, indicate non riconosciuta, read-not-permitted, …). Il codice di stato si trova nell’attributo _status dell’eccezione.

  • aioble.L2CAPDisconnectedError – il canale L2CAP e caduto mentre una send(), recvinto() o flush() era attiva. Uno dei due lati puo aver chiuso il canale, oppure la connessione GAP sottostante e scomparsa.

  • aioble.L2CAPConnectionError – sollevata da l2cap_connect() quando il listener ha rifiutato o il controller ha fatto fallire la configurazione del canale. Il codice di stato Bluetooth e il primo argomento posizionale.

Le operazioni che accettano un esplicito timeout_ms (le chiamate di connect / discovery / read / write / pair, piu timeout() come wrapper) sollevano inoltre asyncio.TimeoutError da asyncio quando la scadenza trascorre prima che l’operazione si completi.