11.14. Récapitulatif

Vous avez parcouru le Bluetooth Low Energy depuis la radio jusqu’à l’API Python utilisée pour le piloter :

  • La motivation – le BLE est la réponse lorsque la caméra veut communiquer avec quelque chose de proche sans aucune infrastructure entre eux. Un téléphone dans la même pièce, un objet porté au poignet, une balise sur un mur. Courte portée, aucun réseau à rejoindre, presque aucune consommation.

  • La radio – 2,4 GHz, 40 canaux : trois pour la publicité, 37 pour les données de connexion, parcourus selon une séquence pseudo-aléatoire avec évitement adaptatif des canaux bruyants. Des paquets brefs, des radios la plupart du temps en sommeil.

  • La couche de liaison – le découpage des paquets en trames, l’adressage, l’ordonnancement des connexions, la retransmission et le chiffrement de la couche de liaison. Rien de tout cela n’est configuré depuis Python ; tout cela transparaît dans les paramètres de connexion et le MTU.

  • Generic Access Profile (GAP) – la découverte et la gestion des connexions. Quatre rôles : périphérique et émetteur (publicité), central et observateur (balayage). Les charges utiles de publicité transportent le nom local, les UUID de service, l’apparence et les données spécifiques au fabricant – 31 octets plus une réponse de balayage optionnelle de 31 octets. L’intervalle de connexion, la latence du périphérique et le délai de supervision déterminent ce à quoi ressemble une connexion ouverte.

  • Generic Attribute Profile (GATT) – un arbre de services, contenant chacun des caractéristiques, contenant chacune optionnellement des descripteurs, identifiés par des UUID (16 bits pour les normes Bluetooth-SIG, 128 bits pour les normes personnalisées). Cinq opérations : read et write (extraction, initiée par le client), notify et indicate (poussée, initiée par le serveur, abonnée via le Client Characteristic Configuration Descriptor). La taille de la charge utile est bornée par le MTU négocié.

  • L’API Pythonaioble transforme chaque schéma BLE en une coroutine asyncio. Un périphérique est aioble.advertise() en boucle sur les connexions, avec des objets Service / Characteristic construits une seule fois et validés par aioble.register_services(). Un central est aioble.scan() pour trouver un pair, connect() pour ouvrir la liaison, service() et characteristic() pour parcourir l’arbre GATT distant, puis read() / write() / subscribe() / notified() pour les données proprement dites. Les déconnexions remontent sous la forme de aioble.DeviceDisconnectedError dans la coroutine qui était en attente.

  • Les canaux L2CAP – l’échappatoire pour les flux d’octets en masse qui ne s’adaptent pas au modèle clé/valeur de GATT. aioble.DeviceConnection.l2cap_accept() / l2cap_connect() ouvrent un canal par application par-dessus la connexion GAP, avec un envoi / une réception contrôlés par crédit et un MTU plus grand que ce que GATT peut transporter.

  • Appairage et chiffrement – les liaisons BLE sont publiques par défaut. aioble.DeviceConnection.pair() initie un échange de clés qui produit une liaison chiffrée ; bond=True (valeur par défaut) conserve les clés afin que les connexions suivantes sautent la poignée de main. Sans mitm=True et une capacité d’entrée/sortie utilisable, le chiffrement protège contre les espions passifs mais pas contre une redirection active pendant l’appairage initial.

Cela suffit pour écrire des applications de caméra qui publient leur état en tant que périphérique, lisent des données de capteur en tant que central, poussent des valeurs en direct vers un téléphone via le BLE, sécurisent la liaison avec une étape d’appairage et de liaison, et – pour le rare cas du transfert en masse – quittent GATT pour passer dans un canal L2CAP.

11.14.1. Dépannage

