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 lancé OpenThread afin de rendre la technologie utilisée dans les produits Nest largement accessible aux 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 d'appareil à appareil fiable, sécurisé et à faible consommation d'énergie 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 maillés et routage maillé.

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

2a6db2e258c32237.png

Points abordés

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

Prérequis

Matériel :

  • 3 cartes de développement Nordic Semiconductor nRF52840
  • 3 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 soumis à la licence Creative Commons Attribution 3.0, et les exemples de code sont soumis à la licence Apache 2.0.

2. Premiers pas

Terminer l'atelier de programmation sur le matériel

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

  • Décrit tous les logiciels dont vous avez besoin pour créer et flasher
  • Vous apprend à créer OpenThread et à le flasher sur les cartes nordiques nRF52840
  • Présenter les principes de base d'un réseau Thread

Aucune de la configuration de l’environnement nécessaire pour compiler OpenThread et flasher les cartes n’est détaillée dans cet atelier de programmation ; seules les instructions de base pour flasher les tableaux. Nous partons du principe que vous avez déjà terminé l'atelier de programmation "Créer un réseau Thread".

Machine Linux

Cet atelier de programmation a été conçu pour utiliser une machine Linux basée sur i386 ou x86 pour flasher 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 PDK nRF52840.

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 suivi l'atelier de programmation "Créer un réseau Thread" selon les besoins, tout ce dont vous avez besoin est déjà installé. Si ce n'est pas le cas, terminez cet atelier de programmation avant de continuer pour vous assurer que vous pouvez 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 sous ./openthread/include/openthread dans le dépôt OpenThread. Ces API donnent accès à une variété de fonctionnalités OpenThread au niveau du thread et de la plateforme pour une utilisation dans vos applications:

  • Informations et contrôle sur les instances OpenThread
  • Services applicatifs tels que IPv6, UDP et CoAP
  • Gestion des identifiants réseau, et rôles "Commissaire" et "Joiner"
  • Gestion des routeurs de bordure
  • Fonctionnalités améliorées, comme la supervision des enfants et la détection des jambes

Les 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 de vos fichiers d'application. Appelez ensuite la fonction souhaitée.

Par exemple, l'application exemple CLI incluse dans 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>

L'instance OpenThread

La structure otInstance est quelque chose que vous utilisez fréquemment lorsque vous travaillez 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 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 à une plate-forme

Si vous souhaitez ajouter des fonctions spécifiques à une plateforme à l'un des exemples d'applications inclus dans 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. Ensuite, implémentez-les dans un fichier source spécifique à la plate-forme. Ainsi, vous pouvez utiliser les mêmes en-têtes de fonction pour d'autres plates-formes.

Par exemple, les fonctions GPIO que nous allons utiliser pour s'associer aux boutons et aux LED 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 les déclarations de fonction suivantes après #include pour 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 la fonction otSysButtonProcess utilise un otInstance. De cette façon, l'application peut accéder aux informations sur l'instance OpenThread lorsqu'un utilisateur appuie sur un bouton, 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 ultérieurement.

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

À l'étape précédente, nous avons passé en revue les déclarations de fonction spécifiques à la plate-forme dans ./openthread/examples/platforms/openthread-system.h, qui peuvent être utilisées pour GPIO. Pour accéder aux boutons et aux LED sur les cartes de développement nRF52840, vous devez implémenter ces fonctions pour la plate-forme nRF52840. Dans ce code, vous ajouterez 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: Ajoutez des définitions.

Ceux-ci servent d’abstraction entre les valeurs spécifiques aux 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 LED nRF52840, consultez le site Nordic Semiconductor Infocenter.

ACTION: Ajoutez des inclusions d'en-tête.

Ajoutez ensuite l'en-tête qui inclut les fonctionnalités dont vous avez 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: Ajout de 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: Ajout d'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: Ajout d'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: Ajout d'une fonction pour changer le mode d'une LED.

Cette fonction permet d'activer/de désactiver la LED4 lorsque l'appareil reçoit un message UDP de multidiffusion.

/**
 * @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: Ajout de fonctions pour initialiser et traiter les pressions sur les boutons.

La première fonction initialise la carte lorsqu'un utilisateur appuie sur un bouton, et la seconde envoie le message UDP de multidiffusion lorsque l'utilisateur appuie sur le bouton 1.

/**
 * @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 fichier gpio.c .

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

Dans notre application, nous voulons que différents voyants s'allument en fonction du rôle de l'appareil. Penchons-nous sur les rôles suivants: responsable, routeur, appareil final. Nous pouvons les attribuer à des LED comme ceci:

  • 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 LED appropriée en réponse. Nous utiliserons l'instance OpenThread pour la première partie et l'abstraction de la plateforme 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: Ajoutez des inclusions d'en-tête.

Dans la section d'inclusion 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: Ajout d'une déclaration de la fonction de gestionnaire pour le changement d'état de l'instance OpenThread.

Ajoutez cette déclaration à main.c, après que l'en-tête inclut 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 ou éteint les voyants si nécessaire.

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 la multidiffusion pour allumer un voyant LED

Dans notre application, nous souhaitons également envoyer des messages UDP à tous les autres appareils du réseau lorsque l'utilisateur appuie sur le bouton "Button1" sur une carte. Pour confirmer la réception du message, nous activerons la 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 du réseau maillé local
  • 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: Ajoutez des inclusions d'en-tête.

Dans la section d'inclusion en haut du fichier main.c, ajoutez les fichiers d'en-tête d'API dont vous avez besoin pour la fonctionnalité UDP de multidiffusion.

#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 correctement les erreurs.

ACTION: Ajout de définitions et de constantes:

Dans le fichier main.c, après la section d'inclusion et avant les instructions #if, ajoutez des constantes spécifiques à UDP et définissez:

#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 de multidiffusion du réseau maillé local. Tout message envoyé à cette adresse sera envoyé à tous les appareils Full Thread du réseau. Voir Multicast sur openthread.io pour plus d'informations sur la prise en charge de la multidiffusion dans OpenThread.

ACTION: Ajoutez des déclarations de fonction.

Dans le fichier main.c, après la définition 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: Ajout d'appels pour initialiser les voyants 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 boutons 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: Ajout d'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 le bouton a été enfoncé et, le cas échéant, appelle le gestionnaire (handleButtonInterrupt) défini à l'étape ci-dessus.

otSysButtonProcess(instance);

ACTION: Implémenter le gestionnaire d'interruption de 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 est le 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 associe le socket à l'interface réseau Thread en transmettant OT_NETIF_THREAD. Pour les autres options d'interface réseau, reportez-vous à l'énumération otNetifIdentifier de la documentation de référence de l'API UDP.

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. Ils garantissent que le message UDP est valide et correctement alloué dans le tampon. Si ce n'est pas le cas, la fonction gère correctement les erreurs en accédant au bloc exit, où il 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 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 voulons que nos appareils démarrent immédiatement Thread et se rejoignent en réseau lorsqu'ils sont allumés. Pour ce faire, nous allons utiliser la structure otOperationalDataset. Cette structure contient tous les paramètres nécessaires à la transmission des identifiants du réseau Thread à un appareil.

L'utilisation de cette structure remplacera les paramètres réseau par défaut intégrés dans OpenThread, afin de rendre notre application plus sécurisée et de limiter les nœuds Thread de notre réseau aux seuls nœuds 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: Ajoutez un en-tête à inclure.

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

#include <openthread/dataset_ftd.h>

ACTION: Ajout d'une déclaration de fonction pour définir la configuration du réseau.

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

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 réseau et la pile 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 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 détaillé dans la fonction, les paramètres réseau Thread que nous utilisons pour cette application sont les suivants:

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

En outre, c'est ici que nous réduisons la gigue de sélection du routeur, afin que nos appareils changent de rôle plus rapidement à des fins de démonstration. Notez que cela ne se fait que si le nœud est un appareil à thread complet (FTD). Nous vous donnerons plus de détails à ce sujet à l'étape suivante.

9. API: fonctions limitées

Certaines API OpenThread modifient les 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 le déploiement en production d'une application utilisant OpenThread.

Par exemple, la fonction otThreadSetRouterSelectionJitter ajuste le délai (en secondes) nécessaire à un appareil final pour se promouvoir en routeur. La valeur par défaut de cette valeur est 120, conformément à la spécification de thread. Pour faciliter l'utilisation dans cet atelier de programmation, nous allons la remplacer par 20. Vous n'aurez donc pas à attendre très longtemps pour qu'un nœud de 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 un build MTD. Nous devrons ensuite spécifier l'option CMake -DOT_MTD=OFF, sinon nous rencontrerons un échec de compilation.

Vous pouvez le vérifier en consultant 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 CMake

Avant de créer votre application, vous devez effectuer quelques mises à jour mineures pour trois fichiers CMake. Le système de compilation les utilise pour compiler et associer votre application.

./third_party/NordicSemiconductor/CMakeLists.txt

Ajoutez maintenant des indicateurs à NordicSemiconductor CMakeLists.txt 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, puis 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 fichier ./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 du pilote nrfx_gpiote.c au fichier CMakeLists.txt de NordicSemiconductor, afin qu'il soit inclus dans le build de la bibliothèque des pilotes nordiques.

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

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 à créer et à flasher l'application sur les trois cartes de développement Nordic nRF52840. Chaque appareil fonctionnera comme un appareil Full Thread (FTD).

Compiler OpenThread

Créez les binaires OpenThread FTD pour la plateforme 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 binaire de la CLI OpenThread FTD et convertissez-le au format hexadécimal avec la chaîne d'outils intégrée ARM:

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

Flasher les tableaux

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

Branchez le câble USB sur le port de débogage micro-USB situé à côté de la broche d'alimentation externe de la carte nRF52840, puis branchez-le sur votre machine Linux. Réglez correctement : la LED5 est allumée.

20a3b4b480356447.png

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

c00d519ebec7e5f0.jpeg

Naviguez jusqu'à l'emplacement des outils de ligne de commande nRFx et flashez le fichier hexadécimal OpenThread CLI FTD 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

La LED5 s'éteint brièvement pendant que le voyant clignote. Le résultat suivant est généré en cas de réussite:

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 ce message "Flashez les tableaux" pour les deux autres tableaux. Chaque carte doit être connectée à la machine Linux de la même manière et la commande pour flasher est la même, sauf pour le numéro de série de la carte. Veillez à utiliser le numéro de série unique de chaque carte dans la

nrfjprog commande flash.

Si l'opération réussit, la LED1, la LED2 ou la LED3 s'allume sur chaque carte. Vous pouvez même voir le voyant LED passer de 3 à 2 (ou de 2 à 1) peu de temps après avoir clignoter (fonctionnalité de changement de rôle de l'appareil).

12. Fonctionnement 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 possède deux fonctionnalités principales.

Indicateurs de rôle de l'appareil

Le voyant LED allumé sur chaque carte indique le rôle actuel du nœud Thread:

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

La LED allumée change à mesure que le rôle change. Vous devriez déjà avoir vu ces changements sur un ou deux tableaux en moins de 20 secondes après le démarrage de chaque appareil.

Multidiffusion UDP

Lorsque l'utilisateur appuie sur le bouton 1 sur une carte, un message UDP est envoyé à l'adresse de multidiffusion locale du réseau maillé, qui inclut tous les autres nœuds du réseau Thread. En réponse à ce message, la LED4 de toutes les autres cartes s'allume ou s'éteint. La LED4 reste allumée ou éteinte pour chaque carte jusqu'à ce qu'elle reçoive un autre message UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Démonstration: Observer les modifications apportées aux rôles des appareils

Les appareils que vous avez flashés sont d'un type spécifique d'appareil Full Thread (FTD) appelé "Router Éligible End Device (REED)". Cela signifie qu'ils peuvent fonctionner comme routeur ou comme appareil final et se promouvoir d'un appareil final vers un routeur.

Thread peut accepter jusqu'à 32 routeurs, mais tente de limiter le nombre de routeurs entre 16 et 23. Si un REED est connecté en tant qu'appareil final et que le nombre de routeurs est inférieur à 16, il est automatiquement converti en routeur. Cette modification doit se produire à un moment aléatoire pendant le nombre de secondes défini pour otThreadSetRouterSelectionJitter dans l'application (20 secondes).

Chaque réseau Thread est également associé à une variante optimale, c'est-à-dire un routeur chargé de gérer l'ensemble de routeurs dans un réseau Thread. Lorsque tous les appareils sont allumés, au bout de 20 secondes, l'un d'entre eux doit être désigné comme leader (LED1) et les deux autres doivent être des routeurs (LED2).

4e1e885861a66570.png

Supprimer le leader

Si la variante optimale est supprimée du réseau Thread, un autre routeur se classe dans cette catégorie pour s'assurer que le réseau dispose toujours d'une variante optimale.

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

4c57c87adb40e0e3.png

Réactivez le tableau de classement d'origine. Il devrait automatiquement rejoindre le réseau Thread en tant qu'appareil final (le voyant LED3 est allumé). Dans un délai de 20 secondes (gigue de sélection du routeur), le routeur se transforme en routeur (la LED2 est 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 commencer dans le rôle "Leader" (la LED1 est allumée). Le premier routeur d'un réseau Thread devient automatiquement responsable.

Dans un premier temps, les deux autres cartes se connectent au réseau en tant qu'appareils finaux (la LED3 allumée), mais doivent se connecter aux routeurs (la LED2 allumée) dans les 20 secondes.

Partitions réseau

Si vos cartes ne reçoivent pas une puissance suffisante ou si la connexion radio entre elles est faible, le réseau Thread peut se diviser en partitions et plusieurs appareils peuvent apparaître en tant que leader.

Le thread s'autorépare. Par conséquent, les partitions doivent à terme fusionner en une seule partition avec une variante optimale.

14. Démonstration: Envoyer une multidiffusion UDP

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

Choisissez n'importe quel tableau et appuyez sur le bouton1. La LED4 de toutes les autres cartes du réseau Thread exécutant l'application doit changer leur état. Si vous continuez à partir de l'exercice précédent, elles doivent maintenant être activées.

f186a2618fdbe3fd.png

Appuyez de nouveau sur le bouton 1 pour la même carte. La LED4 de tous les autres tableaux doit s'allumer à nouveau.

Appuyez sur le bouton 1 d'une autre carte et observez comment la LED4 s'active sur les autres cartes. Appuyez sur le bouton 1 de l'une des cartes où la LED4 est actuellement allumée. La LED4 reste allumée pour ce tableau, mais s'active sur les autres.

f5865ccb8ab7aa34.png

Partitions réseau

Si vos tableaux sont partitionnés et qu'ils contiennent plusieurs leader, le résultat du message multicast diffère d'un tableau à l'autre. Si vous appuyez sur le bouton 1 d'un tableau qui est partitionné (et qui est donc le seul membre du réseau Thread partitionné), la LED4 des autres cartes ne s'allumera pas en réponse. Dans ce cas, réinitialisez les cartes. Dans l'idéal, elles reformeront un seul réseau Thread et la messagerie UDP devrait fonctionner correctement.

15. Félicitations !

Vous venez de créer une application qui utilise les API OpenThread !

Vous savez désormais:

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

Étapes suivantes

Dans la continuité de cet atelier de programmation, essayez les exercices suivants:

  • Modifiez le module GPIO pour qu'il utilise des broches GPIO au lieu des voyants LED intégrés, et connectez des LED RVB externes qui changent de couleur en fonction du rôle du routeur
  • Ajouter la prise en charge de GPIO pour un autre exemple de plate-forme
  • Au lieu d'utiliser la multidiffusion pour pinguer tous les appareils en appuyant sur un bouton, utilisez l'API Router/Leader pour localiser et pinguer un appareil spécifique.
  • 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 obtenir une variété de ressources OpenThread, notamment:

Références :