11.7. GATT-bewerkingen

Een characteristic zit gewoon in de GATT-database als een benoemde waarde. Wat hem nuttig maakt, is de kleine, goed gedefinieerde set bewerkingen die een client erop kan uitvoeren. Elke characteristic geeft aan welke bewerkingen hij ondersteunt via een property bitmask – een server die niets bloot te stellen heeft kan een alleen-lezen waarde publiceren, een control register kan alleen-schrijven zijn, een sensor die updates streamt zou de notify-bit zetten. De client ontdekt de bitmask tijdens de discovery en respecteert hem.

De vijf bewerkingen zijn read, write, write without response, notify en indicate. Ze vallen uiteen in twee groepen – pull (de client vraagt) en push (de server stuurt).

11.7.1. Pull: read en write

Deze twee zijn het eenvoudigst en zien er precies uit als functieaanroepen.

  • Read. De client vraagt om de huidige waarde, de server stuurt hem terug. Eén round trip, de client krijgt welke bytes de server ook voor die characteristic heeft ingesteld, de server krijgt niets te weten over wie heeft gelezen.

  • Write. De client stuurt nieuwe bytes, de server slaat ze op (en voert optioneel applicatielogica uit op de nieuwe waarde). Er bestaan twee varianten:

    • Write with response – de server bevestigt en werpt eventuele applicatiefouten bij een status die niet nul is. Betrouwbaar, één round trip.

    • Write without response – de server slaat de bytes stilzwijgend op; de client krijgt helemaal geen bevestiging. Sneller (geen round trip wachtend op de ack) en handig voor streamen, ten koste van het feit dat je fouten alleen via een side-channel-terugleesactie ontdekt.

In aioble verbergt de client-side API de keuze achter één enkele aioble.ClientCharacteristic.write()-methode met een response-trefwoord (True / False / None om automatisch te selecteren op basis van wat de peer adverteert).

11.7.2. Push: notify en indicate

Het pull-model is verkeerd voor sensordata. Een hartslagband die de telefoon elke seconde zou moeten pollen, zou de batterij verspillen aan honderd onnodige radio-events; een band die alleen een waarde pusht wanneer er een nieuwe meting is, is juist waar het bij BLE in de eerste plaats om draait.

GATT lost dit op met server-geïnitieerde bewerkingen. De client abonneert zich op een characteristic; vanaf dat moment wordt, telkens wanneer de server de waarde bijwerkt, de nieuwe waarde over de link naar de client gepusht. Twee varianten:

  • Notify. Fire-and-forget. De server zet een notificatie in de wachtrij, de link layer verzendt hem tijdens het volgende connection event, de client ontvangt hem. Er is geen bevestiging op GATT-niveau; de normale retransmissie van de link layer vangt verlies aan de radiokant op, maar de applicatie ziet geen bevestiging dat de waarde verwerkt is.

  • Indicate. De server stuurt een notificatie en wacht op de GATT-bevestiging van de client voordat hij de volgende stuurt. Eén indication tegelijk. Wordt gebruikt wanneer de server moet weten dat de client de waarde echt heeft gezien – een characteristic voor een kritiek alarm, een bevestiging van een configuratie.

Twee naast elkaar geplaatste diagrammen van een server en een client. Links stuurt de client "read", de server antwoordt met de waarde. Drie reads achter elkaar, telkens een paar pijlen. Rechts stuurt de client één enkele "subscribe", waarna de server drie "notify"-pakketten pusht op de momenten die hij kiest, zonder enig clientverzoek daartussen.

Pull (read) versus push (notify). Bij notificaties abonneert de client zich één keer en pusht de server nieuwe waarden telkens wanneer ze veranderen.

Abonneren gebeurt door naar een descriptor te schrijven die aan de characteristic gekoppeld is – de Client Characteristic Configuration Descriptor (CCCD, 0x2902). Het schrijven van 0x0001 schakelt notificaties in, 0x0002 schakelt indications in, 0x0000 schakelt beide uit. De methode aioble.ClientCharacteristic.subscribe() voert de write voor je uit, met de trefwoordvlaggen notify=True en indicate=True.

Eenmaal geabonneerd wacht de client op binnenkomende pushes met notified() en indicated() – beide async coroutines die opschorten totdat de volgende push binnenkomt.

11.7.3. De MTU bepaalt de payloadgrootte

Elke bewerking wordt beperkt door de onderhandelde MTU waar de verbinding bij het opzetten van de link op uitkwam. De standaard-MTU is 23 bytes, wat na de GATT-header 20 bytes overlaat voor de bytes van de characteristic-waarde. Alles wat groter is dan dat moet ofwel in een grotere MTU passen (naar boven onderhandeld via aioble.DeviceConnection.exchange_mtu(), tot 512 bytes op de camera) ofwel worden opgesplitst over meerdere characteristics of meerdere notificaties.

Door de client geïnitieerde reads en writes van waarden groter dan de MTU worden achter de schermen afgehandeld door GATT’s long-procedures (Read Long / Prepare-Write + Execute-Write); aioble voert deze transparant uit, dus het aanroepen van read() / write() met een te grote waarde kost gewoon meer round trips. Door de server geïnitieerde notificaties en indications worden niet gefragmenteerd – één push wordt begrensd door de MTU, en de applicatie splitst alles wat groter is op over meerdere notificaties of stapt helemaal van GATT af.

Voor echt grote overdrachten – een vastgelegd frame, een batch metingen, een firmware-blob – is het juiste antwoord meestal om helemaal van GATT af te stappen en in plaats daarvan een L2CAP-kanaal te gebruiken (zie L2CAP-kanalen).

11.7.4. De twee kanten in één oogopslag

De vijf bewerkingen worden aan elke kant van de verbinding anders blootgesteld: