11.7. Operații GATT

O caracteristică stă pur și simplu în baza de date GATT ca o valoare cu nume. Ceea ce o face utilă este setul mic și bine definit de operații pe care un client le poate executa asupra ei. Fiecare caracteristică declară ce operații suportă printr-o mască de biți de proprietăți – un server care nu are nimic de expus poate publica o valoare doar pentru citire, un registru de control poate fi doar pentru scriere, iar un senzor care transmite actualizări ar seta bitul de notificare. Clientul descoperă masca de biți în timpul descoperirii și o respectă.

Cele cinci operații sunt read, write, write without response, notify și indicate. Ele se împart în două grupuri – pull (clientul cere) și push (serverul trimite).

11.7.1. Pull: read și write

Aceste două sunt cele mai simple și arată exact ca niște apeluri de funcție.

  • Read. Clientul cere valoarea curentă, serverul o trimite înapoi. Un singur schimb dus-întors, clientul primește octeții pe care serverul i-a setat pentru acea caracteristică, iar serverul nu află nimic despre cine a citit.

  • Write. Clientul trimite octeți noi, serverul îi stochează (și, opțional, rulează logică de aplicație pe noua valoare). Există două variante:

    • Write with response – serverul confirmă, ridicând orice eroare de aplicație printr-un status diferit de zero. Fiabil, un singur schimb dus-întors.

    • Write without response – serverul stochează octeții în liniște; clientul nu primește nicio confirmare. Mai rapid (fără așteptarea unui schimb dus-întors pentru confirmare) și util pentru transmisie continuă, cu prețul aflării despre erori doar printr-o citire ulterioară pe un canal lateral.

În aioble, API-ul din partea clientului ascunde alegerea în spatele unei singure metode aioble.ClientCharacteristic.write() cu un argument response (True / False / None pentru selectare automată în funcție de ceea ce anunță partenerul).

11.7.2. Push: notify și indicate

Modelul pull este greșit pentru datele de la senzori. O bandă pentru ritm cardiac pe care telefonul ar trebui s-o interogheze în fiecare secundă ar consuma bateria pe o sută de evenimente radio inutile; una care împinge o valoare doar când are o citire nouă este chiar scopul BLE.

GATT rezolvă acest lucru cu operații inițiate de server. Clientul se abonează la o caracteristică; din acel moment, de fiecare dată când serverul actualizează valoarea, noua valoare este împinsă peste legătură către client. Două variante:

  • Notify. Trimite-și-uită. Serverul pune o notificare în coadă, stratul de legătură o transmite în timpul următorului eveniment de conexiune, clientul o primește. Nu există confirmare la nivel GATT; retransmisia normală a stratului de legătură gestionează pierderile pe partea radio, dar aplicația nu vede nicio confirmare că valoarea a fost procesată.

  • Indicate. Serverul trimite o notificare și așteaptă confirmarea la nivel GATT din partea clientului înainte de a o trimite pe următoarea. O singură indicație odată. Folosit când serverul are nevoie să știe că clientul a văzut efectiv valoarea – o caracteristică de alarmă critică, o confirmare de configurare.

Două diagrame alăturate ale unui server și ale unui client. În stânga, clientul trimite "read", iar serverul răspunde cu valoarea. Trei citiri una după alta, fiecare cu o pereche de săgeți. În dreapta, clientul trimite un singur "subscribe", apoi serverul împinge trei pachete "notify" la momentele pe care le alege, fără nicio cerere a clientului între ele.

Pull (read) versus push (notify). Cu notificările, clientul se abonează o singură dată, iar serverul împinge valori noi ori de câte ori acestea se schimbă.

Abonarea se face prin scrierea într-un descriptor atașat caracteristicii – Client Characteristic Configuration Descriptor (CCCD, 0x2902). Scrierea 0x0001 activează notificările, 0x0002 activează indicațiile, iar 0x0000 le dezactivează pe ambele. Metoda aioble.ClientCharacteristic.subscribe() efectuează scrierea în locul tău, cu argumentele notify=True și indicate=True.

Odată abonat, clientul așteaptă mesajele push primite cu notified() și indicated() – ambele corutine async care se suspendă până la sosirea următorului push.

11.7.3. MTU guvernează dimensiunea payload-ului

Fiecare operație este limitată de MTU-ul negociat asupra căruia s-a stabilit conexiunea la momentul stabilirii legăturii. MTU-ul implicit este de 23 de octeți, ceea ce lasă 20 de octeți pentru octeții valorii caracteristicii după antetul GATT. Orice depășește această valoare trebuie fie să încapă într-un MTU mai mare (negociat în sus prin aioble.DeviceConnection.exchange_mtu(), până la 512 octeți pe cameră), fie să fie împărțit în mai multe caracteristici sau mai multe notificări.

Citirile și scrierile inițiate de client ale valorilor mai mari decât MTU-ul sunt gestionate în culise de procedurile long ale GATT (Read Long / Prepare-Write + Execute-Write); aioble le rulează transparent, astfel încât apelarea read() / write() cu o valoare supradimensionată costă doar mai multe schimburi dus-întors. Notificările și indicațiile inițiate de server nu sunt fragmentate – un singur push este limitat de MTU, iar aplicația împarte orice este mai mare în mai multe notificări sau renunță complet la GATT.

Pentru transferuri cu adevărat mari – un cadru capturat, un lot de măsurători, un blob de firmware – răspunsul corect este de obicei să renunți complet la GATT și să folosești în schimb un canal L2CAP (vezi Canale L2CAP).

11.7.4. Cele două părți pe scurt

Cele cinci operații se expun diferit pe fiecare parte a conexiunii: