11.8. Modulul aioble

Specificația Bluetooth Core oferă un vocabular care se mapează pe două module MicroPython.

  • bluetooth – legătura de nivel scăzut cu controlerul BLE. Sincronă, bazată pe evenimente printr-o funcție de retroapelare (callback) de tip IRQ și structurată în jurul tampoanelor (buffer) de octeți, al handle-urilor și al primitivelor GATT brute. Expune protocolul așa cum este, nu așa cum vor să-l consume aplicațiile Python.

  • aioble – un wrapper de nivel superior, scris în Python deasupra bluetooth, care transformă fiecare operațiune la distanță într-o corutină asyncio și fiecare obiect BLE (servicii, caracteristici, conexiuni, rezultate de scanare, canale L2CAP) într-o clasă Python ergonomică. Scanările devin iteratoare asincrone; conexiunile devin gestionare de context asincrone; notificările devin așteptabile.

11.8.1. Când să apelezi la modulul de nivel scăzut

bluetooth este în continuare răspunsul potrivit pentru două cazuri restrânse:

  • Scrii genul de cod din care este construit însuși aioble – un model nou care necesită control la nivel IRQ asupra protocolului.

  • Rulezi pe o țintă hardware unde pachetul aioble nu este disponibil, iar o cojiță subțire în jurul controlerului este singura opțiune.

Pentru fiecare aplicație de cameră, aioble este răspunsul potrivit.

11.8.2. Componentele unui program aioble

Fiecare aplicație bazată pe aioble are un mic set de piese mobile, indiferent de rolurile pe care le joacă.

  • O buclă de evenimente asyncio de lungă durată. Totul în aioble este o corutină, deci aplicația este structurată ca una sau mai multe sarcini pe o singură buclă de evenimente. Consultă Asyncio pentru detalii despre buclă, sarcini și excepții.

  • Un radio pornit. aioble activează implicit radioul BLE la prima utilizare, dar acesta poate fi controlat și explicit cu aioble.config() (care transmite mai departe către bluetooth.BLE.config() după ce se asigură că radioul este pornit) și oprit cu aioble.stop().

  • Unul sau mai multe roluri în desfășurare simultan. Pe partea periferică: un set înregistrat de servicii GATT (vezi aioble.register_services()) și o corutină aioble.advertise() în execuție. Pe partea centrală: un iterator aioble.scan() în execuție sau un aioble.Device.connect() în așteptare. Radioul multiplexează lucrul; aplicația vede fiecare rol ca pe o sarcină independentă.

11.8.3. Un periferic minimal

Cel mai mic program aioble util – un periferic care face publicitate unei singure caracteristici doar pentru citire – este scurt:

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 care nu face nimic mai mult decât să se conecteze și să citească o dată este la fel de scurt:

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

Ambele programe au în jur de cincisprezece linii și acoperă întregul flux de la „radioul este oprit” la „lucru util efectuat”.

11.8.4. Oprirea radioului

Pe o cameră alimentată cu baterie, radioul BLE este cel mai mare consumator discreționar din buget. Două manete contează.

Prima este implicită: aioble activează radioul la prima utilizare, iar radioul intră automat în repaus între evenimentele programate (explozii de publicitate, evenimente de conexiune, ferestre de scanare). Alegerea unor intervale mai lungi la aioble.advertise() / aioble.scan() și convenirea asupra unui interval de conexiune mai lung în momentul connect() menține radioul oprit proporțional mai mult timp. Tabelul de publicitate din Difuzare și scanare este ghidul practic aici.

A doua este oprirea explicită

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() dezactivează radioul BLE subiacent și demontează orice este în desfășurare – conexiunile deschise se întrerup, scanerele și agenții de publicitate se anulează, canalele L2CAP se închid. Corutinele care așteptau acele operațiuni generează excepțiile lor obișnuite (DeviceDisconnectedError și altele), care reprezintă mecanismul de curățare pentru care au fost scrise blocurile async with din jur. Apelarea oricărei corutine aioble ulterior activează din nou radioul de la rece.

Modelul tipic pentru o cameră de senzor periodică alimentată cu baterie este:

  • Trezire conform unui program (temporizator, senzor de mișcare, buton).

  • Rulează explozia de lucru BLE – fă publicitate, acceptă o conexiune, transmite valoarea, deconectează.

  • Apelează aioble.stop() și intră în repaus până la următoarea trezire.

11.8.5. Ce nu face aioble

aioble acoperă în mod deliberat GATT, GAP și L2CAP – straturile pe care le folosește o aplicație. Trei componente sunt în afara domeniului de aplicare:

  • Orice se află sub stratul de legătură. Selecția canalelor, saltul de frecvență, confirmarea pachetelor și criptarea la nivelul stratului de legătură se întâmplă toate în interiorul portului BLE și al siliciului controlerului; aioble nu expune cârlige la acel nivel.

  • Bluetooth Classic. aioble este exclusiv BLE. Legăturile audio, RFCOMM, A2DP și alte caracteristici de profil clasic nu fac parte din API.

  • Bluetooth Mesh. Stratul de rețea mesh al Bluetooth SIG (o stivă separată deasupra publicității BLE) nu este implementat pe cameră. Camera poate face publicitate și poate observa, dar nu poate participa la rolurile de releu / prieten / proxy ale unei rețele mesh.

11.8.6. Excepții

Patru tipuri de excepții provin din aioble. Fiecare se declanșează din interiorul unei corutine care aștepta o operațiune când ceva a mers prost; blocurile async with se derulează în mod curat atunci când acestea se propagă.

  • aioble.DeviceDisconnectedError – legătura BLE către partener s-a întrerupt în timp ce o operațiune GATT (read, write, notified, indicated, subscribe, exchange_mtu, …) era în desfășurare. Generată în interiorul oricărei corutine care aștepta. De departe cea mai frecventă excepție; prinde-o în orice cod care ar trebui să se reconecteze în caz de pierdere.

  • aioble.GattError – o operațiune GATT a ajuns la partener, dar s-a finalizat cu o stare ATT diferită de zero (scriere-cu-răspuns respinsă, indicare neconfirmată, citire-nepermisă, …). Codul de stare se află în atributul _status al excepției.

  • aioble.L2CAPDisconnectedError – canalul L2CAP s-a întrerupt în timp ce un send(), recvinto() sau flush() era în desfășurare. Oricare dintre părți poate fi închis canalul, sau conexiunea GAP subiacentă a dispărut.

  • aioble.L2CAPConnectionError – generată de l2cap_connect() atunci când ascultătorul a refuzat sau controlerul a eșuat configurarea canalului. Codul de stare Bluetooth este primul argument pozițional.

Operațiunile care primesc un timeout_ms explicit (apelurile de conectare / descoperire / citire / scriere / împerechere, plus timeout() ca wrapper) generează în plus asyncio.TimeoutError din asyncio atunci când termenul limită expiră înainte ca operațiunea să se finalizeze.