Linux Embedded

Le blog des technologies libres et embarquées

Contiki-NG et AWS IoT

Le réseau de capteurs sans fil (RCSF) appelé aussi WSN (Wireless Sensor Network) est un domaine de recherche en expansion qui a su trouver son chemin vers l'industrie. Le succès de ces petites cartes embarquées revient principalement à leurs systèmes d'exploitation conçus spécifiquement pour des environnements à ressources très limitées (dont Contiki est le plus connu).
Une liste exhaustive des OS destinés au RCFS est disponible sur : https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3231431/.

Toutefois, Contiki est devenu obsolète (il n'est plus maintenu) mais sa nouvelle génération vient de faire apparition et porte le nom de Contiki-NG que nous allons découvrir dans cet article.

Contiki-NG

Contiki-NG est un OS Open source et léger écrit en langage C. Il est encore plus modulaire que son prédécesseur (Contiki) et fournit davantage d'APIs pour créer des applications (plus riches et plus robustes) destinées aux capteurs WSN (multitâche, proto-threads, TCP/IP (IPv6) ou même l'intégration d'un mini serveur web). On appelle souvent une carte embarquée qui fait tourner Contiki-NG un mote WSN (ou juste mote).

Les bases de Contiki-NG

Avant de nous lancer dans la gestion des timers, événements ou même la communication INTRANET ou vers INTERNET, il faut aborder le concept de processus Contiki-NG car toutes les bibliothèques et modules reposent sur ce dernier (et ont besoin d'un processus pour fonctionner).

Un processus en Contiki-NG

Chaque processus Contiki-NG est composé de deux parties :

  • Bloc de contrôle de processus (PCB) : contient des informations sur le processus, son état et un pointeur vers son implémentation (le PCB est stocké sur la RAM). Le PCB est déclaré sous Contiki-NG avec la MACRO : PROCESS(nom_processus, nom_debug), ou nom_debug est une chaîne de caractère utilisée à des fins de débogage (par exemple : pour lister les processus en cours d'exécution).
  • Protothread : chaque processus est implémenté sous forme de thread léger et adapté aux contraintes des WSN. Ces derniers sont connus dans le jargon de Contiki-NG sous le nom protothread (leur code est stocké sur la ROM). Un protothread est déclaré avec la MACRO : PROCESS_THREAD(nom_processus, ev, data) où :
    • ev : identifiant de l'événement passé au protothread.
    • data : données passées en paramètre.

Un Hello world en Contiki-NG

Un programme Contiki-NG se structure avec au moins deux fichiers : le(s) fichier(s) source(s) et un makefile.
Il est temps de décortiquer les étapes nécessaires pour créer un programme Contiki-NG :

  • Commençons par créer un dossier "my_hello_world" dans contiki-NG/examples (il faut cloner https://github.com/contiki-ng/contiki-ng si cela n'est pas déjà fait). Ce dernier va contenir nos deux fichiers : un fichier source "my_hello_world.c" (servira comme point d'entrée et contiendra le code du programme) et un Makefile.
    N.B : le fichier qui sert de point d'entrée (l'équivalent du main en C) doit toujours être nommé comme le dossier du projet (dans notre cas "my_hello_world") .
  • my_hello_world.c : fichier source qui lance un processus (qui affiche le fameux "bonjour monde") :
#include "contiki.h" // Il faut toujours inclure ce header
#include <stdio.h>   // Pour utiliser des printfs

// Création d'un bloc de contrôle de processus
PROCESS(display_hello_world, "display hello world");

// Lancer le processus dès le démarrage du capteur
AUTOSTART_PROCESSES(&display_hello_world);

// Prothread du processus (implémentation)
PROCESS_THREAD(display_hello_world, ev, data){
    PROCESS_BEGIN();
    printf("Hello world from Contiki-NG\n");
    PROCESS_END();
}
  • Ajouter un makefile : le makefile est similaire sur tous les projets Contiki-NG et change très peu (seulement dans le cas où des modules supplémentaires sont à intégrer ou pour inclure des sous-répertoires lors de la compilation) :
# CONTIKI_PROJECT doit être définit comme
# le nom de votre projet
CONTIKI_PROJECT = my_hello_world

# Les lignes suivantes sont toujours les mêmes
# dans les projets Contiki
all: $(CONTIKI_PROJECT)
# Chemin vers le dossier root des sources contiki-ng
CONTIKI = ../..
include $(CONTIKI)/Makefile.include

Compiler un programme Contiki-NG sur un capteur WSN

Les applications Contiki-NG peuvent être exécutées de différentes manières sur plusieurs environnements. Cependant, la compilation reste la même et doit se faire comme ceci :

make BOARD=chemin_vers_board TARGET=chemin_vers_target

où :

  • TARGET : désigne la plateforme dans contiki-ng/arch/platform, par exemple : cc26x0-cc13x0.
  • BOARD : ce champs est requis seulement si la plateforme choisie (dans TARGET) contient plusieurs cartes cibles à l'intérieur (comme cc26x0-cc13x0 qui regroupe launchpad/cc1310, launchpad/cc2640r2 ou même sensortag/cc2650).
    N.B : Ce champ n'est pas obligatoire dans le cas de plateformes comme : sky, z1 ou cc2538dk.

La commande : sudo make login PORT=/dev/chemin_port BOARD=chemin_board TARGET=chemin_vers_target permet de visualiser la communication sur le port série (entre le mote WSN et le PC).

Il nous est possible désormais de compiler et d'exécuter le programme :

  1. Exécution dans l'environnement de développement (Natif): en cas de nécessité de faire des tests rapides (où même si l'utilisateur n'est pas en possession d'une plateforme WSN), cette solution peut être utile.
    • Compilation du code : il suffit de faire un make TARGET=native WERROR=0
    • Lancer le programme : le binaire généré peut être exécuté comme tout autre programme classique sous Linux (dans notre cas : $ ./my_hello_world.native).
  2. Exécution sur un capteur WSN (Board):
    • Compilation du code : on va suivre la règle de compilation déjà introduite plus haut comme ceci :
    • Lancer le programme : quelques nuances existent pour téléverser le binaire sur une cible supportée par Contiki-NG. Dans le cas général, il suffit de faire un make PORT=/dev/PORT_WSN programme_genere.upload.
      Cependant, cette méthode ne fonctionne pas sur les boards Texas Instruments comme le CC2650-LaunchPAD, dans ce cas, il faut utiliser : UniFLASH. La procédure se fait comme suit :
      • UniFlash doit détecter votre mote WSN.
      • UniFlash peut flasher le .bin généré dans le dossier build, dans notre cas : my_custom_scripts/hello-world/build/cc26x0-cc13x0/launchpad/cc2650/hello-world.bin comme ceci :
  3. Simuler un programme Contiki-NG sous Cooja :
    Cooja est un programme de simulation de réseau Contiki (et Contiki-NG), il est accessible sur : contiki-ng/tools/cooja/. Cooja est très intuitif à utiliser, un cours complet est disponible sur : https://github.com/contiki-ng/contiki-ng/wiki/Tutorial:-Running-Contiki%E2%80%90NG-in-Cooja

Gestion du temps et des timers

Jusqu'ici, nous avons abordé les bases et implémenté des processus Contiki-NG. Cependant, il faut appréhender la notion de la gestion des tâches (par exemple : comment exécuter une lecture sur un port série toutes les 3 secondes?) pour éviter que nos processus se lancent et tournent d'une manière aléatoire. Pour cela nous devons apprendre une notion incontournable "le temps".

Contiki-NG embarque une bibliothèque d'horloge appelée "Clock" pour gérer le temps et des modules pour manipuler les timers : timer, stimer, etimer, ctimer et rtimer. Ces derniers sont accessibles sur contiki-ng/os/sys.

La bibliothèque Clock

Ce module permet de manipuler le temps de manière générale, la liste des APIs peut être consultée dans le fichier contiki-ng/os/sys/clock.h. Les fonctions les plus importantes :

  • clock_time_t clock_time() : pour récupérer l'heure exprimé sous forme d'un nombre de ticks depuis le démarrage.
  • void clock_delay(uint16_t delay) : retarder (d'un nombre donné de microsecondes) l'exécution du processus.
  • La constante CLOCK_SECOND : renvoie le nombre de ticks d'horloge par seconde (cette constante est primordiale lors de l'utilisation des timers non temps réel).

Pour récupérer le temps système sur un mote WSN (tournant Contiki-NG), il suffit de faire :

clock_time_t t = clock_time();
printf("Temps en cours : %lu \n", t);

Prendre en main les timers

Pouvoir mesurer le temps est important (surtout quand il faut déboguer des logs) mais avoir la possibilité de générer des interruptions pour exécuter des bouts de codes régulièrement (et sortir du modèle d'exécution coopérative de Contiki-NG) est une nécessité absolue. En effet, Contiki-NG regroupe des modules pour manipuler (créer, modifier, réenclencher et vérifier l'expiration) différents timers (il en existe 5 dont 1 est temps réel).

  • Module timer.h (scrutation) : le plus basique et le moins avantageux, le processus doit faire une scrutation sur l'expiration du timer.
  • Module stimer.h (scrutation): même que timer.h, la seule distinction est l'usage de "seconde (CLOCK_SECOND)" comme unité par défaut (dans stimer.h).
  • Module etimer.h (événementiel) : Ce type de timer génère un événement possible à détecter (avec une fonction de capture d'événements comme PROCESS_WAIT_EVENT_UNTIL(), la liste complète de ses fonctions est disponible sur : https://contiki-ng.readthedocs.io/en/latest/_api/group__process.html). Les API les plus communes sont :
    • void etimer_set(struct etimer *t, clock_time_t interval) : démarre le timer.
    • void etimer_reset(struct etimer *t) : redémarre le timer.
    • void etimer_stop(struct etimer *t) : pour arrêter le timer.
    • int etimer_expired(struct etimer *t) : vérifier si le timer est expiré. Un exemple d'utilisation pourrait être le suivant:
// Réglage du timer pour s'enclencher toutes les 5 secondes
etimer_set(&etimer_timer, 5 * CLOCK_SECOND);

// Généralement un processus contiki-ng doit s'exécuter à l'infini
while(1){
    /* si le timer n'est pas expiré, PROCESS_WAIT_EVENT_UNTIL libère
    * le processeur (processus en mode SLEEP), sinon poursuivre l'exécution.*/
    PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&etimer_timer));
    printf("Le timer est expiré \n");
    // réarmer le timer, sinon il ne sera exécuté qu'une seul fois.
    etimer_reset(&etimer_timer);
}
  • Module ctimer.h (événementiel) : même que etimer.h, cependant; ctimer.h ajoute la possibilité d'appeler une fonction callback lors d'un événement ctimer (une chose qui fait que cette bibliothèque est très appréciée).
void timer_expired_callback(void *ptr) {
    printf("Le timer est expiré \n");

    ctimer_reset(&ctimer_timer);// réarmer le timer
}

/* Réglage du timer pour s'enclencher toutes les 2 secondes *
*  L'argument NULL est un pointeur vers des données qu'on pourra passer à la callback. */
ctimer_set(&ctimer_timer, 2 * CLOCK_SECOND, timer_expired_callback, NULL);

while(1){
    PROCESS_WAIT_EVENT_UNTIL(ctimer_expired(&ctimer_timer));
}
  • Module rtimer.h (événementiel) : pour effectuer un ordonnancement et une exécution des tâches temps réel. C'est la seule bibliothèque qui utilise sa propre horloge. Pour bien l'utiliser :
    • Remplacer la Macro CLOCK_SECOND par RTIMER_SECOND.
    • et RTIMER_NOW() qui remplace clock_time().

NB : Si vous avez des doutes sur quel module utiliser pour des applications non temps réel, sachez que dans 99% des cas, il faut s'appuyer sur etimer ou ctimer (personnellement, je préfère ctimer car elle permet d'isoler le code de la callback du code principal).

