Station environnementale Blue-ST

Ce tutoriel montre comment associer plusieurs composants pour réaliser une petite station environnementale connectée en BLE, en utilisant comme central un smartphone avec l’application ST BLE Sensor.

Le schéma de principe suivant résume le cas d’usage que nous allons réaliser :

STBLESensor use case


Il sagit d’envoyer des informations de température, humdité et pression de la NUCLEO-WB55 à l’application ST BLE Sensor en utilisant un service personnalisé _ST_APP_SERVICE contenant 3 caractéristiques avec l’attribut “Notify”. La quatrième caractéristique, “Interrupteur”, dotée des attributs “Notify” et “Write”, permet de commander la LED de la NUCLEO-WB55 depuis ST BLE Sensor. Pour une explication de principe plus détaillée, sur un cas plus simple, nous vous renvoyons à ce tutoriel.

Matériel requis

Première partie : mesurer des données environnementales

Dans un premier temps, nous rappelons comment accéder aux MEMS de la carte d’extension X-NUCLEO-IKS01A3 pour mesurer la température, l’humidité et la pression (absolue). Cet exemple est repris quasiment à l’identique de ce tutoriel. Pour cette premère étape, seule la NUCLEO-WB55 et la X-NUCLEO-IKS01A3 seront nécessaires.

Le code MicroPython

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

Commencez par copier les bibliothèques hts221.py et lps22.py dans le lecteur PYBFLASH. Éditez le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 : PYBFLASH.

# Lecture et affichage, sur le port série de l'USB USER
# - de la température, 
# - de l'humidité,
# - de la pression.
# Matériel : une carte NUCLEO-W55 et une carte d'extension X-NUCLEO-IKS01A3

from machine import I2C # Pour gérer le bus I2C
import hts221 # Pour gérer le capteur MEMS HTS221
import lps22 # Pour gérer le capteur MEMS LPS22
import time # Pour gérer les temporisations et la mesure du temps écoulé

# On utilise l'I2C n°1 de la carte NUCLEO-WB55 pour communiquer avec les capteurs
i2c = I2C(1)

# Pause d'une seconde pour laisser à l'I2C le temps de s'initialiser
time.sleep_ms(1000)

# Liste des adresses I2C des périphériques présents
print("Adresses I2C utilisées : " + str(i2c.scan()))

# Instances des capteurs
capteur1 = hts221.HTS221(i2c)
capteur2 = lps22.LPS22(i2c)

# Première lecture des capteurs
capteur1.temperature()
capteur1.humidity()
capteur2.pressure()

while True:

	# Lecture des capteurs et arrondis
	temp = round(capteur1.temperature(),1)
	humi = int(capteur1.humidity())
	pres = int(capteur2.pressure())

	# Affichage sur le port série de l'USB USER
	print("Température : " + str(temp) + " °C, Humidité relative : " + str(humi) + " %, Pression : " + str(pres) + " hPa")

	# Temporisation : une mesure par seconde
	time.sleep_ms(1000)

Deuxième partie : ajout d’un afficheur LCD

Nous allons à présent ajouter un afficheur Grove LCD RGB, sur la base de ce tutoriel.

Pour cette étape quatre élements sont requis : la NUCLEO-WB55, la X-NUCLEO-IKS01A3, un LCD RGB Grove et un Grove base shield. Commencez par enficher la X-NUCLEO-IKS01A3 sur les connecteurs Arduino de la NUCLEO-WB55, puis enfichez le Grove base shield sur les connecteurs Arduino de la X-NUCLEO-IKS01A3. Enfin, branchez le LCD RGB Grove sur le connecteur I2C encore accessible du Grove base shield. Assurez-vous que le commutateur de tension du Grove base shield est bien positionné sur 5V pour alimenter le LCD.

Le code MicroPython

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

En plus de hts221.py et lps22.py qui s’y trouvent déjà, copiez les bibliothèques i2c_lcd.py, i2c_lcd_backlight.py et i2c_lcd_screen.py dans le lecteur PYBFLASH. Éditez le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 : PYBFLASH.

# Lecture de deux capteurs du shield IKS01A3 et affichage sur le port série de l'USB USER
# Affichage des valeurs de température, humidité, pression sur le LCD RGB Grove.
# Adaptation de la couleur du rétro-éclairage du LCD selon la température.
# Matériel : 
#  - Une carte Nucleo WB55
#  - Un Grove Base Shield For Arduino
#  - Un shield X-Nucleo IKS01A3
#  - Un LCD RGB Grove (I2C)

