Déploiement pas à pas d'une application IoT sur LoRaWAN
Objectif de l'application
Nous allons réaliser ici une application utilisant la librairie LMIC pour transmettre régulièrement un message via LoRa. Elle peut servir de base pour transmettre, par exemple, des mesures faites régulièrement. Le but ici est surtout de montrer comment réaliser une application utilisant la librairie LMIC.
Le code est disponible sur le gitlab de l'ENSEEIHT.
Matériel utilisé
Je vais utiliser ici des TTGO LoRa32 V2 de Lilygo (voir ma page IoT pour plus d'information). Cet équipement présente plusieurs avantages :
- il est équipé d'une puce LoRa, donc pas besoin de carte fille ;
- il est équipé d'un petit écran oled et d'un bouton poussoir, ce qui permet une interface minimale sans un PC via l'USB ;
- il est équipé d'un circuit de gestion de batterie, ce qui permet de le rentre intégralement autonome à moindre frais.
Un inconvénient est que la documentation est rare et parfois incohérente (donc partiellement fausse !).
L'infrastructure LoRaWAN sera celle à laquelle participe l'ENSEEIHT et portée par Tetraneutral.
On peut utiliser dans PlatformIO les deux environnements
ttgo-lora32-v2 et heltec_wifi_lora_32 manifestement. J'utiliserai ici le premier.
Création du projet
Nous allons donc créer un projet de la façon suivante
$ mkdir DemoLoRaWAN-LMIC
$ cd DemoLoRaWAN-LMIC
$ pio project init --board ttgo-lora32-v2
Installation des librairies
Nous allons utiliser la librairie mcci-catena pour manipuler les fonctions LoRa et LoRaWAN.
pio lib install "MCCI LoRaWAN LMIC library"
pio lib install "Adafruit SSD1306"
pio lib install "Adafruit_GFX"
La première nous permet de manipuler le chipset LoRa, les autres de profiter de l'écran Oled de nos équipements.
Nous allons modifier le fichier
.pio/libdeps/ttgo-lora32-v2/MCCI\ LoRaWAN\ LMIC\
library/project_config/lmic_project_config.h (son emplacement
précis peut être différent en fonction de votre environnement) de
sorte à utiliser les fréquences européennes :
// project-specific definitions
#define CFG_eu868 1
//#define CFG_us915 1
...
La documentation (et des exemples de code) est disponible sur la page de la librairie mcci-catena.
Écriture du code
Nous allons nous fonder sur le code fourni comme exemple avec la librairie LIMC. Nous allons y ajouter un peu d'affichage sur l'écran Oled, ...
Architecture générale
La librairie LMIC propose un ensemble de fonctions permettant une mise en oeuvre relativement simple :
- Une première étape permet de définir puis configurer (dans la
fonction
setup()) la carte LoRa. - Dans la fonction
loop()on pourra utiliser une fonction de la librairie en charge de traiter les événements par des appels aux fonctions appropriées.
Il va donc falloir écrire certaines fonctions (dites "callback") qui seront invoquées par la librairie LMIC au grès des événements.
Description de la carte
La description de la carte à utiliser passe par la définition d'une
variable globale lmic_pins décrivant le brochage du circuit
LoRa. Nous avons ici par exemple :
/*
* La configuration de l'interfaçage avec la carte LoRa est spécifique
* à notre équipement. Ici pour un lilygo TTGO LoRa32 v2
*/
const lmic_pinmap lmic_pins = {
.nss = 18,
.rxtx = LMIC_UNUSED_PIN,
.rst = 14,
.dio = {26, 33, 32}
};
Paramètres généraux de notre application
Voici les paramètres de notre application :
/*
* L'identifiant de l'application, (non utilisé par notre serveur ChirpStacK)
* codé en LSB first
*/
static const u1_t appEUI[8]={
0xF5, 0xD4, 0x54, 0x4B, 0x1C, 0xAB, 0x54, 0x1C
};
/*
* L'identifiant de l'équipement, codé en LSB fisrt
*/
static const u1_t devEUI[8]={
0x53, 0x51, 0x90, 0xc3, 0x2f, 0x04, 0x53, 0x85
};
/*
* La clef de chiffrement de l'application
*/
static const u1_t devKey[16] = {
0x59, 0x0D, 0x77, 0x1E, 0xE7, 0x5E, 0x5A, 0x94,
0x57, 0xDC, 0xAC, 0xE3, 0x22, 0x19, 0x4F, 0x8A
};
/*
* Le message en cours de transmission
*/
static char messageEnCours[] = "HelloWorld!";
/*
* Quelques stats pour affichage
*/
unsigned int nbTxOK = 0; // Nombre de transmissions réussies
/*
* Période d'émission des messages (en secondes)
*/
#define PERIODE_EMISSION 60
La PERIODE_EMISSION nous permet de définir le délai minimal entre
deux transmissions.
Initialisation
L'initialisation est réalisée par la librairie au travers de la
fonction os_init() et un callback peut être défini qui sera
invoqué lors de l'initialisation.
Nous allons donc écrire cette fonction de callback :
/*
* Actions à mener lors de l'initialisation
*/
osjob_t jobInitialisation;
void initialisationLMIC(osjob_t* j)
{
LMIC_reset(); // On réinitialise la couche MAC
LMIC_startJoining(); // On tente de rejoindre le réseau
}
Comme vous le remarquez, elle est très simple, et lance une procédure de join au réseau.
L'initialisation proprement dite, dans la fonction setup()
est elle aussi plutôt simple :
// Initialisation de la librairie LMIC
os_init();
os_setCallback(&jobInitialisation, initialisationLMIC);
// Un callback pour les changements d'état
LMIC_registerEventCb(gestionEvLMIC, NULL);
Nous avons enregistré une fonction de callback qui sera invoquée à chaque événement de la librairie LMIC. Voyons maintenant cette fonction.
Gestion des événements
La librairie LMIC va donc invoquer, à chaque événement, la fonction
que nous avons définie avec LMIC_registerEventCb(). Voici le
code de cette fonction
/*
* Gestion des événements LMIC
*/
void gestionEvLMIC(void * inutile, ev_t ev)
{
// On met à jour l'affichage
displayUpdate(ev);
switch(ev) {
case EV_JOINED: // Lorsqu'on a rejoint le réseau, on transmet
os_setTimedCallback(&jobTransmission, os_getTime()+delai, demandeTransmission);
break;
case EV_TXCOMPLETE:
nbTxOK++;
// On reprogramme une transmission
os_setTimedCallback(&jobTransmission, os_getTime()+delai, demandeTransmission);
break;
}
}
Vous l'avez comris, cette fonction va essentiellement faire deux choses
- mettre à jour l'affichage à chaque événement ;
- (re)programmer une transmission chaque fois que possible, en les espaçant d'une durée minimale.
Affichage
Nous devons définir les éléments liés à l'affichage, par exemple :
/*
* Le brochage du bus de l'écran
*/
#define SDA_PIN 21
#define SCL_PIN 22
/*
* Les caractéristiques de l'écran
*/
#define LARGEUR_OLED 128
#define HAUTEUR_OLED 64
/*
* Déclaration de l'écran
*/
Adafruit_SSD1306 ecranOled(LARGEUR_OLED, HAUTEUR_OLED, &Wire);
/*
* Pour afficher les événements reçus
*/
const char * evName[] = {"timeout", "beaconf", "beaconm",
"beacont", "joining", "joined ",
"rfu1 ", "ffailed", "rfailed",
"txcmplt", "synlost", "reset ",
"rxcmplt", "lnkdead", "lnklive",
"scanfnd", "txstart", "txcancl",
"rxstart", "jtxcmpl"};
L'initialisation peut se faire ainsi dans la fonction de
setup() :
// Initialisation de l'écran
Wire.begin(SDA_PIN, SCL_PIN);
if(!ecranOled.begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) {
Serial.println(F("SSD1306 allocation failed"));
while(1){};
}
// Affichage d'un petit message
ecranOled.clearDisplay();
ecranOled.setTextColor(SSD1306_WHITE);
ecranOled.setTextSize(2);
ecranOled.setCursor(0,0);
ecranOled.print("C'est parti !");
ecranOled.display();
La fonction displayUpdate() nous permettra d'afficher les
principales informations relatives à la transmission :
/*
* Affichage de l'état général sur l'écran
*/
void displayUpdate(ev_t ev)
{
u1_t id[8];
int n;
os_getDevEui(id);
// Préparation de l'affichage
ecranOled.clearDisplay();
ecranOled.setTextColor(WHITE);
ecranOled.setTextSize(1);
// Affichage du dernier événement
ecranOled.setCursor(0, 0);
ecranOled.print("e=");
ecranOled.print(evName[ev-1]);
// L'identifiant
ecranOled.setCursor(0, 56);
ecranOled.print("id : ");
for (n = 0 ; n < 8; n++){
ecranOled.print(id[n], HEX);
}
// La version de la librairie LMIC
ecranOled.setCursor(65, 0);
ecranOled.print("LMIC ");
ecranOled.print(ARDUINO_LMIC_VERSION_GET_MAJOR(ARDUINO_LMIC_VERSION));
ecranOled.print(".");
ecranOled.print(ARDUINO_LMIC_VERSION_GET_MINOR(ARDUINO_LMIC_VERSION));
ecranOled.print(".");
ecranOled.print(ARDUINO_LMIC_VERSION_GET_PATCH(ARDUINO_LMIC_VERSION));
// Le nombre de transmissions réussies
ecranOled.setCursor(0, 16);
ecranOled.setTextSize(2);
ecranOled.print("Tx : ");
ecranOled.print(nbTxOK);
// Le message en cours d'envoie
ecranOled.setTextSize(1);
ecranOled.setCursor(0, 40);
ecranOled.print(messageEnCours);
// On montre tout ça
ecranOled.display();
}
Je pense que tout cela se passe de commentaires !
La boucle principale
Il faut régulièrement donner la main à la librairie LMIC afin qu'elle traite les événements LoRa. Nous aurons donc par exemple le code suivant
/*
* La boucle principale
*/
void loop() {
// Traitement des événements LoRa
os_runloop_once();
}
Il est possible d'intercaler du code gérant d'autres activités (par exemple utiliser un capteur pour obtenir des mesures), mais elles ne doivent pas prendre un temps trop long, afin de ne pas rater d'échéances LoRa.
Compilation et premier test
$ pio run
On peut ensuite transférer le programme sur la carte si elle est reliée au PC :
$ pio run --target upload
En fait, cette commande réalise également la compilation, si nécessaire (fichiers modifiés) si bien que la commande précédente n'est utile que pour compiler un programme sans le transférer.
L'écran Oled permet de vérifier le bon fonctionnement. Pour cela, naturellement, le serveur doit, lui aussi, être configuré !
Installation du côté serveur
Là, c'est une autre paire de manches, et ce n'est pas l'objectif de ce document. Le serveur de Tetaneutral utilise ChirpStack sur le site duquel on trouvera des informations intéressantes.
Je décrirai cela dans une autre page.
Installation du côté client
Ce sera également décrit ailleurs, ...