Développer avec les API OpenThread

1. Introduction

26b7f4f6b3ea0700.png

OpenThread, publié par Nest, est une implémentation Open Source du protocole réseau Thread®. Nest a publié OpenThread pour mettre la technologie utilisée dans les produits Nest à la disposition des développeurs afin d'accélérer le développement de produits pour la maison connectée.

La spécification Thread définit un protocole de communication sans fil fiable, sécurisé et basse consommation entre appareils basé sur IPv6 pour les applications domestiques. OpenThread implémente toutes les couches réseau Thread, y compris IPv6, 6LoWPAN, IEEE 802.15.4 avec sécurité MAC, établissement de liens Mesh et routage Mesh.

Dans cet atelier de programmation, vous allez utiliser les API OpenThread pour démarrer un réseau Thread, surveiller les changements de rôle des appareils et y réagir, et envoyer des messages UDP. Vous allez également associer ces actions à des boutons et des voyants sur du matériel réel.

2a6db2e258c32237.png

Points abordés

  • Programmer les boutons et les LED sur les cartes de développement Nordic nRF52840
  • Utiliser les API OpenThread courantes et la classe otInstance
  • Surveiller et réagir aux changements d'état OpenThread
  • Envoyer des messages UDP à tous les appareils d'un réseau Thread
  • Modifier les fichiers Makefiles

Prérequis

Matériel :

  • 3 cartes de développement Nordic Semiconductor nRF52840
  • Trois câbles USB vers micro-USB pour connecter les cartes
  • Une machine Linux avec au moins trois ports USB

Logiciels :

  • Chaîne d'outils GNU
  • Outils de ligne de commande Nordic nRF5x
  • Logiciel Segger J-Link
  • OpenThread
  • Git

Sauf indication contraire, le contenu de cet atelier de programmation est régi par la licence Creative Commons Attribution 3.0, et les exemples de code sont régis par la licence Apache 2.0.

2. Premiers pas

Suivez l'atelier de programmation sur le matériel.

Avant de commencer cet atelier de programmation, vous devez suivre l'atelier de programmation Créer un réseau Thread avec des cartes nRF52840 et OpenThread, qui :

  • Détaille tous les logiciels dont vous avez besoin pour la compilation et le flashage
  • Vous apprendrez à créer OpenThread et à l'installer sur les cartes Nordic nRF52840.
  • Présente les bases d'un réseau Thread

La configuration de l'environnement requise pour compiler OpenThread et flasher les cartes n'est pas détaillée dans cet atelier de programmation. Seules des instructions de base pour flasher les cartes sont fournies. Nous partons du principe que vous avez déjà effectué l'atelier de programmation "Créer un réseau Thread".

Machine Linux

Cet atelier de programmation a été conçu pour une utilisation sur une machine Linux i386 ou x86 pour le flashage de toutes les cartes de développement Thread. Toutes les étapes ont été testées sur Ubuntu 14.04.5 LTS (Trusty Tahr).

Cartes Nordic Semiconductor nRF52840

Cet atelier de programmation utilise trois cartes nRF52840 PDK.

a6693da3ce213856.png

Installer le logiciel

Pour compiler et flasher OpenThread, vous devez installer SEGGER J-Link, les outils de ligne de commande nRF5x, la chaîne d'outils ARM GNU et divers packages Linux. Si vous avez terminé l'atelier de programmation "Créer un réseau Thread" comme requis, vous avez déjà tout ce dont vous avez besoin. Si ce n'est pas le cas, suivez cet atelier de programmation avant de continuer pour vous assurer de pouvoir compiler et flasher OpenThread sur les cartes de développement nRF52840.

3. Cloner le dépôt

OpenThread est fourni avec un exemple de code d'application que vous pouvez utiliser comme point de départ pour cet atelier de programmation.

Clonez le dépôt d'exemples OpenThread Nordic nRF528xx et compilez OpenThread :

$ git clone --recursive https://github.com/openthread/ot-nrf528xx
$ cd ot-nrf528xx
$ ./script/bootstrap

4. Principes de base de l'API OpenThread

Les API publiques d'OpenThread se trouvent dans ./openthread/include/openthread du dépôt OpenThread. Ces API permettent d'accéder à diverses fonctionnalités OpenThread au niveau Thread et plate-forme pour une utilisation dans vos applications :

  • Informations et contrôle des instances OpenThread
  • Services d'application tels qu'IPv6, UDP et CoAP
  • Gestion des identifiants réseau, ainsi que des rôles de commissaire et de membre
  • Gestion du routeur de bordure
  • Fonctionnalités améliorées telles que la supervision des enfants et la détection des embouteillages

Des informations de référence sur toutes les API OpenThread sont disponibles sur openthread.io/reference.

Utiliser une API

Pour utiliser une API, incluez son fichier d'en-tête dans l'un des fichiers de votre application. Appelez ensuite la fonction souhaitée.

Par exemple, l'application exemple CLI incluse avec OpenThread utilise les en-têtes d'API suivants :

./openthread/examples/apps/cli/main.c

#include <openthread/config.h>
#include <openthread/cli.h>
#include <openthread/diag.h>
#include <openthread/tasklet.h>
#include <openthread/platform/logging.h>

Instance OpenThread

La structure otInstance est une structure que vous utiliserez fréquemment lorsque vous travaillerez avec les API OpenThread. Une fois initialisée, cette structure représente une instance statique de la bibliothèque OpenThread et permet à l'utilisateur d'effectuer des appels d'API OpenThread.

Par exemple, l'instance OpenThread est initialisée dans la fonction main() de l'application exemple de la CLI :

./openthread/examples/apps/cli/main.c

int main(int argc, char *argv[])
{
    otInstance *instance

...

#if OPENTHREAD_ENABLE_MULTIPLE_INSTANCES
    // Call to query the buffer size
    (void)otInstanceInit(NULL, &otInstanceBufferLength);

    // Call to allocate the buffer
    otInstanceBuffer = (uint8_t *)malloc(otInstanceBufferLength);
    assert(otInstanceBuffer);

    // Initialize OpenThread with the buffer
    instance = otInstanceInit(otInstanceBuffer, &otInstanceBufferLength);
#else
    instance = otInstanceInitSingle();
#endif

...

    return 0;
}

Fonctions spécifiques à la plate-forme

Si vous souhaitez ajouter des fonctions spécifiques à une plate-forme à l'une des applications exemples incluses avec OpenThread, déclarez-les d'abord dans l'en-tête ./openthread/examples/platforms/openthread-system.h, en utilisant l'espace de noms otSys pour toutes les fonctions. Implémentez-les ensuite dans un fichier source spécifique à la plate-forme. Cette abstraction vous permet d'utiliser les mêmes en-têtes de fonction pour d'autres plates-formes d'exemple.

Par exemple, les fonctions GPIO que nous allons utiliser pour nous connecter aux boutons et aux LED du nRF52840 doivent être déclarées dans openthread-system.h.

Ouvrez le fichier ./openthread/examples/platforms/openthread-system.h dans l'éditeur de texte de votre choix.

./openthread/examples/platforms/openthread-system.h

ACTION : Ajoutez des déclarations de fonction GPIO spécifiques à la plate-forme.

Ajoutez ces déclarations de fonction après le #include de l'en-tête openthread/instance.h :

/**
 * Init LED module.
 *
 */
void otSysLedInit(void);
void otSysLedSet(uint8_t aLed, bool aOn);
void otSysLedToggle(uint8_t aLed);

/**
* A callback will be called when GPIO interrupts occur.
*
*/
typedef void (*otSysButtonCallback)(otInstance *aInstance);
void otSysButtonInit(otSysButtonCallback aCallback);
void otSysButtonProcess(otInstance *aInstance);

Nous les implémenterons à l'étape suivante.

Notez que la déclaration de fonction otSysButtonProcess utilise un otInstance. L'application peut ainsi accéder aux informations sur l'instance OpenThread lorsqu'un bouton est enfoncé, si nécessaire. Tout dépend des besoins de votre application. Si vous n'en avez pas besoin dans votre implémentation de la fonction, vous pouvez utiliser la macro OT_UNUSED_VARIABLE de l'API OpenThread pour supprimer les erreurs de compilation liées aux variables inutilisées pour certaines chaînes d'outils. Nous verrons des exemples plus tard.

5. Implémenter l'abstraction de la plate-forme GPIO

À l'étape précédente, nous avons examiné les déclarations de fonctions spécifiques à la plate-forme dans ./openthread/examples/platforms/openthread-system.h qui peuvent être utilisées pour le GPIO. Pour accéder aux boutons et aux LED des cartes de développement nRF52840, vous devez implémenter ces fonctions pour la plate-forme nRF52840. Dans ce code, vous allez ajouter des fonctions qui :

  • Initialiser les broches et les modes GPIO
  • Contrôler la tension sur une broche
  • Activer les interruptions GPIO et enregistrer un rappel

Dans le répertoire ./src/src, créez un fichier nommé gpio.c. Dans ce nouveau fichier, ajoutez le contenu suivant.

./src/src/gpio.c (nouveau fichier)

ACTION : Ajouter des définitions.

Ces définitions servent d'abstractions entre les valeurs spécifiques à nRF52840 et les variables utilisées au niveau de l'application OpenThread.

/**
 * @file
 *   This file implements the system abstraction for GPIO and GPIOTE.
 *
 */

#define BUTTON_GPIO_PORT 0x50000300UL
#define BUTTON_PIN 11 // button #1

#define GPIO_LOGIC_HI 0
#define GPIO_LOGIC_LOW 1

#define LED_GPIO_PORT 0x50000300UL
#define LED_1_PIN 13 // turn on to indicate leader role
#define LED_2_PIN 14 // turn on to indicate router role
#define LED_3_PIN 15 // turn on to indicate child role
#define LED_4_PIN 16 // turn on to indicate UDP receive

Pour en savoir plus sur les boutons et les voyants du nRF52840, consultez l'Infocenter Nordic Semiconductor.

ACTION : Ajouter les inclusions d'en-tête.

Ensuite, ajoutez les inclusions d'en-tête dont vous aurez besoin pour la fonctionnalité GPIO.

/* Header for the functions defined here */
#include "openthread-system.h"

#include <string.h>

/* Header to access an OpenThread instance */
#include <openthread/instance.h>

/* Headers for lower-level nRF52840 functions */
#include "platform-nrf5.h"
#include "hal/nrf_gpio.h"
#include "hal/nrf_gpiote.h"
#include "nrfx/drivers/include/nrfx_gpiote.h"

ACTION : ajoutez des fonctions de rappel et d'interruption pour le bouton 1.

Ajoutez ensuite ce code. La fonction in_pin1_handler est le rappel enregistré lorsque la fonctionnalité d'appui sur le bouton est initialisée (plus loin dans ce fichier).

Notez que ce rappel utilise la macro OT_UNUSED_VARIABLE, car les variables transmises à in_pin1_handler ne sont pas réellement utilisées dans la fonction.

/* Declaring callback function for button 1. */
static otSysButtonCallback sButtonHandler;
static bool                sButtonPressed;

/**
 * @brief Function to receive interrupt and call back function
 * set by the application for button 1.
 *
 */
static void in_pin1_handler(uint32_t pin, nrf_gpiote_polarity_t action)
{
    OT_UNUSED_VARIABLE(pin);
    OT_UNUSED_VARIABLE(action);
    sButtonPressed = true;
}

ACTION : Ajoutez une fonction pour configurer les LED.

Ajoutez ce code pour configurer le mode et l'état de toutes les LED lors de l'initialisation.

/**
 * @brief Function for configuring: PIN_IN pin for input, PIN_OUT pin for output,
 * and configures GPIOTE to give an interrupt on pin change.
 */

void otSysLedInit(void)
{
    /* Configure GPIO mode: output */
    nrf_gpio_cfg_output(LED_1_PIN);
    nrf_gpio_cfg_output(LED_2_PIN);
    nrf_gpio_cfg_output(LED_3_PIN);
    nrf_gpio_cfg_output(LED_4_PIN);

    /* Clear all output first */
    nrf_gpio_pin_write(LED_1_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_2_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_3_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_4_PIN, GPIO_LOGIC_LOW);

    /* Initialize gpiote for button(s) input.
     Button event handlers are set in the application (main.c) */
    ret_code_t err_code;
    err_code = nrfx_gpiote_init();
    APP_ERROR_CHECK(err_code);
}

ACTION : Ajoute une fonction pour définir le mode d'une LED.

Cette fonction sera utilisée lorsque le rôle de l'appareil changera.

/**
 * @brief Function to set the mode of an LED.
 */

void otSysLedSet(uint8_t aLed, bool aOn)
{
    switch (aLed)
    {
    case 1:
        nrf_gpio_pin_write(LED_1_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 2:
        nrf_gpio_pin_write(LED_2_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 3:
        nrf_gpio_pin_write(LED_3_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 4:
        nrf_gpio_pin_write(LED_4_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    }
}

ACTION : Ajoutez une fonction pour activer/désactiver le mode d'une LED.

Cette fonction sera utilisée pour activer ou désactiver la LED4 lorsque l'appareil recevra un message UDP multicast.

/**
 * @brief Function to toggle the mode of an LED.
 */
void otSysLedToggle(uint8_t aLed)
{
    switch (aLed)
    {
    case 1:
        nrf_gpio_pin_toggle(LED_1_PIN);
        break;
    case 2:
        nrf_gpio_pin_toggle(LED_2_PIN);
        break;
    case 3:
        nrf_gpio_pin_toggle(LED_3_PIN);
        break;
    case 4:
        nrf_gpio_pin_toggle(LED_4_PIN);
        break;
    }
}

ACTION : Ajoutez des fonctions pour initialiser et traiter les appuis sur les boutons.

La première fonction initialise la carte pour une pression sur un bouton, et la seconde envoie le message UDP multicast lorsque le bouton 1 est enfoncé.

/**
 * @brief Function to initialize the button.
 */
void otSysButtonInit(otSysButtonCallback aCallback)
{
    nrfx_gpiote_in_config_t in_config = NRFX_GPIOTE_CONFIG_IN_SENSE_LOTOHI(true);
    in_config.pull                    = NRF_GPIO_PIN_PULLUP;

    ret_code_t err_code;
    err_code = nrfx_gpiote_in_init(BUTTON_PIN, &in_config, in_pin1_handler);
    APP_ERROR_CHECK(err_code);

    sButtonHandler = aCallback;
    sButtonPressed = false;

    nrfx_gpiote_in_event_enable(BUTTON_PIN, true);
}

void otSysButtonProcess(otInstance *aInstance)
{
    if (sButtonPressed)
    {
        sButtonPressed = false;
        sButtonHandler(aInstance);
    }
}

ACTION : Enregistrez et fermez le gpio.c .

6. API : réagir aux changements de rôle des appareils

Dans notre application, nous voulons que différentes LED s'allument en fonction du rôle de l'appareil. Suivons les rôles suivants : Leader, Routeur et Appareil final. Nous pouvons les attribuer à des LED comme suit :

  • LED1 = Leader
  • LED2 = Routeur
  • LED3 = Appareil final

Pour activer cette fonctionnalité, l'application doit savoir quand le rôle de l'appareil a changé et comment allumer la bonne LED en conséquence. Nous utiliserons l'instance OpenThread pour la première partie et l'abstraction de plate-forme GPIO pour la seconde.

Ouvrez le fichier ./openthread/examples/apps/cli/main.c dans l'éditeur de texte de votre choix.

./openthread/examples/apps/cli/main.c

ACTION : Ajouter les inclusions d'en-tête.

Dans la section "includes" du fichier main.c, ajoutez les fichiers d'en-tête d'API dont vous aurez besoin pour la fonctionnalité de changement de rôle.

#include <openthread/instance.h>
#include <openthread/thread.h>
#include <openthread/thread_ftd.h>

ACTION : Ajoutez une déclaration de fonction de gestionnaire pour le changement d'état de l'instance OpenThread.

Ajoutez cette déclaration à main.c, après les inclusions d'en-tête et avant toute instruction #if. Cette fonction sera définie après l'application principale.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

ACTION : Ajoutez un enregistrement de rappel pour la fonction de gestionnaire de changement d'état.

Dans main.c, ajoutez cette fonction à la fonction main() après l'appel otAppCliInit. Cet enregistrement de rappel indique à OpenThread d'appeler la fonction handleNetifStateChange chaque fois que l'état de l'instance OpenThread change.

/* Register Thread state change handler */
otSetStateChangedCallback(instance, handleNetifStateChanged, instance);

ACTION : Ajoutez l'implémentation du changement d'état.

Dans main.c, après la fonction main(), implémentez la fonction handleNetifStateChanged. Cette fonction vérifie l'indicateur OT_CHANGED_THREAD_ROLE de l'instance OpenThread et, s'il a changé, allume/éteint les voyants LED selon les besoins.

void handleNetifStateChanged(uint32_t aFlags, void *aContext)
{
   if ((aFlags & OT_CHANGED_THREAD_ROLE) != 0)
   {
       otDeviceRole changedRole = otThreadGetDeviceRole(aContext);

       switch (changedRole)
       {
       case OT_DEVICE_ROLE_LEADER:
           otSysLedSet(1, true);
           otSysLedSet(2, false);
           otSysLedSet(3, false);
           break;

       case OT_DEVICE_ROLE_ROUTER:
           otSysLedSet(1, false);
           otSysLedSet(2, true);
           otSysLedSet(3, false);
           break;

       case OT_DEVICE_ROLE_CHILD:
           otSysLedSet(1, false);
           otSysLedSet(2, false);
           otSysLedSet(3, true);
           break;

       case OT_DEVICE_ROLE_DETACHED:
       case OT_DEVICE_ROLE_DISABLED:
           /* Clear LED4 if Thread is not enabled. */
           otSysLedSet(4, false);
           break;
        }
    }
}

7. API : utiliser le multicast pour allumer une LED

Dans notre application, nous souhaitons également envoyer des messages UDP à tous les autres appareils du réseau lorsque le bouton 1 est enfoncé sur une carte. Pour confirmer la réception du message, nous allons activer le voyant LED4 sur les autres cartes en réponse.

Pour activer cette fonctionnalité, l'application doit :

  • Initialiser une connexion UDP au démarrage
  • Être capable d'envoyer un message UDP à l'adresse de multidiffusion locale du réseau maillé
  • Gérer les messages UDP entrants
  • Activer/Désactiver LED4 en réponse aux messages UDP entrants

Ouvrez le fichier ./openthread/examples/apps/cli/main.c dans l'éditeur de texte de votre choix.

./openthread/examples/apps/cli/main.c

ACTION : Ajouter les inclusions d'en-tête.

Dans la section "includes" en haut du fichier main.c, ajoutez les fichiers d'en-tête de l'API dont vous aurez besoin pour la fonctionnalité UDP multicast.

#include <string.h>

#include <openthread/message.h>
#include <openthread/udp.h>

#include "utils/code_utils.h"

L'en-tête code_utils.h est utilisé pour les macros otEXPECT et otEXPECT_ACTION qui valident les conditions d'exécution et gèrent les erreurs de manière fluide.

ACTION : ajoutez des définitions et des constantes :

Dans le fichier main.c, après la section des inclusions et avant toute instruction #if, ajoutez des constantes et des définitions spécifiques à UDP :

#define UDP_PORT 1212

static const char UDP_DEST_ADDR[] = "ff03::1";
static const char UDP_PAYLOAD[]   = "Hello OpenThread World!";

ff03::1 est l'adresse multicast locale du réseau maillé. Tous les messages envoyés à cette adresse seront envoyés à tous les appareils Full Thread du réseau. Pour en savoir plus sur la compatibilité multicast dans OpenThread, consultez Multicast on openthread.io.

ACTION : ajoutez des déclarations de fonctions.

Dans le fichier main.c, après la définition de otTaskletsSignalPending et avant la fonction main(), ajoutez des fonctions spécifiques à UDP, ainsi qu'une variable statique pour représenter un socket UDP :

static void initUdp(otInstance *aInstance);
static void sendUdp(otInstance *aInstance);

static void handleButtonInterrupt(otInstance *aInstance);

void handleUdpReceive(void *aContext, otMessage *aMessage, 
                      const otMessageInfo *aMessageInfo);

static otUdpSocket sUdpSocket;

ACTION : Ajoutez des appels pour initialiser les LED et le bouton GPIO.

Dans main.c, ajoutez ces appels de fonction à la fonction main() après l'appel otSetStateChangedCallback. Ces fonctions initialisent les broches GPIO et GPIOTE, et définissent un gestionnaire de bouton pour gérer les événements d'appui sur le bouton.

/* init GPIO LEDs and button */
otSysLedInit();
otSysButtonInit(handleButtonInterrupt);

ACTION : Ajoutez l'appel d'initialisation UDP.

Dans main.c, ajoutez cette fonction à la fonction main() après l'appel otSysButtonInit que vous venez d'ajouter :

initUdp(instance);

Cet appel garantit qu'un socket UDP est initialisé au démarrage de l'application. Sans cela, l'appareil ne peut pas envoyer ni recevoir de messages UDP.

ACTION : Ajoutez un appel pour traiter l'événement du bouton GPIO.

Dans main.c, ajoutez cet appel de fonction à la fonction main() après l'appel otSysProcessDrivers, dans la boucle while. Cette fonction, déclarée dans gpio.c, vérifie si l'utilisateur a appuyé sur le bouton et, le cas échéant, appelle le gestionnaire (handleButtonInterrupt) défini à l'étape ci-dessus.

otSysButtonProcess(instance);

ACTION : Implémentez le gestionnaire d'interruption du bouton.

Dans main.c, ajoutez l'implémentation de la fonction handleButtonInterrupt après la fonction handleNetifStateChanged que vous avez ajoutée à l'étape précédente :

/**
 * Function to handle button push event
 */
void handleButtonInterrupt(otInstance *aInstance)
{
    sendUdp(aInstance);
}

ACTION : Implémentez l'initialisation UDP.

Dans main.c, ajoutez l'implémentation de la fonction initUdp après la fonction handleButtonInterrupt que vous venez d'ajouter :

/**
 * Initialize UDP socket
 */
void initUdp(otInstance *aInstance)
{
    otSockAddr  listenSockAddr;

    memset(&sUdpSocket, 0, sizeof(sUdpSocket));
    memset(&listenSockAddr, 0, sizeof(listenSockAddr));

    listenSockAddr.mPort    = UDP_PORT;

    otUdpOpen(aInstance, &sUdpSocket, handleUdpReceive, aInstance);
    otUdpBind(aInstance, &sUdpSocket, &listenSockAddr, OT_NETIF_THREAD);
}

UDP_PORT correspond au port que vous avez défini précédemment (1212). La fonction otUdpOpen ouvre le socket et enregistre une fonction de rappel (handleUdpReceive) pour la réception d'un message UDP. otUdpBind lie le socket à l'interface réseau Thread en transmettant OT_NETIF_THREAD. Pour d'autres options d'interface réseau, consultez l'énumération dans la documentation de référence de l'API UDP.otNetifIdentifier

ACTION : Implémentez la messagerie UDP.

Dans main.c, ajoutez l'implémentation de la fonction sendUdp après la fonction initUdp que vous venez d'ajouter :

/**
 * Send a UDP datagram
 */
void sendUdp(otInstance *aInstance)
{
    otError       error = OT_ERROR_NONE;
    otMessage *   message;
    otMessageInfo messageInfo;
    otIp6Address  destinationAddr;

    memset(&messageInfo, 0, sizeof(messageInfo));

    otIp6AddressFromString(UDP_DEST_ADDR, &destinationAddr);
    messageInfo.mPeerAddr    = destinationAddr;
    messageInfo.mPeerPort    = UDP_PORT;

    message = otUdpNewMessage(aInstance, NULL);
    otEXPECT_ACTION(message != NULL, error = OT_ERROR_NO_BUFS);

    error = otMessageAppend(message, UDP_PAYLOAD, sizeof(UDP_PAYLOAD));
    otEXPECT(error == OT_ERROR_NONE);

    error = otUdpSend(aInstance, &sUdpSocket, message, &messageInfo);

 exit:
    if (error != OT_ERROR_NONE && message != NULL)
    {
        otMessageFree(message);
    }
}

Notez les macros otEXPECT et otEXPECT_ACTION. Ces éléments permettent de s'assurer que le message UDP est valide et correctement alloué dans le tampon. Si ce n'est pas le cas, la fonction gère les erreurs de manière fluide en passant au bloc exit, où elle libère le tampon.

Consultez les références IPv6 et UDP sur openthread.io pour en savoir plus sur les fonctions utilisées pour initialiser UDP.

ACTION : Implémentez la gestion des messages UDP.

Dans main.c, ajoutez l'implémentation de la fonction handleUdpReceive après la fonction sendUdp que vous venez d'ajouter. Cette fonction active ou désactive simplement la LED4.

/**
 * Function to handle UDP datagrams received on the listening socket
 */
void handleUdpReceive(void *aContext, otMessage *aMessage,
                      const otMessageInfo *aMessageInfo)
{
    OT_UNUSED_VARIABLE(aContext);
    OT_UNUSED_VARIABLE(aMessage);
    OT_UNUSED_VARIABLE(aMessageInfo);

    otSysLedToggle(4);
}

8. API : configurer le réseau Thread

Pour faciliter la démonstration, nous souhaitons que nos appareils démarrent immédiatement Thread et rejoignent un réseau lorsqu'ils sont allumés. Pour ce faire, nous utiliserons la structure otOperationalDataset. Cette structure contient tous les paramètres nécessaires pour transmettre les identifiants du réseau Thread à un appareil.

L'utilisation de cette structure remplacera les paramètres réseau par défaut intégrés à OpenThread, afin de rendre notre application plus sécurisée et de limiter les nœuds Thread de notre réseau à ceux qui exécutent l'application.

Ouvrez à nouveau le fichier ./openthread/examples/apps/cli/main.c dans l'éditeur de texte de votre choix.

./openthread/examples/apps/cli/main.c

ACTION : Ajouter l'inclusion d'en-tête.

Dans la section des inclusions en haut du fichier main.c, ajoutez le fichier d'en-tête de l'API dont vous aurez besoin pour configurer le réseau Thread :

#include <openthread/dataset_ftd.h>

ACTION : Ajoute une déclaration de fonction pour définir la configuration réseau.

Ajoutez cette déclaration à main.c, après les inclusions d'en-tête et avant toute instruction #if. Cette fonction sera définie après la fonction d'application principale.

static void setNetworkConfiguration(otInstance *aInstance);

ACTION : Ajoutez l'appel de configuration réseau.

Dans main.c, ajoutez cet appel de fonction à la fonction main() après l'appel otSetStateChangedCallback. Cette fonction configure l'ensemble de données du réseau Thread.

/* Override default network credentials */
setNetworkConfiguration(instance);

ACTION : Ajoutez des appels pour activer l'interface et la pile réseau Thread.

Dans main.c, ajoutez ces appels de fonction à la fonction main() après l'appel otSysButtonInit.

/* Start the Thread network interface (CLI cmd > ifconfig up) */
otIp6SetEnabled(instance, true);

/* Start the Thread stack (CLI cmd > thread start) */
otThreadSetEnabled(instance, true);

ACTION : Implémentez la configuration du réseau Thread.

Dans main.c, ajoutez l'implémentation de la fonction setNetworkConfiguration après la fonction main() :

/**
 * Override default network settings, such as panid, so the devices can join a
 network
 */
void setNetworkConfiguration(otInstance *aInstance)
{
    static char          aNetworkName[] = "OTCodelab";
    otOperationalDataset aDataset;

    memset(&aDataset, 0, sizeof(otOperationalDataset));

    /*
     * Fields that can be configured in otOperationDataset to override defaults:
     *     Network Name, Mesh Local Prefix, Extended PAN ID, PAN ID, Delay Timer,
     *     Channel, Channel Mask Page 0, Network Key, PSKc, Security Policy
     */
    aDataset.mActiveTimestamp.mSeconds             = 1;
    aDataset.mActiveTimestamp.mTicks               = 0;
    aDataset.mActiveTimestamp.mAuthoritative       = false;
    aDataset.mComponents.mIsActiveTimestampPresent = true;

    /* Set Channel to 15 */
    aDataset.mChannel                      = 15;
    aDataset.mComponents.mIsChannelPresent = true;

    /* Set Pan ID to 2222 */
    aDataset.mPanId                      = (otPanId)0x2222;
    aDataset.mComponents.mIsPanIdPresent = true;

    /* Set Extended Pan ID to C0DE1AB5C0DE1AB5 */
    uint8_t extPanId[OT_EXT_PAN_ID_SIZE] = {0xC0, 0xDE, 0x1A, 0xB5, 0xC0, 0xDE, 0x1A, 0xB5};
    memcpy(aDataset.mExtendedPanId.m8, extPanId, sizeof(aDataset.mExtendedPanId));
    aDataset.mComponents.mIsExtendedPanIdPresent = true;

    /* Set network key to 1234C0DE1AB51234C0DE1AB51234C0DE */
    uint8_t key[OT_NETWORK_KEY_SIZE] = {0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE};
    memcpy(aDataset.mNetworkKey.m8, key, sizeof(aDataset.mNetworkKey));
    aDataset.mComponents.mIsNetworkKeyPresent = true;

    /* Set Network Name to OTCodelab */
    size_t length = strlen(aNetworkName);
    assert(length <= OT_NETWORK_NAME_MAX_SIZE);
    memcpy(aDataset.mNetworkName.m8, aNetworkName, length);
    aDataset.mComponents.mIsNetworkNamePresent = true;

    otDatasetSetActive(aInstance, &aDataset);
    /* Set the router selection jitter to override the 2 minute default.
       CLI cmd > routerselectionjitter 20
       Warning: For demo purposes only - not to be used in a real product */
    uint8_t jitterValue = 20;
    otThreadSetRouterSelectionJitter(aInstance, jitterValue);
}

Comme indiqué dans la fonction, les paramètres du réseau Thread que nous utilisons pour cette application sont les suivants :

  • Canal = 15
  • PAN ID = 0x2222
  • Identifiant PAN étendu = C0DE1AB5C0DE1AB5
  • Clé du réseau = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Network Name = OTCodelab

C'est également ici que nous réduisons le jitter de sélection du routeur, afin que nos appareils changent de rôle plus rapidement à des fins de démonstration. Notez que cela n'est fait que si le nœud est un FTD (Full Thread Device). Nous y reviendrons dans la prochaine étape.

9. API : fonctions restreintes

Certaines API OpenThread modifient des paramètres qui ne doivent être modifiés qu'à des fins de démonstration ou de test. Ces API ne doivent pas être utilisées dans un déploiement de production d'une application utilisant OpenThread.

Par exemple, la fonction otThreadSetRouterSelectionJitter ajuste le temps (en secondes) nécessaire à un appareil final pour se promouvoir en routeur. La valeur par défaut est de 120, conformément à la spécification Thread. Pour faciliter l'utilisation dans cet atelier de programmation, nous allons le remplacer par 20 afin que vous n'ayez pas à attendre trop longtemps qu'un nœud Thread change de rôle.

Remarque : Les appareils MTD ne deviennent pas des routeurs, et la prise en charge d'une fonction telle que otThreadSetRouterSelectionJitter n'est pas incluse dans une version MTD. Nous devrons spécifier l'option CMake -DOT_MTD=OFF ultérieurement, sinon la compilation échouera.

Vous pouvez le vérifier en examinant la définition de la fonction otThreadSetRouterSelectionJitter, qui est contenue dans une directive de préprocesseur de OPENTHREAD_FTD :

./openthread/src/core/api/thread_ftd_api.cpp

#if OPENTHREAD_FTD

#include <openthread/thread_ftd.h>

...

void otThreadSetRouterSelectionJitter(otInstance *aInstance, uint8_t aRouterJitter)
{
    Instance &instance = *static_cast<Instance *>(aInstance);

    instance.GetThreadNetif().GetMle().SetRouterSelectionJitter(aRouterJitter);
}

...

#endif // OPENTHREAD_FTD

10. Mises à jour de CMake

Avant de compiler votre application, vous devez apporter quelques modifications mineures à trois fichiers CMake. Elles sont utilisées par le système de compilation pour compiler et associer votre application.

./third_party/NordicSemiconductor/CMakeLists.txt

Ajoutez maintenant des indicateurs au CMakeLists.txt NordicSemiconductor pour vous assurer que les fonctions GPIO sont définies dans l'application.

ACTION : Ajoutez des indicateurs au fichier CMakeLists.txt.

Ouvrez ./third_party/NordicSemiconductor/CMakeLists.txt dans l'éditeur de texte de votre choix et ajoutez les lignes suivantes dans la section COMMON_FLAG.

...
set(COMMON_FLAG
    -DSPIS_ENABLED=1
    -DSPIS0_ENABLED=1
    -DNRFX_SPIS_ENABLED=1
    -DNRFX_SPIS0_ENABLED=1
    ...

    # Defined in ./third_party/NordicSemiconductor/nrfx/templates/nRF52840/nrfx_config.h
    -DGPIOTE_ENABLED=1
    -DGPIOTE_CONFIG_IRQ_PRIORITY=7
    -DGPIOTE_CONFIG_NUM_OF_LOW_POWER_EVENTS=1
)

...

./src/CMakeLists.txt

Modifiez le fichier ./src/CMakeLists.txt pour ajouter le nouveau fichier source gpio.c :

ACTION : Ajoutez la source gpio au ./src/CMakeLists.txt .

Ouvrez ./src/CMakeLists.txt dans l'éditeur de texte de votre choix, puis ajoutez le fichier à la section NRF_COMM_SOURCES.

...

set(NRF_COMM_SOURCES
  ...
  src/gpio.c
  ...
)

...

./third_party/NordicSemiconductor/CMakeLists.txt

Enfin, ajoutez le fichier pilote nrfx_gpiote.c au fichier CMakeLists.txt NordicSemiconductor pour qu'il soit inclus dans la compilation de la bibliothèque des pilotes Nordic.

ACTION : Ajoutez le pilote gpio au fichier NordicSemiconductor CMakeLists.txt.

Ouvrez ./third_party/NordicSemiconductor/CMakeLists.txt dans l'éditeur de texte de votre choix, puis ajoutez le fichier à la section COMMON_SOURCES.

...

set(COMMON_SOURCES
  ...
  nrfx/drivers/src/nrfx_gpiote.c
  ...
)
...

11. Configurer les appareils

Une fois toutes les mises à jour du code effectuées, vous êtes prêt à compiler et à flasher l'application sur les trois cartes de développement Nordic nRF52840. Chaque appareil fonctionnera comme un appareil Thread complet (FTD, Full Thread Device).

Compiler OpenThread

Compilez les binaires OpenThread FTD pour la plate-forme nRF52840.

$ cd ~/ot-nrf528xx
$ ./script/build nrf52840 UART_trans -DOT_MTD=OFF -DOT_APP_RCP=OFF -DOT_RCP=OFF

Accédez au répertoire contenant le fichier binaire de la CLI OpenThread FTD et convertissez-le au format hexadécimal avec la chaîne d'outils ARM Embedded :

$ cd build/bin
$ arm-none-eabi-objcopy -O ihex ot-cli-ftd ot-cli-ftd.hex

Flasher les cartes

Flashez le fichier ot-cli-ftd.hex sur chaque carte nRF52840.

Branchez le câble USB au port de débogage micro-USB à côté du connecteur d'alimentation externe sur la carte nRF52840, puis branchez-le sur votre machine Linux. Si vous avez défini correctement le paramètre, le voyant LED5 est allumé.

20a3b4b480356447.png

Comme précédemment, notez le numéro de série de la carte nRF52840 :

c00d519ebec7e5f0.jpeg

Accédez à l'emplacement des outils de ligne de commande nRFx et flashez le fichier hexadécimal FTD de la CLI OpenThread sur la carte nRF52840, en utilisant le numéro de série de la carte :

$ cd ~/nrfjprog
$ ./nrfjprog -f nrf52 -s 683704924 --verify --chiperase --program \
       ~/openthread/output/nrf52840/bin/ot-cli-ftd.hex --reset

Le voyant LED5 s'éteint brièvement pendant le clignotement. Le résultat suivant est généré en cas de succès :

Parsing hex file.
Erasing user available code and UICR flash areas.
Applying system reset.
Checking that the area to write is not protected.
Programing device.
Applying system reset.
Run.

Répétez l'étape "Flasher les cartes" pour les deux autres cartes. Chaque carte doit être connectée à la machine Linux de la même manière, et la commande pour flasher est la même, à l'exception du numéro de série de la carte. Veillez à utiliser le numéro de série unique de chaque carte dans le

nrfjprog commande de programmation.

Si l'opération réussit, les voyants LED1, LED2 ou LED3 s'allument sur chaque carte. Vous verrez peut-être même le voyant LED passer de 3 à 2 (ou de 2 à 1) peu de temps après le clignotement (fonctionnalité de changement de rôle de l'appareil).

12. Fonctionnalités de l'application

Les trois cartes nRF52840 devraient maintenant être alimentées et exécuter notre application OpenThread. Comme indiqué précédemment, cette application comporte deux fonctionnalités principales.

Indicateurs de rôle de l'appareil

La LED allumée sur chaque carte reflète le rôle actuel du nœud Thread :

  • LED1 = Leader
  • LED2 = Routeur
  • LED3 = Appareil final

Le voyant LED allumé change en fonction du rôle. Vous devriez déjà avoir constaté ces modifications sur un ou deux tableaux dans les 20 secondes suivant l'allumage de chaque appareil.

Multicast UDP

Lorsque le bouton 1 est enfoncé sur une carte, un message UDP est envoyé à l'adresse multicast locale au maillage, qui inclut tous les autres nœuds du réseau Thread. Lorsque ce message est reçu, le voyant LED4 de toutes les autres cartes s'allume ou s'éteint. Le voyant LED4 reste allumé ou éteint pour chaque carte jusqu'à ce qu'elle reçoive un autre message UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Démonstration : observer les changements de rôle des appareils

Les appareils que vous avez flashés sont un type spécifique d'appareil Thread complet (FTD, Full Thread Device) appelé "Router Eligible End Device" (REED). Cela signifie qu'ils peuvent fonctionner comme routeur ou comme appareil final, et qu'ils peuvent passer d'un appareil final à un routeur.

Thread peut prendre en charge jusqu'à 32 routeurs, mais essaie de maintenir le nombre de routeurs entre 16 et 23. Si un REED se connecte en tant qu'appareil final et que le nombre de routeurs est inférieur à 16, il se promeut automatiquement en routeur. Ce changement doit se produire à un moment aléatoire dans le nombre de secondes que vous avez défini pour la valeur otThreadSetRouterSelectionJitter dans l'application (20 secondes).

Chaque réseau Thread dispose également d'un leader, qui est un routeur chargé de gérer l'ensemble des routeurs d'un réseau Thread. Une fois tous les appareils allumés, l'un d'eux devrait être un leader (LED1 allumée) et les deux autres des routeurs (LED2 allumée) au bout de 20 secondes.

4e1e885861a66570.png

Supprimer le responsable

Si le leader est supprimé du réseau Thread, un autre routeur se promeut en tant que leader pour s'assurer que le réseau en possède toujours un.

Éteignez la carte Leader (celle dont la LED1 est allumée) à l'aide du bouton Power. Patientez environ 20 secondes. Sur l'une des deux cartes restantes, le voyant LED2 (routeur) s'éteint et le voyant LED1 (leader) s'allume. Cet appareil est désormais le leader du réseau Thread.

4c57c87adb40e0e3.png

Réactivez le classement d'origine. Il devrait se reconnecter automatiquement au réseau Thread en tant qu'appareil final (la LED 3 est allumée). Dans les 20 secondes (le jitter de sélection du routeur), il se promeut en routeur (LED2 allumée).

5f40afca2dcc4b5b.png

Réinitialiser les tableaux

Éteignez les trois cartes, puis rallumez-les et observez les voyants. La première carte mise sous tension doit démarrer en tant que leader (LED1 allumée). Le premier routeur d'un réseau Thread devient automatiquement le leader.

Les deux autres cartes se connectent initialement au réseau en tant qu'appareils finaux (LED3 allumée), mais devraient se promouvoir en routeurs (LED2 allumée) dans les 20 secondes.

Partitions réseau

Si vos cartes ne reçoivent pas suffisamment d'énergie ou si la connexion radio entre elles est faible, le réseau Thread peut se diviser en partitions et plusieurs appareils peuvent s'afficher comme leaders.

Thread est auto-réparateur. Les partitions devraient donc finir par fusionner en une seule partition avec un seul leader.

14. Démonstration : Envoyer un multicast UDP

Si vous continuez à partir de l'exercice précédent, le voyant LED4 ne doit être allumé sur aucun appareil.

Sélectionnez un tableau et appuyez sur le bouton 1. Le voyant LED4 de toutes les autres cartes du réseau Thread exécutant l'application doit changer d'état. Si vous continuez à partir de l'exercice précédent, ils devraient maintenant être activés.

f186a2618fdbe3fd.png

Appuyez à nouveau sur le bouton 1 pour la même carte. Le voyant LED4 de toutes les autres cartes devrait de nouveau clignoter.

Appuyez sur Button1 sur une autre carte et observez comment LED4 bascule sur les autres cartes. Appuyez sur le bouton 1 de l'une des cartes sur lesquelles la LED 4 est actuellement allumée. Le voyant LED4 reste allumé pour cette carte, mais clignote sur les autres.

f5865ccb8ab7aa34.png

Partitions réseau

Si vos tableaux sont partitionnés et qu'il y a plusieurs responsables parmi eux, le résultat du message multidiffusion sera différent selon les tableaux. Si vous appuyez sur le bouton 1 d'une carte partitionnée (et qui est donc le seul membre du réseau Thread partitionné), le voyant LED4 des autres cartes ne s'allumera pas en réponse. Si cela se produit, réinitialisez les cartes. Idéalement, elles reformeront un seul réseau Thread et la messagerie UDP devrait fonctionner correctement.

15. Félicitations !

Vous avez créé une application qui utilise les API OpenThread.

Vous savez maintenant :

  • Programmer les boutons et les LED sur les cartes de développement Nordic nRF52840
  • Utiliser les API OpenThread courantes et la classe otInstance
  • Surveiller et réagir aux changements d'état OpenThread
  • Envoyer des messages UDP à tous les appareils d'un réseau Thread
  • Modifier les fichiers Makefiles

Étapes suivantes

En complément de cet atelier de programmation, faites ces exercices :

  • Modifiez le module GPIO pour utiliser les broches GPIO au lieu des LED intégrées, et connectez des LED RVB externes qui changent de couleur en fonction du rôle du routeur.
  • Ajouter la prise en charge GPIO pour une autre plate-forme d'exemple
  • Au lieu d'utiliser le multicast pour envoyer un ping à tous les appareils en appuyant sur un bouton, utilisez l'API Router/Leader pour localiser un appareil individuel et lui envoyer un ping.
  • Connectez votre réseau maillé à Internet à l'aide d'un routeur de bordure OpenThread et diffusez-les en multicast depuis l'extérieur du réseau Thread pour allumer les voyants.

Documentation complémentaire

Consultez openthread.io et GitHub pour accéder à diverses ressources OpenThread, y compris :

Références :