from machine import I2C # Pour gérer le bus I2C
import hts221 # Pour gérer le capteur MEMS HTS221
import lps22 # Pour gérer le capteur MEMS LPS22
import time # Pour gérer les temporisations et la mesure du temps écoulé
import i2c_lcd # Pour gérer l'affichage sur le LCD

# On utilise l'I2C n°1 de la carte NUCLEO-WB55 pour communiquer avec les capteurs
i2c = I2C(1)

# Pause d'une seconde pour laisser à l'I2C le temps de s'initialiser
time.sleep_ms(1000)

# Liste des adresses I2C des périphériques présents
print("Adresses I2C utilisées : " + str(i2c.scan()))

# Instances des capteurs
capteur1 = hts221.HTS221(i2c)
capteur2 = lps22.LPS22(i2c)

# Première lecture des capteurs
capteur1.temperature()
capteur1.humidity()
capteur2.pressure()

# Instance de la classe Display
lcd = i2c_lcd.Display(i2c)
lcd.color(255,255,255) #Back light du LCD : blanche
lcd.home() # On replace le curseur en haut à gauche

while True:

	# Lecture des capteurs
	temp = round(capteur1.temperature(),1) # on garde une décimale après la virgule
	humi = int(capteur1.humidity()) # On ne conserve aucune décimale (arrondi à l'entier le plus proche)
	pres = int(capteur2.pressure()) # On ne conserve aucune décimale (arrondi à l'entier le plus proche)

	# Enregistrement des valeurs lues dans des chaînes de caractères 
	stemp = str(temp)
	shumi = str(humi)
	spres = str(pres)

	# Affichage sur le port série de l'USB USER
	print("Température : " + stemp + "°C, Humidité relative : " + shumi + "%, Pression : " + spres + "hPa")

	# Adapte la couleur du rétro-éclairage du LCD selon la température lue
	if temp > 25 :
		lcd.color(255,0,0) #Back light du LCD : rouge
	elif temp > 15 and temp <= 25 :
		lcd.color(255,255,255) #Back light du LCD : blanche
	else:
		lcd.color(0,0,255) #Back light du LCD : bleue

	# Affichage tour à tour sur le LCD de la température, de l'humidité et de la pression

	# Affichage de la température
	lcd.clear() # On efface le LCD
	lcd.move(0,0) # On se place en première colonne (indice 0), première ligne (indice 0)
	lcd.write('Temperature (C)') # On écrit la chaîne "Temperature (C)"
	lcd.move(0,1) # On se place en première colonne (indice 0), deuxième ligne (indice 1)
	lcd.write(stemp) # On écrit la réprésentation affichable de la température
	time.sleep_ms(1000) # Temporisation d'une seconde

	# Affichage de l'humidité
	lcd.clear()
	lcd.move(0,0)
	lcd.write('Humidite (%)')
	lcd.move(0,1)
	lcd.write(shumi)
	time.sleep_ms(1000) # Temporisation d'une seconde

	# Affichage de la pression
	lcd.clear()
	lcd.move(0,0)
	lcd.write('Pression (hPa)')
	lcd.move(0,1)
	lcd.write(spres)
	time.sleep_ms(1000) # Temporisation d'une seconde

Troisième partie : publication des mesures en BLE

Nous allons finalement partager avec la radio BLE les mesures de température, humidité et pression de sorte à pouvoir les afficher dans l’application ST BLE Sensor. Pour les détails sur la mise en oeuvre du protocole Blue-ST, notamment l’ajout des caractéristiques dans le script ble_sensor.py nous vous renvoyons à ce tutoriel.

Le code MicroPython

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

A toute les bibliothèques déjà copiées dans dans le lecteur PYBFLASH il faut à présent ajouter ble_advertising.py et ble_sensor.py.

Analysons tout d’abord, le code de ble_sensor.py qui a été modifié à partir de ce tutoriel pour ajouter les caractéristiques pression et humidité au service environemental :

# Objet du script : Implémentation du protocole Blue-ST pour un périphérique
# Définition d'un service _ST_APP_SERVICE avec quatre caractéristiques :
# 1 - SWITCH : pour éteindre et allumer une LED du périphérique depuis un central
# 2 - PRESSURE : pour envoyer une mesure de pression absolue du périphérique à un central 
# 3 - HUMIDITY : pour envoyer une mesure d'humidité relative du périphérique à un central 
# 4 - TEMPERATURE : pour envoyer une mesure de température du périphérique à un central 

import bluetooth # Bibliothèque bas niveau pour la gestion du BLE
from ble_advertising import advertising_payload # Pour gérer l'advertising GAP
from struct import pack # Pour agréger les octets envoyés par les trames BLE
import pyb # Pour gérer les LED de la NUCLEO-WB55

# Constantes pour construire le service GATT Blue-ST du périphérique

_IRQ_CENTRAL_CONNECT                 = const(1)
_IRQ_CENTRAL_DISCONNECT              = const(2)
_IRQ_GATTS_WRITE                     = const(3)

# Pour les UUID et les codes, on se réfère à la documentation du SDK Blue-ST disponible ici :
# https://www.st.com/resource/en/user_manual/dm00550659-getting-started-with-the-bluest-protocol-and-sdk-stmicroelectronics.pdf.

# 1 - Définition du service personnalisé selon le SDK Blue-ST

# Indique que l'on va communiquer avec une application qui se conforme au protocole Blue-ST
_ST_APP_UUID = bluetooth.UUID('00000000-0001-11E1-AC36-0002A5D5C51B')

# UUID d'une caractéristique de température
_ENV_UUID = (bluetooth.UUID('001C0000-0001-11E1-AC36-0002A5D5C51B'), bluetooth.FLAG_NOTIFY)

# UUID d'une caractéristique d'interrupteur
_SWITCH_UUID = (bluetooth.UUID('20000000-0001-11E1-AC36-0002A5D5C51B'), bluetooth.FLAG_NOTIFY|bluetooth.FLAG_WRITE)

_ST_APP_SERVICE = (_ST_APP_UUID, (_ENV_UUID, _SWITCH_UUID ))

# 2 - Construction de la trame (contenu du message) d'avertising GAP

_PROTOCOL_VERSION   = const(0x01)
_DEVICE_ID          = const(0x80)                           # Carte NUCLEO générique
_FEATURE_MASK       = const(0x201C0000)                     # Switch (2^29), pressure (2^20), humidity (2^19), temperature (2^18)

# Calcul des masques
# Caractéristique SWITCH : 2^29 =      100000000000000000000000000000 (en binaire) = 20000000  (en hexadécimal)
# Caractéristique PRESSURE : 2^20 =    000000000100000000000000000000 (en binaire) = 100000    (en hexadécimal)
# Caractéristique HUMIDITY : 2^19 =    000000000010000000000000000000 (en binaire) = 80000     (en hexadécimal)
# Caractéristique TEMPERATURE : 2^18 = 000000000001000000000000000000 (en binaire) = 40000     (en hexadécimal)
# On fait la somme bit à bit :
# _FEATURE_MASK :                      100000000111000000000000000000 (en binaire) = 201C0000  (en hexadécimal)

_DEVICE_MAC         = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC]  # Adresse matérielle MAC fictive

# Trame d'avertising : concaténation des informations avec la fonction Micropython "pack" 
# La chaîne '>BBI6B' désigne le format des arguments, voir la documentation de pack ici : https://docs.python.org/3/library/struct.html
_MANUFACTURER = pack('>BBI6B', _PROTOCOL_VERSION, _DEVICE_ID, _FEATURE_MASK, *_DEVICE_MAC)

# Initialisation des LED
led_bleue = pyb.LED(3)
led_rouge = pyb.LED(1)