Exemple Timers 1 : affichage des valeurs de capteurs de température et son

Nous allons prétendre qu'une entreprise spécialisée dans le transport de marchandises maritime a besoin de faire tourner un programme Contiki-NG sur des capteurs compatibles. Le but est de surveiller le niveau sonore mais aussi l'état des moteurs au sein des salles machines de leurs navires.

Solution : une méthode possible pour résoudre le problème est proposée sur le schéma suivant :

  • La cible Contiki-NG va lancer 2 processus (à l'infini) :
    1. Processus sound_sensing_process : qui sera réveillé toutes les 3 secondes pour lire la valeur du niveau sonore (exprimée en dB).
    2. Processus motor_rotation_sensing_process : exécuté toutes les 5 secondes pour lire l'état du moteur (en rotation ou pas).
  • Les modules etimer ou ctimer peuvent réveiller ces deux processus régulièrement pour l'acquisition des données (nous utiliserons etimer et ctimer pour démontrer leur utilisation dans cet exemple).

N.B : l'acquisition des données de capteurs par Contiki-NG est différente selon la plateforme utilisée. C'est pour cela que nous allons les simuler dans cette démonstration.

Le code suivant représente une des façons d'implémenter la solution présentée précédemment :

  • sensors_ship_value_acquisition.c : le code complet est disponible sur : https://github.com/jugurthab/sensors_ship_value_acquisition.
    • Déclarer les prérequis du programme
      #include "contiki.h"
      #include "random.h" // Générateur de nombres pseudo-aléatoire
      #include "sys/log.h" // Gestion des logs
      
      #define LOG_MODULE "Test_timer" // Préfixe des logs (obligatoire)
      #define LOG_LEVEL LOG_LEVEL_INFO // Niveau de criticité des logs (obligatoire)
      
      #define MAX_SOUND_INTENSITY 150
      #define MIN_SOUND_INTENSITY 0
      
      PROCESS(motor_rotation_sensing_process, "Motor rotation sensing process");
      PROCESS(sound_sensing_process, "Sound sensing process");
      /* AUTOSTART_PROCESSES lance les processus en paramètres après le démarrage 
      * de la carte */
      
      AUTOSTART_PROCESSES(&sound_sensing_process, &motor_rotation_sensing_process);
      static struct etimer etimer_timer_motor; // timer de type etimer
      static struct ctimer ctimer_timer_sound; // timer de type ctimer
      
    • Processus moteur : relancé toutes les 3 secondes.
      /********* Processus vérification de la rotation du moteur *********/
      // getMotorRorationStatus simule l'état de rotation d'un moteur
      unsigned short getMotorRorationStatus(){
          return (random_rand() & 1); // retourne 1 ou 0 (1 => moteur en rotation)
      }
      
      // Implémentation du processus motor_rotation_sensing_process
      PROCESS_THREAD(motor_rotation_sensing_process, ev, data) {
          PROCESS_BEGIN();
          LOG_INFO("------- Processus rotation moteur démarré ...\n");
          etimer_set(&etimer_timer_motor, 3 * CLOCK_SECOND);
          while(1){
              // Libérer le processeur tant que le timer n'est pas expiré
              PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&etimer_timer_motor));
              LOG_INFO("L'état du moteur : %s\n",
                      getMotorRorationStatus()?"en rotation": "éteint");
              etimer_reset(&etimer_timer_motor); // réarmer le timer
          }
          PROCESS_END();
      }
      
    • Processus niveau sonore : exécuté chaque 5 secondes.
      /********* Processus vérification du niveau sonore *********/
      // getSoundIntensity simule la valeur du niveau sonore 
      void getSoundIntensityCallback(){
          int soundIntensity = random_rand()
                              % (MAX_SOUND_INTENSITY + 1 - MIN_SOUND_INTENSITY)
                              + MIN_SOUND_INTENSITY;
          LOG_INFO("Niveau sonore %d dB\n", soundIntensity);
          ctimer_reset(&ctimer_timer_sound);
      }
      
      // Implémentation du processus sound_sensing_process
      PROCESS_THREAD(sound_sensing_process, ev, data) {
          PROCESS_BEGIN();
          LOG_INFO("------- Processus mesure niveau sonore démarré \n");
          random_init(0); // Init du générateur pseudo-aléatoire
          ctimer_set(&ctimer_timer_sound, 5 * CLOCK_SECOND,
                      getSoundIntensityCallback, NULL);
          while(1){
              /* Plus besoin d'utiliser PROCESS_WAIT_EVENT_UNTIL car
              * le timer est déjà configuré avec la callback qui sera appelée
              * automatiquement lors de l'expiration de ce dernier.
              * on libère le processeur
              * (reprend l'exécution après l'expiration du timer) */
              PROCESS_YIELD();
          }
          PROCESS_END();
      }
      
  • et enfin le makefile
    CONTIKI_PROJECT = sensors_ship_value_acquisition
    all: $(CONTIKI_PROJECT)
    
    CONTIKI = ../..
    include $(CONTIKI)/Makefile.include
    
  • Il est temps de voir le résultat de l'exécution du programme comme démontré dans la figure suivante :

