11.7. GATT-Operationen¶
Eine Charakteristik liegt einfach als benannter Wert in der GATT-Datenbank. Nützlich wird sie durch den kleinen, klar definierten Satz von Operationen, die ein Client auf ihr ausführen kann. Jede Charakteristik deklariert, welche Operationen sie unterstützt, als eine Property-Bitmaske – ein Server, der nichts offenzulegen hat, kann einen schreibgeschützten Wert veröffentlichen, ein Steuerregister könnte nur schreibbar sein, ein Sensor, der Aktualisierungen streamt, würde das Notify-Bit setzen. Der Client ermittelt die Bitmaske während der Discovery und respektiert sie.
Die fünf Operationen sind read, write, write without response, notify und indicate. Sie teilen sich in zwei Gruppen auf – Pull (der Client fragt an) und Push (der Server sendet).
11.7.1. Pull: read und write¶
Diese beiden sind die einfachsten und sehen genau wie Funktionsaufrufe aus.
Read. Der Client fragt den aktuellen Wert ab, der Server sendet ihn zurück. Ein Round Trip, der Client erhält die Bytes, die der Server für diese Charakteristik gesetzt hat, der Server erfährt nichts darüber, wer gelesen hat.
Write. Der Client sendet neue Bytes, der Server speichert sie (und führt optional Anwendungslogik auf dem neuen Wert aus). Es gibt zwei Varianten:
Write with response – der Server bestätigt und löst bei einem von null verschiedenen Status einen Anwendungsfehler aus. Zuverlässig, ein Round Trip.
Write without response – der Server speichert die Bytes stillschweigend; der Client erhält überhaupt keine Bestätigung. Schneller (kein Round Trip, der auf das Ack wartet) und nützlich für Streaming, auf Kosten dessen, dass man von Fehlern nur über ein Side-Channel-Readback erfährt.
In aioble verbirgt die clientseitige API die Wahl hinter einer einzigen Methode aioble.ClientCharacteristic.write() mit einem response-Schlüsselwort (True / False / None zur automatischen Auswahl basierend auf dem, was der Peer ankündigt).
11.7.2. Push: notify und indicate¶
Das Pull-Modell ist für Sensordaten ungeeignet. Ein Herzfrequenzgurt, den das Telefon jede Sekunde abfragen müsste, würde bei hundert unnötigen Funkereignissen Akku verbrauchen; einer, der einen Wert nur dann sendet, wenn er einen neuen Messwert hat, ist überhaupt erst der Sinn von BLE.
GATT löst dies mit serverinitiierten Operationen. Der Client abonniert eine Charakteristik; von diesem Punkt an wird jedes Mal, wenn der Server den Wert aktualisiert, der neue Wert über die Verbindung an den Client gepusht. Zwei Varianten:
Notify. Fire-and-forget. Der Server stellt eine Benachrichtigung in die Warteschlange, der Link Layer überträgt sie während des nächsten Connection Events, der Client empfängt sie. Es gibt keine Bestätigung auf GATT-Ebene; die normale Neuübertragung des Link Layers behandelt Verluste auf der Funkseite, aber die Anwendung erhält keine Bestätigung, dass der Wert verarbeitet wurde.
Indicate. Der Server sendet eine Benachrichtigung und wartet auf die Bestätigung des Clients auf GATT-Ebene, bevor er die nächste sendet. Eine Indication nach der anderen. Wird verwendet, wenn der Server wissen muss, dass der Client den Wert tatsächlich gesehen hat – eine kritische Alarmcharakteristik, eine Konfigurationsbestätigung.
Pull (read) versus Push (notify). Bei Benachrichtigungen abonniert der Client einmal und der Server pusht neue Werte, sobald sie sich ändern.¶
Das Abonnieren erfolgt durch Schreiben in einen an die Charakteristik angehängten Deskriptor – den Client Characteristic Configuration Descriptor (CCCD, 0x2902). Das Schreiben von 0x0001 aktiviert Benachrichtigungen, 0x0002 aktiviert Indications, 0x0000 deaktiviert beides. Die Methode aioble.ClientCharacteristic.subscribe() führt das Schreiben für Sie aus, mit den Schlüsselwort-Flags notify=True und indicate=True.
Nach dem Abonnieren wartet der Client mit notified() und indicated() auf eingehende Pushes – beides async-Coroutinen, die suspendieren, bis der nächste Push eintrifft.
11.7.3. Die MTU bestimmt die Nutzlastgröße¶
Jede Operation ist durch die ausgehandelte MTU beschränkt, auf die sich die Verbindung zum Verbindungszeitpunkt geeinigt hat. Die Standard-MTU beträgt 23 Byte, was nach dem GATT-Header 20 Byte für Charakteristik-Wertbytes übrig lässt. Alles Größere muss entweder in eine größere MTU passen (nach oben ausgehandelt über aioble.DeviceConnection.exchange_mtu(), bis zu 512 Byte auf der Kamera) oder auf mehrere Charakteristiken bzw. mehrere Benachrichtigungen aufgeteilt werden.
Clientinitiierte Lese- und Schreibvorgänge von Werten, die größer als die MTU sind, werden von den langen Prozeduren von GATT im Hintergrund behandelt (Read Long / Prepare-Write + Execute-Write); aioble führt diese transparent aus, sodass der Aufruf von read() / write() mit einem übergroßen Wert lediglich mehr Round Trips kostet. Serverinitiierte Notifications und Indications werden nicht fragmentiert – ein Push ist durch die MTU begrenzt, und die Anwendung teilt alles Größere in mehrere Benachrichtigungen auf oder verlässt GATT vollständig.
Für wirklich große Übertragungen – ein erfasstes Einzelbild, einen Batch von Messungen, einen Firmware-Blob – ist die richtige Antwort meist, GATT vollständig zu verlassen und stattdessen einen L2CAP-Kanal zu verwenden (siehe L2CAP-Kanäle).
11.7.4. Die beiden Seiten auf einen Blick¶
Die fünf Operationen stellen sich auf jeder Seite der Verbindung unterschiedlich dar:
Auf dem Server (dem Peripheral, in der üblichen Anordnung):
aioble.Characteristic.read()– liest den aktuellen lokalen Wert aus der GATT-Datenbank (die Serverseite von „was der Client sehen würde“).aioble.Characteristic.write()– aktualisiert den lokalen Wert und pusht die Aktualisierung optional an jeden abonnierten Client.aioble.Characteristic.notify()/indicate()– sendet einen Push an einen bestimmten Client.aioble.Characteristic.written()– wartet auf den nächsten eingehenden Schreibvorgang von einem beliebigen Client.aioble.Characteristic.on_read()– Callback, der synchron aufgerufen wird, wenn ein Client liest, nützlich zum Berechnen eines Werts bei Bedarf.
Auf dem Client (dem Central, in der üblichen Anordnung):
aioble.ClientCharacteristic.read()– fragt den Server nach dem aktuellen Wert.aioble.ClientCharacteristic.write()– sendet einen neuen Wert, mit oder ohne Antwort.aioble.ClientCharacteristic.subscribe()– aktiviert / deaktiviert Benachrichtigungen und Indications.aioble.ClientCharacteristic.notified()/indicated()– wartet auf den nächsten Push.