class BLESensor:

	# Initialisation, démarrage de GAP et publication radio des trames d'advertising
	def __init__(self, ble, name='WB55-MPY'):
		self._ble = ble
		self._ble.active(True)
		self._ble.irq(self._irq)
		((self._env_handle,self._switch_handle),) = self._ble.gatts_register_services((_ST_APP_SERVICE, ))
		self._connections = set()
		self._payload = advertising_payload(name=name, manufacturer=_MANUFACTURER)
		self._advertise()
		self._handler = None

	# Gestion des évènements BLE...
	def _irq(self, event, data):
		
		# Si un central a envoyé une demande de connexion
		if event == _IRQ_CENTRAL_CONNECT:
			conn_handle, _, _, = data
			self._connections.add(conn_handle)
			print("Connecté")
			led_bleue.on()
			
		# Si le central a envoyé une demande de déconnexion
		elif event == _IRQ_CENTRAL_DISCONNECT:
			conn_handle, _, _, = data
			self._connections.remove(conn_handle)
			# Relance l'advertising pour permettre de nouvelles connexions
			self._advertise()
			print("Déconnecté")
		
		# Si une écriture est détectée dans la caractéristique SWITCH (interrupteur) de la LED
		elif event == _IRQ_GATTS_WRITE:
			conn_handle, value_handle, = data
			if conn_handle in self._connections and value_handle == self._switch_handle:
				# Lecture de la valeur de la caractéristique
				data_received = self._ble.gatts_read(self._switch_handle)
				self._ble.gatts_write(self._switch_handle, pack('<HB', 1000, data_received[0]))
				self._ble.gatts_notify(conn_handle, self._switch_handle)
				# Selon la valeur écrite, on allume ou on éteint la LED rouge
				if data_received[0] == 1:
					led_rouge.on()
				else:
					led_rouge.off()

	# On écrit dans la caractéristique environnementale
	# Points d'ATTENTION :
	# - Les valeurs doivent être transmises dans l'ordre des ID des caractéristiques (pression : 20 eme bit, humidité : 19 eme bit, température 18 eme bit)
	# - Attention à la chaîne de formattage de la fonction Python "pack", égale ici à '<HiHh', voir la documentation de "pack" en Python.
	def set_data_env(self, timestamp, pressure, humidity, temperature, notify):
		self._ble.gatts_write(self._env_handle, pack('<HiHh', timestamp, pressure, humidity, temperature))
		if notify:
			for conn_handle in self._connections:
				# Signale au central que les valeurs des caractéristiques viennent d'être rafraichies et, donc, peuvent être lues
				self._ble.gatts_notify(conn_handle, self._env_handle)

	# Pour démarrer l'advertising avec une période de 5 secondes, précise qu'un central pourra se connecter au périphérique
	def _advertise(self, interval_us=500000):
		self._ble.gap_advertise(interval_us, adv_data=self._payload, connectable=True)
		led_bleue.off()

Éditez à présent le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 : PYBFLASH.

# Lecture de deux capteurs du shield IKS01A3 et affichage sur le port série de l'USB USER
# Affichage des valeurs de température, humidité, pression sur le LCD RGB Grove
# Adaptation de la couleur du rétro-éclairage du LCD selon la température
# "Casting" RF des mesures avec la radio BLE du STM32WB55 
# Correction de la pression en la rapportant au niveau de la mer
# Les données sont traitées par l'application smartphone ST BLE Sensor
# Matériel : 
#  - Une carte NUCLEO-WB55
#  - Un Grove Base Shield For Arduino
#  - Un shield X-NUCLEO IKS01A3
#  - Un LCD RGB Grove (I2C)

# Pression corrigée au niveau de la mer (nécessite la valeur de l'altitude locale)
alti = 485
def PressionNivMer(pression, altitude):
	return pression * pow(1.0 - (altitude * 2.255808707E-5), -5.255)

from machine import I2C

import hts221
import lps22
import time # Pour gérér le temps et les temporisations
import i2c_lcd
import ble_sensor # Pour implémenter le protocole GATT pour Blue-ST
import bluetooth # Classes "primitives du BLE" 
# (voir https://docs.micropython.org/en/latest/library/ubluetooth.html)

# On utilise l'I2C n°1 de la carte NUCLEO-WB55 pour communiquer avec les capteurs
i2c = I2C(1)

# Pause d'une seconde pour laisser à l'I2C le temps de s'initialiser
time.sleep_ms(1000)

# Liste des adresses I2C des périphériques présents
print("Adresses I2C utilisées : " + str(i2c.scan()))

# Instances des capteurs
sensor1 = hts221.HTS221(i2c)
sensor2 = lps22.LPS22(i2c)

#Instance de la classe Display
lcd = i2c_lcd.Display(i2c)
lcd.color(255,255,255) #Back light du LCD : blanche

# Repositionne le curseur de l'afficheur LCD en haut à gauche
lcd.home()

# Instance de la classe BLE
ble = bluetooth.BLE()
ble_device = ble_sensor.BLESensor(ble)