Exemple Timers 2 : Un scheduler temps réel avec rtimer.h

Comme déjà mentionné plus haut dans cet article, les processus Contiki-NG sont soumis à une politique coopérative. Cela va à l'encontre de certaines applications critiques dont les tâches doivent être exécutées selon un certain ordre avec des priorités bien précises.
Le module rtimer.h nous permet de créer des fils FIFO (contenant des tâches à exécuter en temps réel).

Pour cela nous allons faire évoluer l'exemple précédent comme ceci :

Comme on peut le voir sur le schéma, les tâches sont déclarées dans une liste FIFO que le processus va consommer à chaque expiration du module rtimer.

Une implémentation possible est la suivante :

  • Déclaration des prérequis
    // Déclaration et lancement du processus "rtime_sensor_reader_scheduler"
    PROCESS(rtime_sensor_reader_scheduler, "FIFO real time sensor reader scheduler");
    AUTOSTART_PROCESSES(&rtime_sensor_reader_scheduler);
    
    static struct rtimer rtimer_timer; // Création d'un timer temps réel
    
    /*
    * Structure d'une tâche
    */
    struct sensor_task {
        struct sensor_task *next_sensor; // Pointeur vers la prochaine tâche
        unsigned short requestReadOperation; /* Indique le type de tâche
                                              * (comme :
                                              * REQUESTED_READ_OPERATION_MOTOR_ROTATION)
                                              */  
        int requestReadOperationValue; // Valeur retournée par l'exécution de la tâche
    };
    
    static struct sensor_task *firstSensorStructHeader; // Renvoie la tâche à exécuter
    volatile short isAllFifoTasksExecuted = 1;
      
    // Déclaration d'une liste chainée avec le type sensor_task;
    // Le nom doit toujours être sous la forme "NOM_STRUCTURE + _list"
    LIST(sensor_task_list);
    
  • Processus de gestion de tâches temps réel
    PROCESS_THREAD(rtime_sensor_reader_scheduler, ev, data)
    {
        PROCESS_BEGIN();
        int i = 0;    
        LOG_INFO("------- REAL TIME FIFO CONTIKI-NG SCHEDULER ----------\n");
        // Initialisation de la liste qui contiendra nos tâches    
        list_init(sensor_task_list);
       
        // Contiendra les tâches à exécuter
        struct sensor_task sensorTasks[MAX_NB_SENSOR_TASKS_FIFO];
    
        random_init(0); // Pour la génération pseudo-aléatoire des valeurs des capteurs
    
        LOG_INFO("Remplissage initiale de la liste\n");
    
        for(i = 0; i < MAX_NB_SENSOR_TASKS_FIFO; i++){
            /* Assignation du type de la tâche
            *  (comme : REQUESTED_READ_OPERATION_SOUND_INTENSITY)
            */
            sensorTasks[i].requestReadOperation = i;
            // La tâche ne contient aucune valeur initialement.
            sensorTasks[i].requestReadOperationValue = 0;
            list_add(sensor_task_list, &sensorTasks[i]); // Ajouter la tâche à la liste
        }
    
        for(;;) {
            /* Si première exécution ou si toutes les tâches ont été
            *  accomplies, ré-exécuter les tâches
            */       
            if(isAllFifoTasksExecuted == 1){
                isAllFifoTasksExecuted = -1;
                // Recherche du premier élément de la liste
                //if(firstSensorStructHeader==NULL)            
                firstSensorStructHeader = list_head(sensor_task_list);
                
                /* Configuration d'un timer rtime et passage en paramètre de l'adresse
                * du premier élément de la liste.
                * le deuxième paramètre désigne le moment où la tâche sera exécutée.
                * le troisième paramètre n'est pas utilisé par Contiki-NG
                * (on passe 0 ou 1 par convention).
                */
                rtimer_set(&rtimer_timer, RTIMER_NOW() + (RTIMER_SECOND/2), 1,
                           sensor_read_operation_callback, firstSensorStructHeader);
            }
        }
        PROCESS_END();
    }
    
  • callback d'exécution de tâches temps réel
    void sensor_read_operation_callback(struct rtimer *t, void *ptr)
    {
        struct sensor_task *sensors = ptr;
    
        switch(sensors->requestReadOperation){ // Décodage du type de la tâche
            case REQUESTED_READ_OPERATION_MOTOR_ROTATION:
                displayMotorRorationStatus(sensors);  
            break;
    
            case REQUESTED_READ_OPERATION_SOUND_INTENSITY:
                displaySoundIntensity(sensors);
            break;
            
            case REQUESTED_READ_OPERATION_TEMPERATURE:
                displayTemeratureValue(sensors);
            break;
    
            default:
            break;
        }
    
        // Lire la prochaine tâche
        firstSensorStructHeader = list_item_next(firstSensorStructHeader);
        // S'il reste encore des tâches dans la liste, on réarme le timer    
        if(firstSensorStructHeader != NULL){
            rtimer_set(&rtimer_timer, RTIMER_NOW() + (RTIMER_SECOND/2),
                1, sensor_read_operation_callback, firstSensorStructHeader);
        } else {
            isAllFifoTasksExecuted = 1;
            LOG_INFO("--------- (la pile est maintenant vide) -------------\n");
        }
    }
    

