Emission et lecture de messages hors-connexion entre plusieurs NUCLEO-WB55

Ce tutoriel montre comment configurer plusieurs cartes NUCLEO-WB55 en scanners et en advertisers de sorte à échanger des informations unilatéralement, des advertisers vers les scanners, sans connexions. Il s’agit d’une application immédiate du protocole GAP :

  • Les advertisers émettent des trames qui contiennent des informations, en l’occurrence des données de température et d’humidité simulées ;
  • Les scanners sélectionnent ces trames, et affichent les informations qu’elles contiennent.

La structure des trames d’advertising émises est illustrée par la figure suivante :


BLE GAP advertisement


  • Une première partie contient un identifiant de l’émetteur de la trame
  • Un champ adv_type partage avec tous les objets scanners les informations sur les services et caractéristiques accessibles en cas de connexion avec un central.
  • Un champ rssi (pour “Received Signal Strength Indication”) qui donne l’atténuation de la trame à sa réception sur le scanner. La signification de la mesure, exprimée dans une échelle logarithmique (souvent en dBm) est la suivante : une valeur de 0 dBm correspond à une puissance reçue de 1 mW, −30 dBm correspond à 1 µW. Cela permet de connaitre la qualité de la réception et éventuellement d’ajuster, par rétroaction, le niveau d’émission de l’émetteur distant.
  • Un champ adv_data qui contient les données utilisateur.

Le schéma suivant résume le principe du protocole GAP mis en œuvre : chaque advertiser diffuse des trames que tous les scanners présents (un seul dans cet exemple) pourront recevoir.


BLE GAP use case


Matériel requis

Le cas d’usage que nous allons réaliser comprendra trois cartes NUCLEO-WB55 :

  • Deux cartes NUCLEO-WB55 advertisers (“Adv1” et “Adv2”)
  • Une carte NUCLEO-WB55 scanner (“Scanny”).

Attention, il est possible que vous deviez mettre à jour le firmware BLE HCI de vos cartes NUCLEO-WB55, la procédure est expliquée ici.

Rien ne s’oppose à le généraliser en ajoutant d’autres scanners et d’autres advertisers dans le réseau, il faudra cependant veiller à ce que les advertisers aient tous un nom différent qui commence par “Adv” (“Adv3”, “Adv4”, etc.) de sorte que les scanners puissent les identifier d’après leurs messages.

Les codes MicroPython pour les scanners

Vous pouvez télécharger les scripts MicroPython de ce tutoriel (entre autres) en cliquant ici.

Deux fichiers scripts MicroPython seront nécessaires pour le scanner :

  • Le script permettant de construire les trames d’advertising, intitulé ble_advertising.py. Nous ne détaillerons pas son contenu, vous pouvez le copier directement dans le répertoire PYBFLASH.
  • Le script du programme principal, main.py, qui intègre la classe BLE_Scan_Env pour mettre en œuvre le protocole GAP.

Éditez le script main.py contenu dans le répertoire PYBFLASH du disque USB virtuel associé à la NUCLEO-WB55 qui fera office de scanner et enregistrez y ce code :

# Exemple de mise en œuvre d'une NUCLEO-WB55 en mode scanner BLE à l'écoute de trames provenant 
# d'autres cartes NUCLEO-WB55 en mode advertising.
# Le scanner capture toutes les dix secondes les trames d'advertising dont le nom commence par "Adv".
# Il affiche également les données de température et d'humidité envoyées sous forme de texte 
# dans le nom des trames d'advertising.

import bluetooth # Bibliothèque pour gérer le BLE
from time import sleep_ms # Méthode pour générer des temporisations en millisecondes
from ble_advertising import decode_services, decode_name # Méthodes pour décoder le contenu des trames d'advertising

# Constantes utilisées pour GAP (voir https://docs.MicroPython.org/en/latest/library/ubluetooth.html)
_IRQ_SCAN_RESULT = const(5)  # Le scan signale une trame d'advertising
_IRQ_SCAN_DONE = const(6)  # Le scan a pris fin

# Notifications "advertising" des objets qui ne sont pas connectables
_ADV_SCAN_IND = const(0x02)
_ADV_NONCONN_IND = const(0x03)

# Paramètres pour fixer le rapport cyclique du scan
_SCAN_DURATION_MS = const(1000)
_SCAN_INTERVAL_US = const(10000)
_SCAN_WINDOW_US = const(10000)