Les échecs BLE sont surtout des décalages entre ce que les deux côtés attendent, et un inspecteur côté téléphone est le moyen le plus rapide de voir de quel côté les attentes sont erronées. L’outil standard est nRF Connect for Mobile (Nordic Semiconductor, gratuit sur Android et iOS) : il balaye, se connecte, parcourt la base de données GATT, lit et écrit des caractéristiques, et s’abonne aux notifications – de sorte que le comportement côté caméra peut être testé isolément, sans écrire la moindre application compagnon.

Les modes de défaillance courants :

  • « Mon appareil apparaît dans le scanner mais ne se connecte pas. » Le plus souvent, le paquet de publicité a connectable=False (mode émetteur), ou bien une connexion précédente est toujours ouverte et la caméra a déjà dépassé aioble.advertise(). Ajoutez des instructions print autour de l’appel à advertise pour le confirmer.

  • « exchange_mtu(512) s’est exécuté mais mes notifications sont toujours plafonnées à 20 octets. » Le MTU négocié est min(local, peer) – le téléphone ou la bibliothèque centrale n’a peut-être pas demandé un MTU plus grand de son côté, auquel cas la connexion reste à 23. Inspectez mtu une fois que exchange_mtu() retourne. Notez également que exchange_mtu() ne fonctionne qu’une seule fois par connexion ; appelez-le avant la première opération volumineuse.

  • « L’appairage échoue avec une erreur générique. » Deux coupables habituels : le décalage de capacité d’entrée/sortie (demander mitm=True sur une caméra qui déclare io=3 / aucune entrée aucune sortie – il n’y a aucun moyen de confirmer le code numérique, donc le moteur d’appairage abandonne), et une heure murale totalement erronée sur la caméra lorsque le pair l’exige. Réglez l’horloge avec ntptime.settime() avant la première tentative d’appairage.

  • « Les notifications n’arrivent jamais au client. » Deux choses à vérifier, dans l’ordre : (a) la caractéristique a-t-elle été déclarée avec notify=True ? – le bit de propriété doit être positionné côté serveur ; (b) le client a-t-il appelé subscribe() ? – sans écrire le Client Characteristic Configuration Descriptor (CCCD), le serveur croit qu’aucun client ne veut de notifications et les abandonne silencieusement.

  • « Le nom publié est tronqué ou absent. » La charge utile de publicité fait 31 octets, et les champs flags + UUID de service + apparence prennent chacun des octets sur le total. Un long name= combiné à plusieurs UUID de service provoque un débordement. Soit raccourcissez le nom, soit utilisez le balayage actif afin que la réponse de balayage (31 octets supplémentaires) transporte le débordement. nRF Connect affiche les deux moitiés séparément, ce qui rend la séparation évidente.

  • « La connexion L2CAP lève une erreur immédiatement. » Il s’agit généralement d’un décalage de PSM – les deux côtés doivent se mettre d’accord sur le même numéro de PSM hors bande. Une L2CAPConnectionError transporte le code d’état Bluetooth comme premier argument ; l’état 2 (« PSM not supported ») est le signe révélateur.

  • « Les connexions liées déclenchent toujours une poignée de main d’appairage complète à chaque reconnexion. » aioble.security.load_secrets() n’a pas été appelé au démarrage. Sans cela, les clés enregistrées sont dans la mémoire flash mais jamais chargées en mémoire, de sorte que l’identité du pair est inconnue et que l’appairage repart de zéro à chaque fois.

Lorsque tout le reste échoue, le module bas niveau bluetooth expose une fonction de rappel IRQ qui se déclenche pour chaque événement sous-jacent ; s’y abonner brièvement et imprimer les événements équivaut à une trace Wireshark pour le côté caméra.

11.14.2. Utiliser cette référence plus tard

Considérez les chapitres Bluetooth comme du matériel de référence ; y revenir pour la disposition exacte de la charge utile de publicité d’un périphérique ou le déroulement central de balayage et d’abonnement est l’usage prévu. Les pages de référence aioble — BLE asynchrone et bluetooth — Bluetooth bas niveau listent toutes les méthodes, tous les drapeaux et toutes les constantes en un seul endroit lorsque la question est simplement « quel est le nom exact de cet appel ».