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’occurence 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 un é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 microwatt. 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 oeuvre : chaque advertiser envoie des trames qui seront “écoutées” par le scanner.


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”).

Rien ne s’oppose à le généraliser en ajoutant d’autres scanners et advertisers dans le réseau, il faudra juste veiller à ce que les advertisers supplémentaires y aient tous un nom unique (“Adv3”, “Adv4”, etc.) de sorte à pouvoir discriminer leurs messages.

Les codes MicroPython pour le scanner

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

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 oeuvre 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.

# Exemple de mise en oeuvre d'une NUCLEO-WB55 en mode scanner BLE à l'écoute de trames provenant 
# d'autres (deux dans notre exemple) cartes NUCLEO-WB55 en mode advertising.
# Le scanner capture 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 GAP
_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:
					# 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
		objet, temp, humi = payload.split("|")
		print("Message de " + objet + " :")
		print(" - Température : " + temp + "°C")
		print(" - Humidité : " + humi + "%")

while True:
	# Lance le scan des trames d'advertising
	scanner.scan(callback=on_scan)
	# Temporisation de 10 secondes
	sleep_ms(10000)

Les codes MicroPython pour les advertisers

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

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 oeuvre 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.

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

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 advertising_payload # Méthode pour construire des trames d'advertising

# Identifiant de l'advertiser
mon_nom = "Adv1"

# 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 = advertising_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 " + mon_nom)

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

while True:

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

	# Temporisation de cinq secondes
	sleep_ms(5000)

Mise en oeuvre

Une fois les scripts copiés sur les 3 cartes, commencez par démarrer les scripts des deux advertisers (CTRL+D dans le terminal PuTTY), et ensuite celui du scanner (idem). 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


Pour aller plus loin

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 présentés ci-après sont disponibles dans la zone de téléchargement.

Pour aller encore plus loin

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.