Publication LoRaWAN de mesures environnementales avec une carte NUCLEO-WL55JC1

Ce tutoriel explique comment mettre en œuvre une publication LoRaWAN sur The Things Network (TTN) avec une intégration TagoIO en utilisant l’environnement STM32duino pour une carte NUCLEO-WL55JC1 côté objet.
Pour en apprendre un peu plus sur LoRa et LoRaWAN, vous pouvez consulter cette page et pour vraiment tout comprendre lisez ce document de Sylvain Montagny (Université de Savoie - Mont Blanc).

Matériel requis et montage

Notre système connecté sera constitué des composants suivants :

  1. Pour la communication LoRa / LoRaWAN côté objet, nous avons choisi une carte NUCLEO-WL55JC1 de STMicroelectronics, utilisant un système sur puce (SoC) STM32WLE5JC.
  2. Une carte d’extension de base Grove avec son commutateur d’alimentation en position 3,3V.
  3. Un module Grove BME280.
  4. Pour la passerelle LoRaWAN, nous avons choisi la The Things Indoor Gateway (TTIG).
Une carte NUCLEO-WL55JC1
(face avant, sans blindage et antenne)
Une passerelle LoRa-WiFi TTIG
Carte NUCLEO-WL55JC1 Passerelle TTIG
Crédit image : STMicroelectronics Crédit image : The Things Network

Placez la carte d’extension Grove sur la carte NUCLEO et connectez-y le module BME280 (sur une fiche grove I2C).

IMPORTANT

  • Ce tutoriel fait appel à la bibliothèque STM32duinoLoRaWAN qui ne fonctionne qu’avec la carte NUCLEO-WL55JC1, respectant la réglementation radiofréquences de l’Union Européenne. Il existe une autre version, la NUCLEO-WL55JC2, conçue pour d’autres zones géographiques (US notamment), mais celle-ci n’est pas supportée par la bibliothèque STM32duinoLoRaWAN.

  • Le sketch fourni dans cet exemple n’a été testé que pour des cartes NUCLEO-WL55JC1 dans leur révision “MBxxx-HIGHBAND-E02”xxxx est un nombre variable. Cette information est indiquée par une étiquette collée en face arrière des cartes. Faute de tests étendus, nous ne pouvons pas garantir que le sketch fonctionnera correctement sur une autre révision de la carte que celle-ci.

Première étape : Obtention d’un DevEUI pour la carte NUCLEO-WL55JC1

Le DevEUI est un identifiant unique encodé sur 64 bits qui servira pour l’enregistrement de notre NUCLEO-WL55JC1 sur le réseau LoRaWAN de TTN. Celui ci peut-être choisi arbitrairement, ce qui compte c’est qu’il soit suffisamment “compliqué” pour avoir une très forte probabilité d’être unique.

Pour le cas où vous manqueriez d’inspiration, STMicroelectronics fournit un DevEUI pour chaque carte NUCLEO-WL55JC1. Celui-ci est inscrit sur une étiquette collée sur le MCU de son interface ST-LINK. A titre d’exemple, le DevEUI de la carte prise en photo ci-dessus est : 00:80:E1:15:00:00:4B:59.

Deuxième étape :
- Création d’un réseau LoRaWAN privé avec la passerelle TTIG pour une carte NUCLEO-WL55JC1
- Création d’un lien entre TTN et l’intégration TagoIO

Pour la configuration de la passerelle et de tous les services permettant de recueillir les mesures d’une carte NUCLEO-WL55JC1 d’abord sur TTN puis sur TagoIO, nous vous renvoyons à ce tutoriel. Pour aller jusqu’au bout, vous aurez besoin de la clef DevEui obtenue ci-avant, à la première étape.

Cependant, ce tutoriel ayant été écrit pour des modules LoRa-E5 et non pour des cartes NUCLEO-WL55JC1, nous recopions ici la configuration de TTN pour notre “end device” :


Réseau LoRaWAN


Pour TagoIO, pas de changement particulier, nous avons simplement créé un nouveau “device” puis son “Dashboard” associé.

Troisième étape : Connexion au réseau LoRaWAN et publication des mesures sur TTN et TagoIO

