11.4. Diffusion et balayage

Deux appareils BLE qui ne se sont jamais rencontrés auparavant doivent d’abord se trouver. La mise en réseau résout cela en attribuant à chaque appareil une adresse issue d’un pool partagé et en laissant chaque côté joindre l’autre via des routeurs. Le BLE n’a ni routeurs, ni pool partagé et, entre la plupart des paires d’appareils, aucune relation préalable. Le Generic Access Profile (GAP) résout la découverte par un schéma de diffusion et d’écoute. Un côté diffuse (advertise) : il transmet un court paquet sur les trois canaux de diffusion à intervalle régulier, décrivant qui il est. L’autre côté balaye (scan) : il parcourt ces trois mêmes canaux à l’écoute de ces paquets.

Le GAP définit quatre rôles autour de ce schéma, chacun étant une combinaison spécifique de diffusion et d’écoute.

11.4.1. Les quatre rôles GAP

Une matrice deux par deux. Les lignes sont intitulées « diffuse » et « ne diffuse pas ». Les colonnes sont intitulées « accepte les connexions » et « n'accepte pas les connexions ». Les quatre cellules contiennent les noms des rôles : Peripheral, Broadcaster, Central, Observer.

Les quatre rôles GAP. L’axe vertical indique si l’appareil diffuse ; l’axe horizontal indique s’il accepte (ou initie) des connexions.

  • Un peripheral diffuse des paquets qui disent « je suis là et tu peux te connecter à moi ». Lorsqu’un autre appareil ouvre une connexion, le peripheral cesse de diffuser et commence à traiter les requêtes GATT. Les ceintures cardiofréquencemètres, les thermomètres et la plupart des caméras-en-tant-que-capteurs agissent comme des peripherals.

  • Un central balaye à la recherche de peripherals, en choisit un et initie une connexion. Une fois connecté, il dialogue en GATT en tant que client. Les téléphones, les ordinateurs portables et les caméras agissant comme collecteurs de données sont des centrals.

  • Un broadcaster diffuse mais n’accepte jamais de connexions. Sa charge utile de diffusion est la donnée : il n’y a rien à connecter. Les iBeacons et la plupart des balises de présence en magasin sont des broadcasters.

  • Un observer balaye ces annonces et en lit la charge utile, là encore sans jamais se connecter. Une caméra qui écoute les balises à proximité et agit selon ce qu’elle entend est un observer.

Un même appareil peut jouer plusieurs rôles en même temps : une caméra peut être un peripheral qui publie son propre état et un central qui se connecte à un capteur voisin. La radio multiplexe le travail.

11.4.2. Ce que contient un paquet de diffusion

Un paquet de diffusion est petit : 31 octets de charge utile, ou 62 si le diffuseur publie également une réponse de balayage (scan response) que les balayeurs peuvent demander à la volée. La charge utile est une liste de courts champs typés :

  • Flags. Connectable ou non, découvrable général / limité.

  • Nom local. Une courte chaîne lisible par l’humain : le nom que le système d’exploitation d’un téléphone ou d’un ordinateur portable affiche dans son menu Bluetooth.

  • UUID de service. Une liste d’identifiants de service GATT que l’appareil héberge, afin qu’un balayeur puisse reconnaître les peripherals compatibles sans se connecter d’abord. Une ceinture cardiofréquencemètre diffuse 0x180D (l’UUID standard du service Heart-Rate), et une application de fréquence cardiaque sur le téléphone sait, sur cette seule base, que l’appareil mérite qu’on s’y connecte.

  • Appearance. Une valeur sur 16 bits issue de la liste des numéros assignés Bluetooth (capteur, média générique, montre générique, etc.) : un indice donné au central sur ce qu’il convient d’afficher.

  • Données spécifiques au fabricant. Des octets libres préfixés par un identifiant de société. Les iBeacons utilisent ce champ pour transporter leur UUID, leur major et leur minor ; les applications personnalisées peuvent y mettre ce qu’elles veulent.

Les charges utiles de diffusion sont restreintes. La limite de 31 octets fait du choix de ce qu’on y inclut une véritable décision de conception : un long nom lisible par l’humain peut rapidement ne plus laisser de place aux UUID de service. L’API aioble.advertise() prend chacun de ces éléments comme argument nommé et assemble les octets pour vous, en débordant automatiquement dans la réponse de balayage si le paquet principal est plein.

11.4.3. Balayage actif et passif

Un balayeur peut fonctionner en mode passif, où il écoute les paquets de diffusion et analyse ce qui arrive, ou en mode actif, où il envoie en plus une requête de balayage (scan request) à chaque diffuseur et analyse la réponse de balayage qui revient.

Le balayage passif ne voit que le paquet de diffusion initial (jusqu’à 31 octets). Le balayage actif double cela : la réponse de balayage représente 31 octets supplémentaires que le peripheral peut utiliser pour des champs qui n’ont pas tenu. Le balayage actif consomme aussi de l’énergie des deux côtés, puisque le balayeur émet et que le diffuseur transmet un paquet supplémentaire ; c’est donc un choix plutôt qu’un comportement par défaut.