# Classe pour créer un scanner BLE environnemental
class BLE_Scan_Env:

	# Initialisations
	def __init__(self, ble):
		self._ble = ble
		self._ble.active(True)
		self._ble.irq(self._irq)
		self._reset()

	# Effacement des données en mémoire cache
	def _reset(self):
		# Noms et adresses mises en mémoire cache après une étape de scan des périphériques
		self._message = set()
		# Callback du scan d'advertising
		self._scan_callback = None

	# Gestion des évènements
	def _irq(self, event, data):
		# Le scan des périphériques a permis d'identifier au moins un advertiser
		if event == _IRQ_SCAN_RESULT:
			# Lecture du contenu de la trame d'advertising
			addr_type, addr, adv_type, rssi, adv_data = data
			
			# Si la trame d'advertising précise que son émetteur n'est pas connectable
			if adv_type in (_ADV_SCAN_IND, _ADV_NONCONN_IND):
				# Le message est contenu dans le champ "name" de la trame d'advertising
				smessage = decode_name(adv_data)
				# Si le message commence par "Adv", enregistre le dans un "set".
				# (pour éviter d'enregistrer plusieurs fois le même message pendant le scan)
				if smessage[0:3] == "Adv":
					self._message.add(smessage)

		# Lorsque le scan a pris fin (après _SCAN_DURATION_MS)
		elif event == _IRQ_SCAN_DONE:
			if self._scan_callback: # Si un callback relatif à cet évènement a été assigné
				if len(self._message) > 0:
					# Si au moins un message a été enregistré pendant le scan, appelle le callback pour afficher
					self._scan_callback(self._message)
					# Désactive le callback
					self._scan_callback = None

	# Procède au scan
	def scan(self, callback = None):
		# Initialise (vide) le set qui va contenir les messages
		self._message = set()
		# Assigne le callback qui sera appelé en fin de scan
		self._scan_callback = callback
		# Scanne pendant _SCAN_DURATION_MS, pendant des durées de _SCAN_WINDOWS_US espacées de _SCAN_INTERVAL_US
		self._ble.gap_scan(_SCAN_DURATION_MS, _SCAN_INTERVAL_US, _SCAN_WINDOW_US)

# Programme principal

print("Hello, je suis Scanny")

# Instanciation du BLE
ble = bluetooth.BLE()
scanner = BLE_Scan_Env(ble)

# Fonction "callback" appelée à la fin du scan
def on_scan(message):
	# Pour chaque message d'advertising enregistré
	for payload in message:
		# On sépare les mesures grâce à l'instruction split
		device, temp, humi = payload.split("|")
		print("Message de " + device + " :")
		print(" - Température : " + temp + "°C")
		print(" - Humidité : " + humi + "%")

while True:
	
	# Lance le scan des trames d'advertising
	scanner.scan(callback=on_scan)

	# Temporisation de dix secondes
	sleep_ms(10000)

Les codes MicroPython pour les advertisers

Vous pouvez télécharger les scripts MicroPython de ce tutoriel (entre autres) en cliquant ici.

Deux fichiers scripts MicroPython seront nécessaires pour chaque advertiser :

  • Le script permettant de construire les trames d’advertising, intitulé ble_advertising.py. Nous ne détaillerons pas son contenu, vous pouvez le copier directement dans le répertoire PYBFLASH.
  • Le script du programme principal, main.py, qui intègre la classe BLE_Adv_Env pour mettre en œuvre le protocole GAP.

Bien que nous utilisions deux advertisers, nous n’allons pas reproduire deux scripts main.py. La seule ligne de code qui change entre le code de Adv1 et celui de Adv2 est justement la chaîne de caractère “mon_nom” dans main.py. Le code ci-dessous est donc celui pour Adv1. Le code pour Adv2 s’obtient simplement en remplaçant la ligne mon_nom = “Adv1” par mon_nom = “Adv2”.

Éditez le script main.py contenu dans le répertoire PYBFLASH du disque USB virtuel associé à la NUCLEO-WB55 qui fera office d’advertiser et enregistrez y ce code :

# Cet exemple montre comment programmer le standard Bluetooth SIG
# pour publier des mesures de température et d'humidité en mode advertising (GAP).
# Les mesures sont simulées par un générateur de nombres aléatoires puis mises à jour  
# et diffusées toutes les cinq secondes.

import bluetooth # Bibliothèque pour la gestion du BLE
import random # Bibliothèque pour la génération de valeurs aléatoires
from time import sleep_ms # Méthode pour la gestion des temporisations en millisecondes
from ble_advertising import adv_payload # Méthode pour construire des trames d'advertising

# Identifiant de l'advertiser (à modifer pour chaque advertiser)
my_name = "Adv1" # ou "Adv2", "Adv3", etc.

# Icône pour une trame GAP environnementale.
# Voir org.bluetooth.characteristic.gap.appearance.xml
_ADV_APPEARANCE_GENERIC_ENVSENSOR = const(5696)