Nous allons à présent partager le sketch Arduino qui se connectera à TTN et postera les mesures de température, d’humidité et de pression du capteur BME280 dans un format hexadécimal compact approprié pour les trames LoRaWAN. Ceci signifie que nous devrons également configurer sur notre compte TagoIO un script en langage NodeJS pour extraire ces mesures des trames LoRaWAN et les “décoder” avant de les afficher dans un dashboard.

Bibliothèque(s) requise(s)

Ce sketch utilise trois bibliothèques :

  1. La bibliothèque STM32duinoLoRaWAN, dans sa révision 0.2.0 (ou ultérieure).

  2. La bibliothèque GyverBME280 permettant de réaliser des mesures avec le module Grove BME280 en exploitant son mode basse consommation.

  3. La bibliothèque STM32duino Low Power pour mettre en sommeil le MCU STM32L476RG de la carte NUCLEO entre deux publications LoRaWAN.

Le sketch Arduino

Le sketch pour cet exemple (et tous les autres) peut être téléchargé en cliquant ici. Vous le trouverez dans le dossier \LoRaWAN - Publication NUCLEO-WL55JC1\NUCLEO-WL55JC1_Publish.

Lancez l’IDE Arduino, ouvrez un nouveau sketch vide et copiez-y le code qui suit :

//*******************************************************************************************
// Connexion d'une NUCLEO-WL55JC1 à un réseau LoRaWAN privé sur TTN, préalablement configuré.
// Publication de données de température, humidité et pression sur TTN dans un format
// hexadécimal qui devra ensuite être "décodé" par un parser de payloads sur TagoIO.
// La bibliothèque pour la publication LoRaWAN utilisée est STM32duinoLoRaWAN rev 0.2.0.
// Voir la page README sur https://github.com/stm32duino/STM32duinoBLE.
//*******************************************************************************************

// Version du sketch
#define SKETCH_REV "0.02"

// Bibliothèque pour la lecture du capteur BME280, choisie pour les commandes permettant
// d'utiliser celui-ci en mode basse consommation.
#include <GyverBME280.h>

// Instance du capteur BME280
GyverBME280 bme;

// Variables globales pour les mesures
float temp;  // Température (Celsius)
float pres;  // Pression (hPa)
float humi;  // Humidité relative (%)

// Offset de température du BME280
// (déterminé expérimentalement et différent pour chaque capteur)
#define TEMP_OFFSET (-2.1)

// Doit-on compiler les instructions pour utiliser le mode basse consommation ?
// ("0" pour "non", "1" pour "oui")
#define SET_LOW_POWER 1

#if SET_LOW_POWER == 1

// Bibliothèque basse consommation pour le SoC STM32WL55
#include "STM32LowPower.h"

// Bibliothèque RTC pour le SoC STM32WL55
#include <STM32RTC.h>

// Instace de la RTC
STM32RTC& rtc = STM32RTC::getInstance();

#endif

// Compteur de la RTC, déclaré en "volatile" car incrémenté par une interruption
volatile uint8_t rtc_alarm_counter = 0;

// Débit, en bauds, du port série associé au ST-LINK
#define STL_BDRATE 115200

// Temps de sommeil du SoC STM32WL55JC1 entre deux posts
#define SLEEP_DURATION_MS 600000  // Exprimée en millisecondes, soit 10 minutes

// Bibliothèque LoRaWAN
#include <STM32LoRaWAN.h>

#define PAYLOAD_SIZE 5              // Nombre d'octets dans les Payloads LoRaWAN
char payloadUp[PAYLOAD_SIZE];       // Réservation mémoire pour la payload "Uplink"
char sizePayloadUp = PAYLOAD_SIZE;  // Taille de la payload "Uplink"

// Instanciation du modem LoRaWAN du SoC STM32WL55JC1
STM32LoRaWAN lora_wl55;

void setup() {

  // Démarre le port série du ST-LINK
  Serial.begin(STL_BDRATE);
  Serial.print("\r\nRévision du sketch : " );
  Serial.println(SKETCH_REV);

  // Initialisation du modem LoRaWAN du SoC STM32WL55JC1
  lora_wl55.begin(EU868);

  Serial.println("Procédure de join OTAA en cours ...");

  // Réalisation du join "Over The Air Activation" (OTAA)
  // Paramètres :
  // - AppEui arbitraire, ce doit être le même ici et dans la description de l'objet dans la console de TTN
  // - AppKey fourni par TTN lors de la création de l'objet
  // - DevEui arbitraire, mais ce doit être un identifiant unique et ce doit être le même ici et dans la description de l'objet dans la console de TTN
  bool connected = lora_wl55.joinOTAA(/* AppEui */ "0101010101010101", /* AppKey */ "780C7F43A005035B9D4087B26167FB18", /* DevEui */ "0080E11505310656");

  if (connected) {
    Serial.println("Succès du join !");
  } else {
    Serial.println("Echec du join !");
    while (true) {};  //  Suspend l'exécution
  }

  // Configure la LED utilisateur de la carte NUCLEO
  pinMode(LED_BUILTIN, OUTPUT);

#if SET_LOW_POWER == 1

  // Active et configure le mode basse consommation du MCU STM32
  // La ligne rtc.begin(true) est nécessaire pour contourner un bug :
  // lora_wl55.begin() plante la RTC", il faut forcer son démarrage.
  
  Serial.println("Mode basse consommation actif");
  delay(1000);  // Temporisation d'une seconde

  rtc.begin(true);
  LowPower.begin();
  LowPower.enableWakeupFrom(&rtc, alarmMatch);

#else

  Serial.println("Mode basse consommation inactif");

#endif
}

//*******************************************************************************************
// Boucle du programme principal
//*******************************************************************************************
void loop() {

  // Force un reset du MCU après 4 posts pour contrer une éventuelle fragmentation mémoire
  if (rtc_alarm_counter == 4) {
    rtc_alarm_counter = 0;
    Serial.println("\r\nReset système !");
    delay(1000);
    NVIC_SystemReset();
  }

  // Allume la LED
  digitalWrite(LED_BUILTIN, HIGH);

  Serial.println("\r\nMesure en cours ...");

  // Mesure de la température, de la pression et de l'humidité
  measure();

  // Construction de la payload LoRaWAN à partir des mesures
  build_LoRaWAN_payload();

  // Emission de la trame LoRaWAN
  send_LoRaWan_Frame();

  // Vérifie si un Downlink est planifié sur RX1-RX2 (device de classe A)
  // Appelle processDownlink() pour traiter les octets reçus le cas échéant.
  processDownlink();

#if SET_LOW_POWER == 1
  Serial.printf("\r\nMise en veille pour %u minutes\r\n", SLEEP_DURATION_MS / 60000);
#else
  Serial.printf("\r\nTemporisation pour %u minutes\r\n", SLEEP_DURATION_MS / 60000);
#endif

  // Eteint la LED
  digitalWrite(LED_BUILTIN, LOW);

  Serial.flush();  // Vide le buffer du port série avant la mise en sommeil
  delay(1000);     // Temporisation blocante pendant 1 seconde

#if SET_LOW_POWER == 1
  // Place le SoC STM32WL55JC1 en mode sommeil pendant SLEEP_DURATION_MS millisecondes
  LowPower.deepSleep(SLEEP_DURATION_MS);
#else
  // Suspend l'exécution avec une temporisation exécutée par le MCU
  delay(SLEEP_DURATION_MS);
#endif

}

//*******************************************************************************************
// Mesure de la température, de la pression et de l'humidité
//*******************************************************************************************
void measure(void) {

  // Initialisation du BME280
  // On utilise le capteur en mode : "lis et rendors-toi !"
  bme.setMode(FORCED_MODE);
  bme.begin();

  // Lance une lecture du capteur, il se remet en veille après
  bme.oneMeasurement();

  // Polling en attendant que la lecture soit terminée
  while (bme.isMeasuring()) {};

  // Réalisation des mesures avec le capteur
  temp = bme.readTemperature() + TEMP_OFFSET;
  pres = bme.readPressure() * 0.01;
  humi = bme.readHumidity();

  // Affichage des mesures
  // Attention, pourt qu'elles s'affichent correctement, activer
  // "Newlib Nano + Float printf" dans le menu "Outils/C Runtime Library".
  Serial.printf("  - Température : %.1f °C\r\n", temp);
  Serial.printf("  - Pression : %1.f hPa\r\n", pres);
  Serial.printf("  - Humidité relative : %.1f %%\r\n", humi);
  Serial.println();
}