et voici le résultat d'exécution du programme :

Le code complet est disponible sur : https://github.com/jugurthab/rtime-scheduler.c.

Communiquer avec l'extérieur

Contiki-NG nous propose diverses solutions pour communiquer avec d'autres entités. Nous allons découvrir les plus appréciées.

COM vers le PC (Les shells)

L'une des méthodes possibles pour communiquer avec votre ordinateur et de mettre à disposition de ce dernier un Shell. Si le lecteur est familier avec Contiki, alors il faut savoir que les fonctionnalités Shell ont bien changées.

Important : le shell ne fonctionne pas en native

Le shell permet souvent de voir le statut du mote WSN et de mettre en place des configurations avancées (comme le routage)

  • Utiliser le shell par défaut : Contiki-NG fournit un Shell par défaut (solution utilisée la plupart du temps) qui regroupe les commandes intégrées comme : help, ping, ..., etc.
    Pour inclure ce Shell, il suffit simplement d'ajouter le module concerné "Shell" dans le makefile comme ceci (le projet utilisé est le "hello-world" fourni par Contiki-NG) :
    CONTIKI_PROJECT = hello-world
    all: $(CONTIKI_PROJECT)
    
    CONTIKI = ../..
    
    MODULES += os/services/shell
    
    include $(CONTIKI)/Makefile.include 
    

    Il est maintenant temps d'exécuter le programme (appuyez sur Entrée pour faire apparaitre le shell dans la console après redémarrage de la carte) comme ceci :

  • Ajouter des commandes au Shell Contiki-NG : Il est possible d'ajouter nos propres commandes au Shell. Pour le faire, il faut modifier le fichier /os/services/shell/shell-commands.c comme ceci :
    1. Déclarer la commande : Ajoutez votre commande au tableau "builtin_shell_commands" comme ceci :
    2. Implémenter la commande : Vous pouvez ajouter votre code (par exemple en dessous de la commande reboot) comme démontré ci dessous :
    3. Chargez le nouveau programme sur votre carte et entrez la nouvelle commande sur le shell comme suit :

N.B : Il est important de garder le nombre de commandes shell au minimum pour économiser la taille des programmes Contiki-NG.

Autre méthode de COM avec votre PC

Il est tout à fait possible de communiquer avec un mote Contiki-NG sur la liaison série de l'USB (par exemple : avec le module pyserial de python). En effet, la commande make login lance une application pour écouter sur le port COM (/dev/tty*) et affiche les données reçues. Créer un programme de communication peut être utile pour personnaliser l'affichage, ajouter des filtres sur ses données ou même pour relayer les informations vers des espaces de stockage (comme les bases de données).

Communiquer avec d'autres éléments

Contiki-NG offre une pile réseau assez complète pour interagir avec le monde extérieur. Le module réseau est accessible sur "<contiki-ng>/os/net". Cependant, ce dernier repose sur le 6LowPAN, une chose qui demande souvent d'avoir un middleware pour convertir vers des trames IPV6 classiques (et vice-versa) pour permettre un accès vers l'extérieur.

Quand un réseau est composé de plusieurs motes Contiki-NG, un mote de référence doit être désigné appelé "root". Ce dernier servira comme passerelle pour réaliser une communication dans l'intranet (entre motes) et vers internet. Contiki-NG applique l'algorithme "RPL Lite" (qui est une implémentation optimisée de l'algorithme de Dijkstra) pour calculer les chemins les plus courts vers le mote "root". En pratique, on utilise la fonction NETSTACK_ROUTING.root_start(); pour affecter le rôle de root à un mote. Nous allons voir comment la communication se fait dans deux cas :

  1. Communication avec d'autres motes Contiki-NG : les motes peuvent communiquer avec le protocole UDP.
    • Le serveur : configuré en mote root dans notre cas.
      #define UDP_SERVER_PORT 5678
      #define UDP_CLIENT_PORT 8765
      PROCESS(udp_server_process, "UDP server");
      static void message_received_callback(struct simple_udp_connection *c,
                                            const uip_ipaddr_t *sender_addr,
                                            uint16_t sender_port,
                                            const uip_ipaddr_t *receiver_addr,
                                            uint16_t receiver_port, const uint8_t *data,
                                            uint16_t datalen) {
          LOG_INFO("=> Reçu '%.*s'\n", datalen, (char *) data); 
      }
      /*---------------------------------------------------------------------------*/
      PROCESS_THREAD(udp_server_process, ev, data) {
      
          PROCESS_BEGIN();
      
          /* Initialiser le mote en tant que ROOT */
          NETSTACK_ROUTING.root_start();
      
          /* Enregistrement d'un socket UDP et une callback à exécuter
          * lors de la réception d'un message */
          simple_udp_register(&udp_server_socket, UDP_SERVER_PORT, NULL, UDP_CLIENT_PORT, message_received_callback);
      
          PROCESS_END();
      }
      
    • Le client : qui se charge d'envoyer des données au mote Root. Le client doit aussi vérifier le que le Root est joignable.
      PROCESS_THREAD(udp_client_process, ev, data) {
          static char str[32];
          uip_ipaddr_t dest_ipaddr;
          PROCESS_BEGIN();
      
          /* Initialisation du socket UDP */
          simple_udp_register(&udp_client_socket, UDP_CLIENT_PORT,
                              NULL, UDP_SERVER_PORT, NULL);
      
          while(1) {     
              // Il faut toujours vérifier si le le mote root est joignable
              if(NETSTACK_ROUTING.node_is_reachable()
                  && NETSTACK_ROUTING.get_root_ipaddr(&dest_ipaddr)) {
      
                  snprintf(str, sizeof(str), "Ping du client", count);
                  // Envoyer le message à l'IP du mote root
                  // (Cette adresse peut être aussi l'IP d'un autre mote)
                  simple_udp_sendto(&udp_client_socket, str, strlen(str),
                                    &dest_ipaddr);
              } else {
                  // Communication impossible si le mote root est déconnecté
                  LOG_INFO("Not reachable yet\n");
              }
          }
          PROCESS_END();
      }
      

      Voici un exemple de messages de température envoyés par un client Contiki-NG vers le mote Root sur : https://github.com/jugurthab/linux_embedded_articles/tree/master/Contiki-NG; et le résultat après exécution :

    N.B : Contiki-NG permet aussi de trouver les adresses IP des autres motes dans le réseau. Les motes clients peuvent également implémenter une callback pour recevoir des messages du destinataire. Bref, un mote peut jouer le rôle d'un client et d'un serveur (la communication sera toujours possible tant que le mote root est fonctionnel).

  2. Communication vers le réseau internet : Dans ce cas précis, le mote Contiki-NG désigné en tant que "root" doit être compilé avec un module appelé "RPL border router" (ce module va créer la passerelle). Contiki-NG met à notre disposition le programme tunslip6 pour créer un tunnel et rediriger le trafic 6LowPAN du mote root connecté à votre PC (via USB) vers internet.

    Un exemple serait plus parlant, nous allons modifier l'un des programmes que nous avons codé jusqu'ici (par exemple le fameux "hello world") comme ceci :

    1. Ajouter le support pour "border router" dans le makefile du projet :
      CONTIKI_PROJECT = hello-world
      all: $(CONTIKI_PROJECT)
      
      CONTIKI = ../..
      
      MODULES += os/services/rpl-border-router
      
      include $(CONTIKI)/Makefile.include
      
    2. Lancer tun6slip : l'outil est disponible sur contiki-ng/tools/serial-io/tunslip6 (il faut le compiler) et se lance comme ceci :
      ./tunslip6 prefix_ip -s port_tty_mote_contiki_ng
      ou : est le préfixe IPV6 qui sera assigné à tous les motes. Généralement, on utilise fd00::1/64 comme préfixe (ce qui veut dire que les motes se verront attribués des adresses IP de la forme : fd00::xxxx). L'image suivante illustre le résultat de la commande : ./tunslip6 fd00::1/64 -s /dev/ttyACM0 :
    3. Accéder au mote par internet :
      Notre mote est dorénavant accessible depuis internet, un simple ping permet de le confirmer :

AWS IoT

AWS est une plateforme cloud offrant des services de stockage, de bases données, de traitement et de sécurité. Avec plus de 195 services, AWS s'adapte à tout types d'entreprises ou particulier. Une introduction générale à AWS (en anglais) est déjà disponible sur : http://easycompterlearning.blogspot.com/2019/11/loosely-coupled-applications-using-aws.html.

Nous allons faire communiquer notre mote Contiki-NG à AWS. Un middleware doit être ajouté également pour des raisons qu'on va expliquer plus tard.

Interagir avec AWS IoT

AWS a introduit le service "AWS IoT" pour permettre à tout objet IoT d'émettre ou recevoir des données ou des commandes du cloud. Nous allons présenter la manière de configurer AWS IoT pour pouvoir acheminer les données envoyées par nos mote Contiki-NG.

La configuration AWS que nous allons aborder n'est pas propre seulement à Contiki-NG mais peut être utilisée pour tout autre système qui souhaite envoyer ou recevoir des données du CLOUD.

Préparer et configurer AWS IoT

Tout objet IoT doit être configuré reconnu et autorisé par AWS avant toute interaction.

  1. Mettre en place une stratégie : désigne les permissions affectées à une entité (un programme ou une personne) lors de l'usage des ressources AWS. Notre mote Contiki-NG doit posséder assez de droits pour pouvoir publier de l'information sur un topic (nous verrons plus tard que c'est le middleware qui prendre en charge le relai des données). Les étapes suivantes sont à effectuer lors de la création d'une stratégie :
    • Pratiquement, les stratégies sont stockées dans un fichier JSON. Voici un exemple de stratégie :

      {
         "Version":"2012-10-17",
         "Statement":[
            {
               "Effect":"Allow",
               "Action":[
                  "iot:*"
               ],
               "Resource":"arn:aws:iot:VOTRE_REGION:VOTRE_COMPTE:topic/mon_topic"
            },
            {
               "Effect":"deny",
               "Action":[
                  "s3:*"
               ]
            }
         ]
      }
      

      ou le premier Effect autorise les accès à toutes les fonctionnalités IoT et le deuxième Effect interdit chaque activité sur le service stockage (appelé S3).

      N.B : par défaut, seul l'utilisateur root (créé automatiquement lors de la création d'un compte AWS) possède toutes les autorisations d'accès aux ressources (c'est la stratégie par défaut).

      Commencez par naviguer sur la plage de création de stratégies et cliquez sur "Créer" :

    • La fenêtre suivante vous invite à définir le contenu de la stratégie (comme le nom et les ARN de ressources). Le champ "Action" représente les fonctionnalités AWS Iot auxquelles il faut accorder ou refuser les accès (avec la case à cocher dans "Effet").
    • Une fois le contenu défini, il ne reste plus qu'à cliquer sur "créer" et vous serez redirigé vers la liste des stratégies comme ci dessous :
  2. Création d'un objet virtuel et d'un certificat: chaque objet dans la réalité doit avoir un objet virtuel associé sous AWS (ces deux derniers doivent aussi utiliser les mêmes certificats pour communiquer). Nous allons illustrer la procédure :
    • Commençons par se diriger vers le menu "Objets" pour pouvoir créer notre objet virtuel :
    • L'étape suivante consiste à donner un nom à l'objet (vous pouvez laisser les autres paramètres par défaut à moins que vous voulez ajouter des TAGs à l'objet):
    • Création d'un certificat : la page suivante vous propose de créer un certificat, en effet; ce dernier va être associé à la fois : à votre objet virtuel et votre device réel (pour s'authentifier à AWS). Cliquez sur "Créer un certificat".
    • Le certificat est désormais créé, il ne faut pas oublier de l'activer (avec le bouton en bas à gauche) pour récupérer les 4 éléments (certificats) représentés en bas :
    • La dernière étape est d'attacher notre stratégie (qui contient déjà notre certificat activé) à notre objet afin d'accéder au service IoT :
    • à la fin de l'opération, l'objet virtuel est prêt :

    Serveur web sur un mote Contiki-NG

    La plupart du temps, les motes Contiki-NG communiquent en MQTT ou en HTTP. Nous apprendrons comment embarquer un serveur web sur un mote Contiki-NG.

    1. Le serveur web : Il existe déjà un serveur web implémenté dans contiki-ng/examples/rpl-border-router/. Copiez le dossier "webserver" dans votre projet.
    2. Réponse JSON : le serveur web répond par une page minimaliste HTML; mais nous allons le forcer à répondre par un objet JSON (pour pouvoir l'exploiter dans le middleware). Il suffit de remplacer le contenu de la variable http_content_type_html pour renvoyer une réponse avec header JSON :
      const char http_content_type_html[] = "Content-type: application/json\r\n\r\n"; 
    3. web_server_contiki.c : Processus main qui lance le web serveur :
      // Thread de lecture et renvoi des messages JSON
      static PT_THREAD(generate_routes(struct httpd_state *s))
      {
      char buff[80];
          PSOCK_BEGIN(&s->sout);
          sprintf(buff,"{\"temperature\":%d, \"sound_level\":%d, \"motor_rotation\":%d}",
                  getTemperatureValue(), getSoundIntensity(), getMotorRorationStatus());
          SEND_STRING(&s->sout, buff); // Retourner le json au middleware
          PSOCK_END(&s->sout);
      }
      
      /* Méthode requise par le serveur web */
      httpd_simple_script_t httpd_simple_get_script(const char *name) {
          return generate_routes;
      }
      
      PROCESS_THREAD(webserver_nogui_process, ev, data) {
          PROCESS_BEGIN();
          httpd_init(); // Lancement du serveur web
          while(1) {
              PROCESS_WAIT_EVENT_UNTIL(ev == tcpip_event);
              httpd_appcall(data);
          }
          PROCESS_END();
      }
      

    Le code complet est disponible sur : https://github.com/jugurthab/web_server_contiki.c

    Création du Middleware

    Le rôle du middleware est de permettre à Contiki-NG (limité en fonctionnalités réseau) d'interagir avec des entités externes comme les bases de données ou même les services du CLOUD.

    Le middleware est un programme à créer sur le même PC auquel est connecté votre mote Contiki-NG. Le tunnel déja créé par tunslip6 permet d'échanger entre les deux (middleware et mote). Les certificats récupérés sur AWS doivent être fournis au middleware pour l'authentification.

    L'exemple suivant illustre la manière de créer un middleware et de l'utiliser pour récupérer les données du mote Contiki-NG (configuré avec le server web) et d'envoyer le résultat à AWS (avec une connection sécurisée).

    from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
    import requests
    import json
    import time
    
    # Création d'un client MQTT, le paramètre peut être choisi
    # aléatoirement (Identifiant du client)
    myMQTTClient = AWSIoTMQTTClient("Contiki-NG-MIDDLEWARE")
    
    myMQTTClient.configureEndpoint("YOUR_ENDPOINT.iot.YOUR_REGION.amazonaws.com", 8883)
    # Ajout des certificats obtenues sur AWS lors de la
    # création de l'objet virtuel
    myMQTTClient.configureCredentials("cert/AmazonRootCA1.pem",
     "cert/cc2650-private.pem.key", "cert/cc2650-certificate.pem.crt")
    
    # Paramétrage de la configuration MQTT
    myMQTTClient.configureOfflinePublishQueueing(-1)
    myMQTTClient.configureDrainingFrequency(2)
    myMQTTClient.configureConnectDisconnectTimeout(10)
    myMQTTClient.configureMQTTOperationTimeout(5)
    
    myMQTTClient.connect() # Lancer la connection à AWS
    
    while True:
        # Récupérer les données du mote Contiki-NG    
        r = requests.get('http://[fd00::212:4b00:7b1:a885]/',
        headers={'Cache-Control': 'no-cache'})
        message_json_aws = {}
        message_json_aws['temperature'] = r.json()['temperature']
        message_json_aws['sound_level'] = r.json()['sound_level']
        message_json_aws['motor_rotation'] = r.json()['motor_rotation']
    
        messageFormattedJson = json.dumps(message_json_aws)
        # Publiation du message JSON sur le topic "/contiki-ng-cc2650"
        myMQTTClient.publish('/contiki-ng-cc2650', messageFormattedJson, 1)
    
        time.sleep(5)
    
    myMQTTClient.disconnect()
    

    Il est temps de tester nos programmes, il faut s'assurer d'avoir bien démarré votre mote Contiki-NG (et que tunslip6 tourne), puis lancez le middleware. La rubrique de "test de topic" d'AWS nous permet de visualiser le contenu des messages reçus comme le montre la figure suivante :

    Conclusion

    Dans cet article, nous avons pris en main le système Contiki-NG et découvert son fonctionnement en pratique. Nous avons également apprit à connecter un mote Contiki-NG à AWS pour pouvoir acheminer et traiter des données sur le Cloud.
    Pour aller encore plus loin sur Contiki-NG, vous pouvez consulter son wiki.

    Laisser un commentaire

    Votre adresse de messagerie ne sera pas publiée.