11.7. Operazioni GATT¶
Una caratteristica si limita a stare nel database GATT come un valore con nome. Ciò che la rende utile è l’insieme ridotto e ben definito di operazioni che un client può eseguire su di essa. Ogni caratteristica dichiara quali operazioni supporta tramite una bitmask di proprietà – un server che non ha nulla da esporre può pubblicare un valore di sola lettura, un registro di controllo potrebbe essere di sola scrittura, un sensore che trasmette aggiornamenti imposterebbe il bit notify. Il client scopre la bitmask durante la fase di discovery e la rispetta.
Le cinque operazioni sono read, write, write without response, notify e indicate. Si suddividono in due gruppi – pull (il client chiede) e push (il server invia).
11.7.1. Pull: read e write¶
Queste due sono le più semplici e somigliano esattamente a delle chiamate di funzione.
Read. Il client chiede il valore corrente, il server glielo rimanda. Un solo round trip, il client ottiene i byte che il server ha impostato per quella caratteristica, il server non ottiene alcuna informazione su chi ha effettuato la lettura.
Write. Il client invia nuovi byte, il server li memorizza (ed eventualmente esegue una logica applicativa sul nuovo valore). Esistono due varianti:
Write with response – il server conferma, sollevando un eventuale errore applicativo in caso di stato diverso da zero. Affidabile, un solo round trip.
Write without response – il server memorizza i byte silenziosamente; il client non riceve alcuna conferma. Più veloce (nessun round trip in attesa dell’ack) e utile per lo streaming, al costo di scoprire gli errori solo tramite una rilettura via canale laterale.
In aioble, l’API lato client nasconde la scelta dietro un unico metodo aioble.ClientCharacteristic.write() con un argomento response (True / False / None per selezionare automaticamente in base a ciò che il peer pubblicizza).
11.7.2. Push: notify e indicate¶
Il modello pull è inadatto ai dati dei sensori. Una fascia cardio che il telefono dovesse interrogare ogni secondo consumerebbe batteria per un centinaio di eventi radio inutili; una che invece invia un valore solo quando ha una nuova lettura è proprio lo scopo per cui esiste BLE.
GATT risolve questo problema con operazioni avviate dal server. Il client si iscrive a una caratteristica; da quel momento in poi, ogni volta che il server aggiorna il valore, il nuovo valore viene inviato attraverso il collegamento al client. Due varianti:
Notify. Fire-and-forget. Il server mette in coda una notifica, il link layer la trasmette durante il successivo evento di connessione, il client la riceve. Non c’è alcuna conferma a livello GATT; la normale ritrasmissione del link layer gestisce le perdite sul lato radio, ma l’applicazione non riceve alcuna conferma che il valore sia stato elaborato.
Indicate. Il server invia una notifica e attende la conferma a livello GATT da parte del client prima di inviare la successiva. Una indication alla volta. Si usa quando il server ha bisogno di sapere che il client ha effettivamente visto il valore – una caratteristica di allarme critico, una conferma di configurazione.
Pull (read) contro push (notify). Con le notifiche, il client si iscrive una sola volta e il server invia nuovi valori ogni volta che cambiano.¶
L’iscrizione avviene scrivendo su un descrittore associato alla caratteristica – il Client Characteristic Configuration Descriptor (CCCD, 0x2902). Scrivere 0x0001 abilita le notifiche, 0x0002 abilita le indication, 0x0000 disabilita entrambe. Il metodo aioble.ClientCharacteristic.subscribe() esegue la scrittura per te, con i flag keyword notify=True e indicate=True.
Una volta iscritto, il client attende i push in arrivo con notified() e indicated() – entrambe coroutine asincrone che si sospendono finché non arriva il push successivo.
11.7.3. L’MTU governa la dimensione del payload¶
Ogni operazione è vincolata dall”MTU negoziato su cui la connessione si è stabilizzata al momento del link-up. L’MTU predefinito è di 23 byte, il che lascia 20 byte per i byte di valore della caratteristica dopo l’header GATT. Qualsiasi cosa più grande deve o rientrare in un MTU maggiore (negoziato verso l’alto tramite aioble.DeviceConnection.exchange_mtu(), fino a 512 byte sulla camera) oppure essere suddivisa in più caratteristiche o più notifiche.
Letture e scritture avviate dal client di valori più grandi dell’MTU sono gestite dietro le quinte dalle procedure long di GATT (Read Long / Prepare-Write + Execute-Write); aioble le esegue in modo trasparente, quindi chiamare read() / write() con un valore di dimensioni eccessive costa semplicemente più round trip. Le notifiche e le indication avviate dal server non vengono frammentate – un singolo push è limitato dall’MTU, e l’applicazione suddivide qualsiasi cosa più grande in più notifiche oppure abbandona del tutto GATT.
Per trasferimenti realmente grandi – un frame catturato, un batch di misurazioni, un blob di firmware – la risposta giusta è di solito abbandonare del tutto GATT e usare invece un canale L2CAP (vedi Canali L2CAP).
11.7.4. I due lati a colpo d’occhio¶
Le cinque operazioni si presentano in modo diverso su ciascun lato della connessione:
Sul server (il dispositivo periferico, nella configurazione comune):
aioble.Characteristic.read()– legge il valore locale corrente dal database GATT (il lato server di «ciò che vedrebbe il client»).aioble.Characteristic.write()– aggiorna il valore locale, inviando eventualmente l’aggiornamento a ogni client iscritto.aioble.Characteristic.notify()/indicate()– inviano un push a uno specifico client.aioble.Characteristic.written()– attende la successiva scrittura in arrivo da qualsiasi client.aioble.Characteristic.on_read()– callback invocata in modo sincrono quando un client effettua una lettura, utile per calcolare un valore su richiesta.
Sul client (il dispositivo centrale, nella configurazione comune):
aioble.ClientCharacteristic.read()– chiede al server il valore corrente.aioble.ClientCharacteristic.write()– invia un nuovo valore, con o senza risposta.aioble.ClientCharacteristic.subscribe()– abilita / disabilita notifiche e indication.aioble.ClientCharacteristic.notified()/indicated()– attendono il push successivo.