Dans l’API aioble, active=True sur aioble.scan() bascule le mode, et chaque ScanResult expose les adv_data combinées avec resp_data ainsi que des assistants comme result.name() et result.services() qui masquent l’analyse au niveau des octets.

Note

Les attributs adv_data et resp_data sont les charges utiles brutes de diffusion et de réponse de balayage (bytes). Les assistants – name(), services(), manufacturer() – couvrent les champs standard courants et constituent le bon choix dans 99 % des cas. Ne recourez aux octets bruts que lorsque vous avez besoin d’un champ propriétaire que les assistants n’analysent pas (URL Eddystone, UUID/major/minor d’iBeacon, types de diffusion personnalisés). La disposition des octets est la disposition TLV standard : chaque champ est length, type, value....

11.4.4. L’intervalle de diffusion

La fréquence à laquelle le peripheral diffuse est un compromis entre consommation et latence de découverte. Des annonces émises toutes les 20 ms sont captées presque immédiatement par un balayeur mais maintiennent la radio occupée et déchargent la batterie ; des annonces toutes les secondes ne consomment presque rien mais ralentissent la détection de l’appareil par un balayeur.

interval_us sur aioble.advertise() définit l’intervalle en microsecondes :

  • 20 000 à 100 000 us (20 ms - 100 ms) : appairage rapide, application attendant une réponse rapide, appareil branché sur secteur.

  • 250 000 à 1 000 000 us (250 ms - 1 s) : une valeur par défaut raisonnable pour un peripheral alimenté par batterie qui veut être découvrable sans consommer trop de charge.

  • Au-dessus de 1 000 000 us : diffusion d’arrière-plan lente, balises qui envoient une mise à jour de position toutes les quelques secondes.

Le côté balayeur a ses propres réglages : aioble.scan() prend interval_us et window_us (à quelle fréquence le balayeur réveille sa radio et combien de temps il écoute à chaque fois). Les valeurs par défaut conviennent ; le seul changement courant consiste à les rendre égales pour un balayage continu lorsque la batterie n’est pas une préoccupation.

11.4.5. Schémas sans connexion : broadcaster et observer

Les pages Agir comme périphérique et Agir en tant que central détaillent la forme connectable de l’API, où un peripheral accepte une connexion et où les deux côtés échangent des données via GATT. L’autre forme est sans connexion : un broadcaster transmet la charge utile en tant qu’annonce, et tout observer à portée peut la lire sans jamais se connecter. Les balises, les capteurs de présence et la télémétrie unidirectionnelle relèvent tous de ce cas.

Un broadcaster est aioble.advertise() avec connectable=False. Les données spécifiques au fabricant transportent la charge utile:

import aioble
import asyncio
import struct

_COMPANY_ID = const(0xFFFF)                # 0xFFFF is "no specific vendor"

async def beacon():
    seq = 0
    while True:
        seq = (seq + 1) & 0xFFFF
        payload = struct.pack("<H", seq)
        await aioble.advertise(
            interval_us=500000,
            connectable=False,
            name="openmv-beacon",
            manufacturer=(_COMPANY_ID, payload),
            timeout_ms=1000,                # one cycle, then loop
        )

asyncio.run(beacon())

Le mot-clé timeout_ms met fin à l’appel d’annonce après une seconde ; la boucle externe la réémet avec le numéro de séquence suivant afin que les écouteurs voient des données fraîches. Le drapeau connectable=False est ce qui rend l’annonce de type broadcaster : la caméra ne répondra pas à une demande de connexion même si elle en reçoit une.

Un observer est le balayeur en lecture seule correspondant. Il exécute aioble.scan() indéfiniment, analyse les annonces entrantes et n’appelle jamais connect():

import aioble
import asyncio

_COMPANY_ID = const(0xFFFF)

async def watch():
    async with aioble.scan(duration_ms=0, active=False) as scanner:
        async for result in scanner:
            for company, data in result.manufacturer(filter=_COMPANY_ID):
                print(result.device.addr_hex(),
                      "rssi", result.rssi, "data", data)

asyncio.run(watch())

duration_ms=0 balaye jusqu’à la sortie du gestionnaire de contexte ; active=False garde la propre radio de l’observer silencieuse (aucune requête de réponse de balayage) pour la consommation la plus faible. L’argument filter= de manufacturer() écarte toute annonce qui ne correspond pas à l’identifiant de société, de sorte que la boucle ne se déclenche que pour le trafic du broadcaster.

11.4.6. De la découverte à une connexion

Une fois qu’un central choisit un peripheral avec lequel communiquer, il cesse d’écouter, envoie une demande de connexion (connect request) sur le canal de diffusion que le peripheral a utilisé en dernier, et les deux côtés basculent dans les canaux de données à saut de fréquence de la couche de liaison. Le peripheral cesse généralement de diffuser à ce stade. Ce qui se passe ensuite (paramètres de connexion, découverte GATT, durée de vie de la liaison) est traité dans Connexions.