# Classe pour gérer l'advertising de données environnementales
class BLE_Adv_Env:

	# Initialisations
	def __init__(self, ble):
		self._ble = ble
		self._ble.active(True)
		self._connections = set()
		self._handler = None

	# Envoie des trames d'advertising toutes les 5 secondes, précise que l'on ne pourra pas se connecter à l'advertiser
	def advertise(self, interval_us=500000, message = None):
		self._payload = adv_payload(name=message, services=None, appearance=_ADV_APPEARANCE_GENERIC_ENVSENSOR)
		self._ble.gap_advertise(interval_us, adv_data=self._payload, connectable = False)

# Programme principal

print("Hello, je suis " + my_name)

# Initialisations du BLE et du protocole GAP
ble = bluetooth.BLE()
ble_device = BLE_Adv_Env(ble)

while True:

	# Mesures (simulées)
	temperature = random.randint(-20, 90)  # Valeur aléatoire entre -20 et 90 (supposément en °C)
	humidity = random.randint(0, 100) # Valeur aléatoire entre 0 et 100 (supposément en %)
	
	stemperature = str(temperature)
	shumidity = str(humidity)
	
	print(my_name + " publie  :")
	print(" - Température (°C) : " + stemperature)
	print(" - Humidité relative (%) : " + shumidity)
	
	# Publication en BLE de la température et de l'humidité
	ble_device.advertise(message = my_name + "|" + stemperature + "|" + shumidity)

	# Temporisation de cinq secondes
	sleep_ms(5000)

Mise en œuvre

Une fois les scripts copiés sur les 3 cartes, commencez par les démarrer sur les deux advertisers puis (dans cet ordre) sur le scanner ([CTRL]-[D] dans le terminal PuTTY). Si tout se déroule correctement vous devriez observer les échanges ci-dessous (aux valeurs de température et humidité près) sur les trois terminaux, qui montrent de gauche à droite l’affichage de “Adv1”, celui de “Adv2” et celui de “Scanny”.


Sortie GAP BLE


Ces scripts présentent des défauts évidents : Selon la fréquence respective du scan et de l’advertising, le scanner pourra “rater” certains messages si les advertisers sont nombreux sur le réseau ou encore répéter plusieurs fois le message d’un advertiser donné. La correction des ces problèmes est difficile, elle passe probablement par une alternance des rôles des objets, entre scanner et advertiser (pour confirmer la réception d’un message donné ou redemander sa diffusion). Bref, cela dépasse le cadre de notre initiation !

Pour aller plus loin

1. Utiliser des adresses MAC
Notre exemple peut être légèrement amélioré en utilisant comme identificateurs des messages non pas “Adv1” et “Adv2” mais plutôt les adresses MAC des cartes advertiser. Chaque adresse MAC est unique et permettra de ne pas avoir à écrire un script différent par advertiser.
En contrepartie, le filtrage des messages sera plus complexe côté scanner. Il faudra lui donner à l’avance la liste exacte de toutes les adresses MAC des advertisers qu’il devra écouter.

Les scripts pour cette variante sont disponibles dans la zone de téléchargement.
Bien sûr, les adresses MAC des cartes que vous utiliserez comme advertisers diffèreront certainement de celles figurant dans ces scripts ; pensez à les modifier !

2. Faire un petit reset de temps en temps
La première application du protocole GAP qui vient à l’esprit consiste à utiliser les advertisers pour réaliser des mesures de température, dioxyde de carbone, niveau de bruit, etc. dans votre maison. Ceux-ci vont donc jouer le rôle d’objets connectés basse consommation qui vont fonctionner jours et nuits. Si vous réalisez cette expérience, vous constaterez que, immanquablement, au bout de quelques dizaines d’heures de fonctionnement continu, vos advertisers vont se “planter”. Soit ils seront “figés”, soit ils renverront des mesures identiques, signalant que vos capteurs ne répondent plus sur leur bus I2C, etc.
Il se trouve que, tout comme votre ordinateur de temps en temps, votre carte à microcontrôleur a besoin de redémarrer pour passer outre certaines bugs inhérents à MicroPython ou aux drivers utilisés. L”instruction machine.reset() est là pour cela, n’hésitez pas à vous en servir en l’appelant toutes les heures, par exemple, dans votre script !

3. Simuler BLE mesh
Une application amusante inspirée de cet exemple consisterait à permettre aux cartes NUCLEO-WB55 de se comporter alternativement comme des scanners et des advertisers afin de propager des messages de proche en proche. Une carte démarre en mode scanner, capture un message provenant d’un advertiser puis passe en mode advertiser pour le communiquer à d’autres scanners un peu plus loin, etc.