//*******************************************************************************************
// Construction de la payload LoRaWAN
//*******************************************************************************************
void build_LoRaWAN_payload(void) {

  // On convertit les mesures de température, pression et humidité en entiers pour décodage
  // ultérieur avec TagoIO.
  int16_t temp_ = round(10 * temp);
  int16_t pres_ = round(10 * pres);
  int16_t humi_ = round(2 * humi);

  // Construction de la payload LoRaWAN, on agrège les données au format hexadécimal.
  // Voir ce tutoriel :
  // https://www.carnetdumaker.net/articles/quelques-fonctions-bien-pratiques-du-framework-arduino/
  // pour quelques astuces sur les fonctions lowByte(x) et highByte(x) du framework Arduino qui
  // permettent de simplifier le code ci-dessous.

  // Température, donnée codée sur 16 bits
  payloadUp[0] = (temp_ >> 8) & 0xFF;  // Extraction de l'octet de poids faible
  payloadUp[1] = temp_ & 0xFF;         // Extraction de l'octet de poids fort

  // Pression, donnée codée sur 16 bits
  payloadUp[2] = (pres_ >> 8) & 0xFF;  // Extraction de l'octet de poids faible
  payloadUp[3] = pres_ & 0xFF;         // Extraction de l'octet de poids fort

  // Humidité, donnée codée sur un seul octet
  payloadUp[4] = humi_;
}

//*******************************************************************************************
// Envoie une trame LoRaWAN
//*******************************************************************************************
void send_LoRaWan_Frame(void) {

  // Configuration du modem LoRaWAN
  lora_wl55.setPort(10);
  lora_wl55.beginPacket();

  // Envoie "sizePayloadUp" octets depuis le tableau "payloadUp"
  lora_wl55.write(payloadUp, strlen(payloadUp));

  if (lora_wl55.endPacket() == (int)strlen(payloadUp)) {
    Serial.println("\r\nTrame LoRaWAN envoyée !");
  } else {
    Serial.println("\r\nEchec de l'envoi de la trame LoRaWAN !");
  }
}

//*******************************************************************************************
// Callback (ISR) de l'interruption périodique de la RTC, pour réveiller le SoC STM32WL55JC1
//*******************************************************************************************
void alarmMatch(void* data) {
  UNUSED(data);
  rtc_alarm_counter++;
}

//*******************************************************************************************
// Fonction de traitement d'un éventuel downlink
//*******************************************************************************************
void processDownlink() {
  if (lora_wl55.available()) {
    Serial.print("\r\nDonnées reçues du serveur TTN sur le port ");
    Serial.print(lora_wl55.getDownlinkPort());
    Serial.print(" : ");
    // Lecture des octets renvoyés par TTN, un par un
    // On pourrait bien évidemment déclarer un tableau pour les enregistrer plutôt que les afficher.
    while (lora_wl55.available()) {
      uint8_t b = lora_wl55.read();
      Serial.print(" ");
      Serial.print(b >> 4, HEX);
      Serial.print(b & 0xF, HEX);
    }
    Serial.println();
  }
}

Nous rappelons que nous utilisons ici la “configuration OTAA” pour laquelle :

  • La valeur de devEUI a été obtenue à l’aide de la commande AT+ID en première partie de ce tutoriel. Elle nous a ensuite servi pour obtenir appKey via TTN en deuxième partie de ce tutoriel.
  • La valeur de appEui doit rester à “01 01 01 01 01 01 01 01”. En fait elle est arbitraire, mais elle doit être identique à celle renseignée dans la définition de l’objet, dans l’interface de TTN.
  • La valeur de appKey a été obtenue au moment de la création d’une application via l’interface de TTN.

Mise en œuvre du sketch Arduino

Après avoir vérifié que l’IDE Arduino est correctement configurée, notamment qu’elle est connectée au port COM attribué au ST-LINK de votre carte (sous Windows), cliquez sur “Téléverser”.

Si tout est correct, vous devriez lire dans le terminal série de l’IDE Arduino la publication régulière, toutes les dix minutes, de trames LoRaWAN à l’attention des serveurs de TTN :

