11.7. Opérations GATT

Une caractéristique se contente d’exister dans la base de données GATT en tant que valeur nommée. Ce qui la rend utile, c’est le petit ensemble bien défini d’opérations qu’un client peut effectuer dessus. Chaque caractéristique déclare quelles opérations elle prend en charge sous forme de masque de bits de propriétés – un serveur qui n’a rien à exposer peut publier une valeur en lecture seule, un registre de contrôle peut être en écriture seule, un capteur qui diffuse des mises à jour positionnerait le bit de notification. Le client découvre le masque de bits lors de la découverte et le respecte.

Les cinq opérations sont read, write, write without response, notify et indicate. Elles se répartissent en deux groupes – pull (le client demande) et push (le serveur envoie).

11.7.1. Pull : read et write

Ces deux opérations sont les plus simples et ressemblent exactement à des appels de fonction.

  • Read. Le client demande la valeur courante, le serveur la renvoie. Un aller-retour, le client obtient les octets que le serveur a définis pour cette caractéristique, le serveur n’apprend rien sur qui a lu.

  • Write. Le client envoie de nouveaux octets, le serveur les stocke (et exécute éventuellement une logique applicative sur la nouvelle valeur). Il en existe deux variantes :

    • Write with response – le serveur accuse réception, levant toute erreur applicative en cas de statut non nul. Fiable, un aller-retour.

    • Write without response – le serveur stocke les octets silencieusement ; le client ne reçoit aucun accusé de réception. Plus rapide (pas d’aller-retour à attendre pour l’accusé) et utile pour la diffusion, au prix de ne découvrir les erreurs que via une relecture par canal secondaire.

Dans aioble, l’API côté client masque ce choix derrière une unique méthode aioble.ClientCharacteristic.write() avec un mot-clé response (True / False / None pour une sélection automatique selon ce qu’annonce le pair).

11.7.2. Push : notify et indicate

Le modèle pull est inadapté aux données de capteur. Une ceinture de fréquence cardiaque que le téléphone devrait interroger chaque seconde épuiserait la batterie sur une centaine d’événements radio inutiles ; une ceinture qui pousse une valeur uniquement lorsqu’elle dispose d’une nouvelle mesure est précisément la raison d’être de BLE.

GATT résout cela avec des opérations initiées par le serveur. Le client s’abonne à une caractéristique ; à partir de ce moment, chaque fois que le serveur met à jour la valeur, la nouvelle valeur est poussée à travers la liaison vers le client. Deux variantes :

  • Notify. Sans suivi (fire-and-forget). Le serveur met une notification en file d’attente, la couche de liaison la transmet lors du prochain événement de connexion, le client la reçoit. Il n’y a aucun accusé de réception au niveau GATT ; la retransmission normale de la couche de liaison gère les pertes côté radio, mais l’application ne reçoit aucune confirmation que la valeur a été traitée.

  • Indicate. Le serveur envoie une notification et attend la confirmation du client au niveau GATT avant d’envoyer la suivante. Une indication à la fois. Utilisé lorsque le serveur a besoin de savoir que le client a réellement vu la valeur – une caractéristique d’alarme critique, un accusé de réception de configuration.

Deux schémas côte à côte d'un serveur et d'un client. À gauche, le client envoie « read », le serveur répond avec la valeur. Trois lectures d'affilée, chacune une paire de flèches. À droite, le client envoie un seul « subscribe », puis le serveur pousse trois paquets « notify » aux moments qu'il choisit, sans aucune requête du client entre eux.

Pull (read) versus push (notify). Avec les notifications, le client s’abonne une fois et le serveur pousse les nouvelles valeurs chaque fois qu’elles changent.

L’abonnement se fait en écrivant dans un descripteur rattaché à la caractéristique – le Client Characteristic Configuration Descriptor (CCCD, 0x2902). Écrire 0x0001 active les notifications, 0x0002 active les indications, 0x0000 désactive les deux. La méthode aioble.ClientCharacteristic.subscribe() effectue l’écriture à votre place, avec les indicateurs de mot-clé notify=True et indicate=True.

Une fois abonné, le client attend les poussées entrantes avec notified() et indicated() – deux coroutines asynchrones qui se suspendent jusqu’à l’arrivée de la prochaine poussée.

11.7.3. Le MTU régit la taille de la charge utile

Chaque opération est contrainte par le MTU négocié, sur lequel la connexion s’est accordée au moment de l’établissement de la liaison. Le MTU par défaut est de 23 octets, ce qui laisse 20 octets pour les octets de valeur de la caractéristique après l’en-tête GATT. Tout ce qui dépasse cette taille doit soit tenir dans un MTU plus grand (négocié à la hausse via aioble.DeviceConnection.exchange_mtu(), jusqu’à 512 octets sur la caméra), soit être réparti sur plusieurs caractéristiques ou plusieurs notifications.

Les lectures et écritures de valeurs plus grandes que le MTU initiées par le client sont prises en charge en coulisses par les procédures long de GATT (Read Long / Prepare-Write + Execute-Write) ; aioble les exécute de manière transparente, de sorte qu’appeler read() / write() avec une valeur surdimensionnée ne fait qu’engendrer davantage d’allers-retours. Les notifications et indications initiées par le serveur ne sont pas fragmentées – une poussée est bornée par le MTU, et l’application répartit tout ce qui est plus grand sur plusieurs notifications ou abandonne complètement GATT.

Pour des transferts réellement volumineux – une trame capturée, un lot de mesures, un blob de micrologiciel – la bonne réponse consiste généralement à abandonner complètement GATT et à utiliser plutôt un canal L2CAP (voir Canaux L2CAP).

11.7.4. Les deux côtés en un coup d’œil

Les cinq opérations s’exposent différemment de chaque côté de la connexion :