11.11. Canali L2CAP

GATT e un modello chiave/valore. Le operazioni che offre (read, write, notify, indicate) spostano un breve valore alla volta, e il payload singolo piu grande che possono trasportare e quello consentito dall’MTU negoziato – al massimo qualche centinaio di byte. Funziona bene per letture di sensori, registri di comando e flag di stato. Crolla con kilobyte o megabyte: suddividere un lungo blob in centinaia di piccole write costa round trip rispetto ai quali la radio e molto piu veloce.

Per i flussi di dati massivi – un frame catturato che la camera trasmette in streaming a uno smartphone, un’immagine di aggiornamento over-the-air, un’esportazione in batch di misurazioni – BLE offre un percorso alternativo: il Logical Link Control and Adaptation Protocol, L2CAP. L2CAP si colloca tra il link layer e GATT e consente a un’applicazione di rivendicare un proprio canale orientato alla connessione sopra lo stesso collegamento radio. Il canale e un percorso di byte con controllo di flusso a credito, con un MTU per pacchetto molto piu grande e senza framing GATT nel mezzo.

11.11.1. Quando usare L2CAP

I canali L2CAP sono lo strumento giusto quando:

  • Il trasferimento supera qualche centinaio di byte.

  • Entrambi gli estremi sanno che verra usato un canale L2CAP (non e esposto nel payload di advertising; il client deve conoscere fuori banda il numero del protocol/service multiplexer, ovvero PSM, del canale).

  • L’applicazione e disposta a rinunciare alle comodita di GATT: nessuna indirizzabilita integrata tramite UUID, nessuna individuabilita da parte del client attraverso app standard, nessuna notifica.

Il caso piu comune nelle applicazioni basate su aioble e lo spostamento di un blob binario tra due software che conoscono entrambi la convenzione del PSM – un protocollo personalizzato camera-smartphone, una coppia di camere openmv che dialogano tra loro, un percorso interno di aggiornamento firmware sotto il servizio GATT di un peripheral.

Per tutto il resto, resta su GATT. Uno stato breve, un registro di controllo, una lettura di un sensore: tutto questo appartiene a una caratteristica.

11.11.2. Stabilire un canale

L2CAP viene eseguito sopra una aioble.DeviceConnection esistente, quindi il flusso lato GAP di discovery / advertising / connessione e esattamente lo stesso di GATT. Una volta che entrambi i lati hanno una connessione, un lato resta in ascolto su un PSM e l’altro lato vi si connette.

Il PSM e semplicemente un piccolo intero. Il Bluetooth SIG riserva la parte bassa dell’intervallo per l’uso standardizzato (0x0001-0x007F); per i canali specifici dell’applicazione usa un numero dell’intervallo dinamico (0x0080-0x00FF per i PSM fissi, da 0x0040 in poi tipicamente liberi per l’uso personalizzato). Entrambi i lati devono concordare il valore in anticipo.

L’MTU su un canale L2CAP e la piu grande SDU (Service Data Unit) singola che ciascun lato consegnera in un solo send() – non l’MTU del collegamento BLE. Aioble frammenta automaticamente i payload piu grandi. L’host BLE della camera limita l’MTU L2CAP a 1017 byte; 512 e un valore predefinito ragionevole che lascia margine su entrambi i lati senza consumare RAM.

Sul lato listener (ad es. la camera come peripheral):

async def serve_l2cap(connection, image_bytes):
    channel = await connection.l2cap_accept(psm=0x80, mtu=512)
    async with channel:
        # image_bytes is a bytearray -- e.g. csi0.snapshot().bytearray()
        # or a compressed JPEG buffer. send() fragments into MTU-sized
        # chunks automatically and awaits flow-control credits between.
        await channel.send(image_bytes)
        await channel.flush()

Sul lato connector (ad es. uno smartphone o un central):

async def open_l2cap(connection, total_bytes):
    channel = await connection.l2cap_connect(psm=0x80, mtu=512)
    async with channel:
        image_bytes = bytearray(total_bytes)
        view = memoryview(image_bytes)
        received = 0
        while received < total_bytes:
            n = await channel.recvinto(view[received:])
            if n == 0:
                break
            received += n
        return image_bytes

l2cap_accept() blocca finche il peer non si connette (o finche non scatta timeout_ms); l2cap_connect() blocca finche il listener non accetta (o fallisce). Entrambi restituiscono un aioble.L2CAPChannel, esso stesso un context manager asincrono che chiude il canale all’uscita.

11.11.3. Invio e ricezione

Le due operazioni principali su un canale sono send() (scrive byte verso il peer) e recvinto() (legge in un buffer pre-allocato). Entrambe sono coroutine.

  • send() frammenta il buffer in blocchi della dimensione dell’MTU e attende i crediti di controllo di flusso del link layer tra di essi. Una send lunga e un singolo await dal punto di vista dell’applicazione; internamente puo accodare molti pacchetti e mettersi in pausa ogni volta che i crediti di ricezione del peer si esauriscono.

  • recvinto() riempie il buffer passato con tutto cio che e disponibile (fino all’MTU del canale) e restituisce il conteggio dei byte. Attende se non c’e nulla di disponibile.

  • available() restituisce True in modo sincrono se ci sono dati bufferizzati pronti – utile per il polling senza sospendere.

  • flush() attende finche qualsiasi send in sospeso non e stata completamente trasmessa al controller.

I canali L2CAP sono simili a uno stream nel senso che i byte arrivano in ordine e senza perdite, ma i confini di una singola send sono preservati: ogni SDU esce da un singolo recvinto. Questo e diverso da TCP, dove i confini di una send() possono spalmarsi su piu chiamate recv().

11.11.4. Gestione della disconnessione

Il canale scompare in tre condizioni: uno dei due lati chiama disconnect(), la connessione GAP sottostante cade, oppure arriva la disconnessione a livello L2CAP. Le operazioni attive sollevano aioble.L2CAPDisconnectedError. Come per il lato GATT, questo emerge come un’eccezione nella coroutine che era in attesa, e il blocco async with channel esce in modo pulito.

Se un canale diventa irraggiungibile a causa di una disconnessione a livello GAP, l’applicazione torna all’advertising o alla scansione esattamente come farebbe per una disconnessione GATT.

11.11.5. Costo in memoria

MTU piu grandi e code piu lunghe usano piu RAM su entrambi i lati. Un MTU di 512 byte piu un buffer di ricezione per canale corrisponde a circa 1 KB per canale – non gratuito su una camera di piccole dimensioni se diversi canali sono aperti contemporaneamente. Attieniti a un canale per connessione e scegli un MTU che corrisponda alla dimensione del messaggio prevista; il valore predefinito di un L2CAPChannel per DeviceConnection e sufficiente per la maggior parte delle applicazioni.

L2CAP e la valvola di sicurezza di BLE. GATT e cio a cui quasi ogni applicazione ricorre per prima cosa, e il resto degli esempi central / peripheral di questa sezione si attiene a GATT. L’API a sapore di canale e la risposta quando un’applicazione supera il modello chiave/valore.