Procédure de join OTAA en cours ...
Setting TX Config: modem=MODEM_LORA, power=13, fdev=0, bandwidth=0, datarate=8, coderate=1 preambleLen=8, fixLen=0, crcOn=1, freqHopOn=0, hopPeriod=0, iqInverted=0, timeout=4000
TX on freq 868100000 Hz at DR 4
TX: 00 01 01 01 01 01 01 01 01 56 06 31 05 15 e1 80 00 b0 58 a2 ad 7a 23
MAC txDone
RX_1 on freq 868100000 Hz at DR 4
MAC rxDone
RX: 2039300d7babb15f6c69e9e8cc60410b255540cfd40944838151445e72b822b58b
MlmeConfirm: req=MLME_JOIN, status=LORAMAC_EVENT_INFO_STATUS_OK, airtime=114, margin=0, gateways=0
Succès du join !

Mesure en cours ...
  - Température : 28.1 °C
  - Pression : 962 hPa
  - Humidité relative : 43.7 %

Setting TX Config: modem=MODEM_LORA, power=13, fdev=0, bandwidth=0, datarate=8, coderate=1 preambleLen=8, fixLen=0, crcOn=1, freqHopOn=0, hopPeriod=0, iqInverted=0, timeout=4000
TX on freq 868100000 Hz at DR 4
TX: 40 e7 8f 0b 26 80 01 00 0a 9a b4 28 05 73 63 df 86 1d
MAC txDone
RX_1 on freq 868100000 Hz at DR 4
IRQ_RX_TX_TIMEOUT
MAC rxTimeOut
RX_2 on freq 869525000 Hz at DR 3
MAC rxDone
RX: 60e78f0b268601000350ff000106019018705885
McpsConfirm: req=MCPS_UNCONFIRMED, status=LORAMAC_EVENT_INFO_STATUS_OK, datarate=4, power=0, ack=0, retries=0, airtime=93, upcnt=1, channel=0
McpsIndication: ind=MCPS_UNCONFIRMED, status=LORAMAC_EVENT_INFO_STATUS_OK, multicast=0, port=1, datarate=3, pending=0, size=1, rxdata=1, ack=0, dncnt=1, devaddr=260b8fe7, rssi=-28, snr=11, slot=1

Trame LoRaWAN envoyée !

Mise en veille pour 6000 secondes

IMPORTANT

Bien qu’il ne comportait pas d’erreurs, notre sketch, dans un premier temps, se compilait correctement mais ne s’exécutait manifestement pas. Nous sommes parvenus à résoudre ce problème en modifiant plusieurs paramètres, sans parvenir à identifier celui qui était déterminant :

  1. Nous avons mis à jour le firmware du ST-LINK de la carte NUCLEO-WL55JC1 à l’aide de STM32CubeProgrammer (voir ce tutoriel ).
  2. Dans le menu Outils/Upload method de l’IDE Arduino, nous avons sélectionné STM32CubeProgammer (SWD).
  3. Après l’upload du sketch, nous avons débranché - rebranché la carte de son câble USB pour forcer un “reset matériel”.

IL est tout à fait possible que ces manipulations nn’étaient pas vraiment utiles et que la cause de notre “bug” se trouvait ailleurs (sur le PC utilisé, par exemple), mais nous partageons ces informations au cas où vous rencontreriez le même problème de programmation de la NUCLEO-WL55JC1.

Le sketch ci-avant contient aussi du code pour la réception de données descendantes ou downlink, depuis le serveur TTN sur la NUCLEO-WL55JC1. Le protocole LoRaWAN permet en effet une communication bidirectionnelle objet <-> serveur. Nous n’utiliserons pas cette possibilité, mais nous pouvons néanmoins vérifier qu’elle fonctionne.

Pour envoyer un message depuis TTN à ka NUCLEO-WL55JC1

  • Rendez-vous sur la page de votre device ;
  • Puis, dans le menu vertical à gauche, choisissez End devices ;
  • Cliquez sur le device concerné ;
  • Sélectionnez le sous menu Messaging ;
  • Sélectionnez le sous-sous-menu Downlink;
  • Finalement, entrez une valeur hexadécimale dans la boite Payload (deux caractères, simplement “0A” dans notre cas) et cliquez sur le bouton Schedule downlink en bas. Un message Downlink scheduled, en bas à droite, confirme que l’opération est planifiée. Pour notre device, voici l’aspect de la page de downlink sur le site de TTN à ce moment :