while True:

	# Lecture des capteurs
	temp = sensor1.temperature()
	humi = sensor1.humidity()
	pres = PressionNivMer(sensor2.pressure(), alti)

	# Conversion en texte des valeurs renvoyées par les capteurs 
	stemp = str(round(temp,1))
	shumi = str(int(humi))
	spres = str(int(pres))

	# Affichage sur le port série de l'USB USER
	print("Température : " + stemp + " °C, Humidité relative : " + shumi + " %, Pression : " + spres + " hPa")

	# Préparation des données pour envoi en BLE.
	# Le protocole Blue-ST code les températures, pressions et humidités sous forme de nombres entiers.
	# Donc on multiplie les différentes mesures par 10 ou par 100 pour conserver des décimales avant
	# d'arrondir à l'entier le plus proche.
	# Par exemple si temp = 18.45°C => on envoie ble_temp = 184. 
	ble_pres = int(pres*100)
	ble_humi = int(humi*10)
	ble_temp = int(temp*10)
	timestamp = time.time()

	# Envoie des données en BLE 
	ble_device.set_data_env(timestamp, ble_pres, ble_humi, ble_temp, True) 

	# Adapte la couleur du rétro-éclairage du LCD selon la température lue
	if temp > 25 :
		lcd.color(255,0,0) #Back light du LCD : rouge
	elif temp > 15 and temp <= 25 :
		lcd.color(255,255,255) #Back light du LCD : blanche
	else:
		lcd.color(0,0,255) #Back light du LCD : bleue

	# Ecriture sur le LCD
	lcd.clear()
	lcd.move(0,0)
	lcd.write('Temperature (C)')
	lcd.move(0,1)
	lcd.write(stemp)
	time.sleep_ms(1000)

	lcd.clear()
	lcd.move(0,0)
	lcd.write('Humidite (%)')
	lcd.move(0,1)
	lcd.write(shumi)
	time.sleep_ms(1000)

	lcd.clear()
	lcd.move(0,0)
	lcd.write('Pression (hPa)')
	lcd.move(0,1)
	lcd.write(spres)
	time.sleep_ms(1000)

Vous pouvez lancer le script avec Ctrl + D sur le terminal PuTTY et observer les messages qu’il renvoie :


Sortie env BLE


Connexion et enregistrement de données avec ST BLE Sensor

Vous pouvez donc vous connecter à la station environnementale à l’aide de l’application ST BLE Sensor et lire les valeurs mesurées. La figure qui suit résume comment faire, écran après écran :


Sortie env BLE


Il est également possible d’enregistrer les données mesurées dans un fichier au format CSV et de se l’envoyer par mail. La figure qui suit rappelle la procédure à suivre en trois étapes :


Sortie env BLE


  • Etape 1 : Appuyez en haut à droite sur la disquette, l’enregistrement démarre.
  • Etape 2 : Attendez quelques minutes pour enregistrer assez de valeurs.
  • Etape 3 : Appuyer en haut à droite sur la croix. L’enregistrement s’arrête et un message vous demande si vous souhaitez envoyer l’enregistrement par mail. Appuyez sur OK, précisez votre adresse mail. Vous devriez reçevoir sous peu un e-mail intitulé [ST BLE Sensor] BlueSTSDK Log Data contenant les données de température, humidité et pression enregistrées dans trois fichiers .csv attachés.

Par exemple, pour un e-mail envoyé le 3 mai 2021 à 23h 00min 10s, le fichier contenant les données de températures est nommé comme suit “20210503_230010_Temperature.csv” et son contenu, édité dans un tableur, est stucturé comme suit :


Logs env BLE


Les trois premières lignes du fichier donnent les informations suivantes :

  • La première précise l’heure à laquelle l’enregistrement à commencé (“03/05/2021 23:00” dans notre cas).
  • La deuxième précise le type des données enregistrées (“Temperature(°C)” dans notre cas).
  • La troisième précise le nom du périphérique BLE (“Node”) d’où proviennent les données (“WB55-MPY @4E2516” dans notre cas).

La quatrième ligne est constituée par les en-têtes de colonnes des enregistrements, et toutes les lignes suivantes contiennent les enregistrements. Il sont donc structurés en six colonnes :

  • La colonne Date donne l’heure de l’enregistrement.
  • La colonne HostTimestamp (ms) donne le temps écoulé en millisecondes mesuré par ST BLE Sensor (le central) depuis le début de l’enregistrement.
  • La colonne NodeName rappelle l’identifiant du périphérique.
  • La colonne NodeTimestamp rappelle l’horodatage envoyé par le périphérique. Il s’agit du contenu de la variable timestamp dans le script main.py.
  • La colonne RawData contient la valeur de l’enregistrement codée en hexadécimal. Attention, il faut permuter les deux octets avant de procéder à la conversion hexadécimal vers décimal. Par exemple, à la ligne 5 on lis “CE00” composé des octets “CE” et “00”. On les permute pour obtenir le nombre hexadécimal codant la température, qui est en fait “00CE”. Converti en base décimale il correspond à “206”. Il faut encore diviser cette valeur par dix pour retrouver la température (20.6 °C).
  • La sixième et dernière colonne contient la valeur décodée de la caractéristique concernée (la température exprimée en degrés Celsius dans notre cas).