Downlink sur TTN


  • Il vous faut attendre le prochain uplink de la NUCLEO-WL55JC1 vers le serveur TTN. Une fois celui-ci réalisé, le message du serveur est bien reçu par le module, comme le confirment les logs sur le terminal série :

... (lignes supprimées par soucis de concision)

Trame LoRaWAN envoyée !

Données reçues du serveur TTN sur le port 1 :  0A 0B 0C 0D 0E

Mise en veille pour 6000 secondes

Complément 2 : Quel gain avec le mode basse consommation ?

Vous constaterez que notre sketch contient une directive de préprocesseur au début, #define SET_LOW_POWER 1, qui permet de lui préciser, au moment de la compilation, si on exploite le mode basse consommation du MCU (instruction LowPower.deepSleep(SLEEP_DURATION_MS)) pour sa mise en veille de 10 minutes, ou bien si on fait plutôt une “pause” (instruction delay(SLEEP_DURATION_MS)) pendant laquelle le MCU restera actif.

La première solution est bien plus économe en énergie mais, étonnamment, le gain d’autonomie avec le mode basse consommation n’est pas sensible avec notre système. Alimenté par une batterie USB rechargeable de capacité 1200 mA, il n’a pas dépassé les 9h de fonctionnement que le mode “low power” soit utilisé ou pas.
Ce résultat décevant et surprenant de prime abord, s’explique si on mesure les courants consommés (avec, par exemple, un multimètre USB JT-AT34) :


Version de la temporisation Phase de l’exécution Courant moyen consommé (mA)
Toutes Join avec TTN et envoi trame LoRaWAN > 70
delay(SLEEP_DURATION_MS) Temporisation entre deux mesures 64
deepSleep(SLEEP_DURATION_MS) Temporisation entre deux mesures 57


Le mode Deep Sleep permet donc de réduire le courant requis de 7 mA. Si on se réfère à cette présentation du STM32WL55JC et à la documentation de la bibliothèque STM32duino Low Power on comprend que ce mode ramène la consommation du MCU à quelques microampères alors qu’en fonctionnant pleinement il consomme autour de 6 mA.
Le compte y est, les 7 mA que nous économisons avec deepSleep, en éteignant le MCU, sont cohérents (aux incertitudes mesures près) avec sa consommation théorique de 6 mA !

Mais alors d’où les 50 mA de consommation “résiduelle” proviennent-ils ?
De trois sources principalement :

  • D’abord du régulateur de tension qui alimente la carte via l’USB (5 V) ;
  • Ensuite du ST-Link ;
  • Enfin, des différentes diodes qui restent allumées, notamment celle du ST-Link (encore !) et celle du shield Grove. A titre indicatif, simplement en replaçant le shield Grove par un câble dupont-grove directement connecté à la carte NUCLEO, on réalise une économie supplémentaire de 1 mA.

Il est possible de “shunter” le régulateur de tension et le ST-Link en alimentant la carte NUCLEO-WL55JC1 directement par des broches prévues à cet effet. Et ses LED peuvent être désactivées en faisant fondre des fusibles (irréversible donc déconseillé). Tout ceci est documenté dans sa fiche technique.

Visualisation des mesures sur TTN puis sur TagoIO

Cette dernière étape est identique à celle du tutoriel MicroPython disponible ici, que nous vous invitons à consulter. Le parser permettant de “décoder” la payload LoRaWAN est disponible dans l’archive ZIP téléchargeable. Vous le trouverez dans le fichier Parser_LoRaWAN.txt du dossier “\Publication LoRaWAN\NUCLEO-WL55JC1_Publish.

Voici l’aspect de notre tableau de bord :

Tableau de bord TagoIO


Liens et ressources

Sur LoRa et LoRaWAN en général :

L’installation d’une passerelle TTIG est abordée par notre tutoriel dédié, qui donne aussi d’autres références.

Bibliothèque Arduino pour publier en LoRaWAN avec la NUCLEO-WL55JC1 : STM32duinoLoRaWAN.

Sur l’intégration TagoIO :

La documentation du multimètre USB JT-AT34 est disponible ici