Développer avec les API OpenThread

1. Introduction

26b7f4f6b3ea0700.png

OpenThread publié par Nest est une implémentation Open Source du protocole de mise en réseau Thread®. Nest a lancé OpenThread pour permettre aux développeurs d'accéder à la technologie utilisée dans les produits Nest 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 entre appareils de type IPv6 pour les applications à domicile, qui est fiable et sécurisé. OpenThread implémente toutes les couches de mise en réseau Thread, y compris IPv6, 6LoWPAN, IEEE 802.15.4 avec sécurité MAC, Établissement des liens maillés et routage du réseau maillé.

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

2a6db2e258c32237.png

Points abordés

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

Ce dont vous avez besoin

Appareil:

  • 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

Logiciel :

  • Chaîne d'outils GNU
  • Outils de ligne de commande nordique 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, suivez l'atelier de programmation Créer un réseau Thread avec des cartes nRF52840 et un OpenThread:

  • Davantage d'informations sur les logiciels dont vous avez besoin pour créer et clignoter
  • Apprenez à créer OpenThread et à le faire clignoter sur des cartes Nordic nRF52840
  • Montre les principes de base d'un réseau Thread

Aucun environnement de configuration nécessaire pour créer OpenThread et faire clignoter les tableaux n'est détaillé dans cet atelier de programmation, seulement des instructions de base pour les flasher. Nous partons du principe que vous avez déjà terminé l'atelier de programmation sur la création d'un réseau Thread.

Machine Linux

Cet atelier de programmation a été conçu pour utiliser une machine Linux i386 ou x86 afin de lancer 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 créer et flasher OpenThread, vous devez installer SEGGER J-Link, les outils de ligne de commande nRF5x, l'ARM GNU Toolchain et divers packages Linux. Si vous avez terminé l'atelier de programmation sur la création d'un réseau Thread, vous aurez déjà tout ce dont vous avez besoin pour l'installer. Si ce n'est pas le cas, suivez cet atelier de programmation avant de continuer à créer et à flasher OpenThread sur les cartes de développement nRF52840.

3. Cloner le dépôt

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

Clonez les exemples d'OpenThread Nordic nRF528xx et créez 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 dans le dépôt OpenThread. Ces API donnent accès à diverses fonctionnalités d'OpenThread à la fois au niveau du thread et de la plate-forme pour une utilisation dans vos applications:

  • Informations et contrôle des instances OpenThread
  • Services d'application tels que IPv6, UDP et CoAP
  • Gestion des identifiants réseau, ainsi que rôles du commissaire et de la jointure
  • Gestion des routeurs de bordure
  • Fonctionnalités avancées comme la supervision des enfants et la détection de Jams

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'exemple d'application CLI inclus 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>

L'instance OpenThread

La structure otInstance est un élément 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'exemple d'application de 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'un des exemples d'application inclus avec OpenThread, déclarez-les d'abord dans l'en-tête ./openthread/examples/platforms/openthread-system.h à l'aide de l'espace de noms otSys pour toutes les fonctions. puis mettez-les en œuvre dans un fichier source spécifique à la plate-forme. De cette manière, vous pouvez utiliser les mêmes en-têtes de fonction pour d'autres exemples de plates-formes.

Par exemple, les fonctions GPIO que nous allons utiliser pour brancher les boutons nRF52840 et les voyants 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 la 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 intégrerons à l'étape suivante.

Notez que la déclaration de la fonction otSysButtonProcess utilise un otInstance. Ainsi, l'application peut accéder aux informations concernant l'instance OpenThread lorsque l'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 autour des variables inutilisées pour certaines chaînes d'outils. Nous verrons ce type d'exemple plus tard.

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 des cartes de développement nRF52840, vous devez mettre en œuvre ces fonctions pour la plate-forme nRF52840. Dans ce code, vous allez ajouter des fonctions qui:

  • Initialiser les modes et repères GPIO
  • Contrôler la tension sur une broche
  • Activer les interruptions dans 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 valeurs sont utilisées comme abstractions entre les valeurs et variables spécifiques à nRF52840 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 plus d'informations sur les boutons et les voyants nRF52840, consultez le Nordic Semiconductor Infocenter.

ACTION: L'ajout d'en-têtes est inclus.

Ajoutez ensuite l'en-tête "vous inclut" pour les fonctionnalités 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 ce code ensuite. La fonction in_pin1_handler est le rappel enregistré lorsque la fonctionnalité d'appui sur un bouton est initialisée (plus tard dans ce fichier).

Notez que ce rappel utilise la macro OT_UNUSED_VARIABLE, car les variables transmises à in_pin1_handler ne sont pas 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 permettant de configurer les voyants LED.

Ajoutez ce code pour configurer le mode et l'état de tous les voyants 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: Ajoutez 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'un voyant LED.

Cette fonction permettra d'activer/de désactiver le voyant 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 pressions de boutons.

La première fonction initialise le tableau lors d'un appui sur un bouton, et la deuxième envoie le message UDP multicast 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 gpio.cfichier.

6. API: réaction aux changements de rôle de l'appareil

Dans notre application, nous voulons que différentes LED s'allument en fonction du rôle de l'appareil. Suivez les rôles suivants: leader, routeur, appareil de fin. Nous pouvons les associer à des LED comme suit:

  • LED1 = leader
  • LED2 = Routeur
  • LED3 = Appareil de fin

Pour activer cette fonctionnalité, l'application doit être informée de la modification du rôle de l'appareil et de la procédure à suivre pour activer la LED correspondante. Nous utiliserons l'instance OpenThread pour la première partie et l'abstraction de la plate-forme GPIO pour la deuxième.

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

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

ACTION: L'ajout d'en-têtes est inclus.

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 modification du rôle.

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

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

Ajoutez cette déclaration à main.c, après l'inclusion de l'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 la modification de l'état.

Dans main.c, après la fonction main(), implémentez la fonction handleNetifStateChanged. Cette fonction vérifie l'option OT_CHANGED_THREAD_ROLE de l'instance OpenThread et, si elle a changé, active ou désactive les LED, 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 le mode multicast 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 1 sur une carte. Pour confirmer la réception du message, nous activons le voyant LED4 sur les autres tableaux.

Pour activer cette fonctionnalité, l'application doit:

  • Initialiser une connexion UDP au démarrage
  • Recevoir un message UDP à l'adresse multicast 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: L'ajout d'en-têtes est inclus.

Dans la section d'inclusion au début du fichier main.c, ajoutez les fichiers d'en-tête d'API dont vous aurez 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 efficacement les erreurs.

ACTION: Ajouter des constantes et des définitions :

Dans le fichier main.c, après la section "include" et avant toute instruction #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 multicast locale du réseau maillé. Les messages envoyés à cette adresse seront envoyés à tous les appareils Thread sur le réseau. Pour en savoir plus sur la compatibilité multicast dans OpenThread, consultez Multicast sur openthread.io.

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: 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 repères GPIO et GPIOTE et définissent un gestionnaire de boutons pour gérer les événements de transmission de boutons.

/* 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 pourra 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 le bouton a été appuyé 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 boutons

Dans main.c, ajoutez la mise en œuvre 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 du protocole UDP.

Dans main.c, ajoutez la mise en œuvre 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) lorsqu'un message UDP est reçu. otUdpBind associe le socket à l'interface réseau Thread en transmettant OT_NETIF_THREAD. Pour découvrir d'autres options d'interface réseau, consultez l'énumération otNetifIdentifier dans la documentation de référence de l'API UDP.

ACTION: Mettez en œuvre la messagerie UDP.

Dans main.c, ajoutez la mise en œuvre 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. Cela garantit que le message UDP est valide et alloué correctement dans le tampon. Sinon, la fonction gère les erreurs en passant directement 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 le traitement 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 non l'affichage 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 connectent à un 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 pour transmettre les identifiants du réseau Thread à un appareil.

L'utilisation de cette structure remplacera les paramètres par défaut du réseau intégrés à OpenThread, pour renforcer la sécurité de notre application et limiter les nœuds Thread de notre réseau aux utilisateurs 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 un en-tête "Inclure".

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

#include <openthread/dataset_ftd.h>

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

Ajoutez cette déclaration à main.c, après l'inclusion de l'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 du 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 réseau Thread que nous utilisons pour cette application sont les suivants:

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

De plus, nous avons ainsi réduit la gigue de la sélection du routeur afin de permettre à nos appareils de changer de rôle plus rapidement à des fins de démonstration. Notez que cette opération n'est effectuée que si le nœud est un appareil FTD (Full Thread Device). Nous reviendrons sur ce point à l'étape suivante.

9. API: fonctions restreintes

Certaines API OpenThread's 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 de production d'une application utilisant OpenThread.

Par exemple, la fonction otThreadSetRouterSelectionJitter ajuste la durée (en secondes) nécessaire pour qu'un appareil de fin soit promu sur un routeur. La valeur par défaut pour cette valeur est 120, selon la spécification Thread. Pour faciliter l'utilisation dans cet atelier de programmation, nous allons le remplacer par 20. Vous n'aurez donc pas à attendre très longtemps pour qu'un nœud Thread change de rôle.

Remarque: Les appareils MTD ne deviennent pas des routeurs, et une fonction telle que otThreadSetRouterSelectionJitter n'est pas compatible avec un build MTD. Plus tard, nous devons 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 se trouve dans une instruction 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 créer votre application, vous devez effectuer quelques mises à jour mineures pour 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 à 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, 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 afin d'ajouter le nouveau fichier source gpio.c:

ACTION: ajoutez la source gpio au ./src/CMakeLists.txtfichier.

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 de pilote nrfx_gpiote.c au fichier CMakeLists.txt NordicSemiconductor, de sorte qu'il soit inclus dans la version de la bibliothèque des pilotes nordiques.

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

Maintenant que toutes les mises à jour de code sont effectuées, vous pouvez créer et appliquer l'application aux trois cartes nordiques nRF52840. Chaque appareil fonctionnera comme un appareil Thread (Full Thread Device).

Compiler OpenThread

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

$ 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.

Connectez le câble USB au port de débogage USB à côté de l'orifice d'alimentation externe de la carte nRF52840, puis branchez-le sur votre ordinateur Linux. Configurez correctement le LED5.

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 insérez le fichier hexadécimal FTD de la CLI OpenThread sur la carte nRF52840 à l'aide du 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 flash. Le résultat suivant est généré:

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 cette étape." Flashez les étapes pour les deux autres tableaux. Chaque carte doit être connectée à la machine Linux de la même manière, et la commande Flash doit être identique, à l'exception du numéro de série de la carte. Assurez-vous d'utiliser le numéro de série unique de chaque carte dans le

nrfjprog Commande de flash

Si l'appareil réussit, un voyant LED1, LED2 ou LED 3 s'allumera sur chaque carte. Vous verrez peut-être le voyant LED s'allumer de 3 à 2 (ou 2 à 1) peu de temps après le flash (fonctionnalité de changement de rôle de l'appareil).

12. Fonctionnement de l'application

Les trois cartes nRF52840 doivent désormais être sous tension et exécuter notre application OpenThread. Comme expliqué précédemment, cette application possède 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 de fin

Le voyant LED s'allume en même temps que le rôle. Vous devriez avoir perçu ces modifications sur un tableau ou deux dans les 20 secondes suivant le démarrage de chaque appareil.

Multicast UDP

Lorsque vous appuyez sur le bouton 1 d'un tableau, un message UDP est envoyé à l'adresse multicast du réseau maillé, qui inclut tous les autres nœuds du réseau Thread. En réponse à ce message, la LED4 sur toutes les autres cartes s'allume ou s'éteint. La LED4 reste allumée ou éteinte pour chaque carte jusqu'à la réception d'un autre message UDP.

203dd094acca1f99.png

9bbd96d9b1c63503.png

13. Démonstration: Observez les modifications apportées au rôle de l'appareil

Les appareils que vous avez flashés sont un appareil FTD (Full Thread Device) spécifique appelé "Appareil de fin éligible au routeur" (REED). Ils peuvent donc servir de routeur ou d'appareil de bout en bout, et ils peuvent se promouvoir en tant que routeur en tant qu'appareil de fin.

Thread peut accepter jusqu'à 32 routeurs, mais tente de maintenir le nombre de routeurs entre 16 et 23. Si un REED est associé à un appareil de fin et que le nombre de routeurs est inférieur à 16, il s'affiche automatiquement en tant que routeur. Cette modification doit être effectuée au hasard, dans le délai que vous avez défini (en secondes) pour la valeur otThreadSetRouterSelectionJitter de l'application.

Chaque réseau Thread a également un leader, qui est responsable de la gestion de l'ensemble des routeurs dans un réseau Thread. Tous les appareils étant allumés, après 20 secondes, l'un doit être en position de tête (LED 1 activé) et les deux autres doivent être des routeurs (LED 2).

4e1e885861/66570.png

Supprimer la variante optimale

Si le leader est supprimé du réseau Thread, un autre routeur est promu à ce rôle, afin de s'assurer que le réseau a toujours un leader.

Éteignez le tableau principal (celui avec un éclairage LED1) à l'aide du bouton Marche/Arrêt. Attendez environ 20 secondes. Sur l'une des deux cartes restantes, le voyant LED2 (routeur) s'éteint et le voyant LED (Leader) s'allume. Cet appareil est désormais le leader du réseau Thread.

4c57c87adb40e0e3.png

Réactivez le panneau d'origine. Il devrait rejoindre automatiquement le réseau Thread en tant qu'appareil de fin (LED 3 est éclairé). Dans les 20 secondes (la gigue de la sélection du routeur), le code s'affiche en tant que routeur (le voyant LED2 est éclairé).

5f40afca2dcc4b5b.png

Réinitialiser les tableaux

Éteignez les trois cartes, puis rallumez-les et observez les voyants. La première carte qui s'exécute doit commencer par le rôle de responsable (LED1) : 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 (le voyant LED3 est éclairé), mais elles doivent communiquer sur les routeurs (le voyant LED2) est éclairé dans un délai de 20 secondes.

Partitions réseau

Si vos cartes ne sont pas suffisamment alimentées ou si la connexion radio entre elles est faible, le réseau Thread peut être divisé en plusieurs partitions, et plusieurs appareils peuvent s'afficher en tant que leaders.

Le thread est autoréparable, les partitions doivent donc fusionner avec elles en une seule partition avec un leader.

14. Démonstration: envoyer une multidiffusion UDP

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

Sélectionnez n'importe quel tableau et appuyez sur le bouton Bouton 1. Le voyant LED4 sur toutes les autres cartes du réseau Thread exécutant l'application doit changer leur état. Si vous poursuivez l'exercice précédent, ils devraient maintenant être activés.

F186A2618fdbe3fd.png

Appuyez de nouveau sur le bouton 1 pour le même tableau. Le voyant LED4 de toutes les autres cartes devrait changer.

Appuyez sur le bouton Bouton 1 sur un autre tableau et observez comment le voyant LED4 s'allume ou non sur les autres tableaux. Appuyez sur le bouton Bouton 1 sur l'une des cartes où le LED4 est allumé. Le voyant LED4 reste activé pour cette carte, mais active les autres.

F5865ccb8ab7aa34.png

Partitions réseau

Si vos tableaux sont partitionnés et s'il y a plusieurs leaders, le résultat du message de multicast sera différent entre les tableaux. Si vous appuyez sur le bouton Bouton 1 sur une carte partitionnée (et qu'il est le seul membre du réseau Thread partitionné), le voyant LED4 sur les autres cartes ne s'allumera pas en réponse. Le cas échéant, réinitialisez les tableaux (idéalement, ils réformeront un seul réseau Thread, et la messagerie UDP devrait fonctionner correctement).

15. Félicitations !

Vous avez créé une application qui utilise des API OpenThread !

Vous savez désormais:

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

Étapes suivantes

En vous appuyant sur cet atelier de programmation, essayez les exercices suivants:

  • Modifiez le module GPIO pour utiliser 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 compatibilité GPIO pour un autre exemple de plate-forme
  • Au lieu d'utiliser le multicasting pour pinguer tous les appareils à partir d'un bouton, utilisez l'API Router/Leader pour localiser et pinguer un appareil.
  • Connectez votre réseau maillé à Internet en utilisant un routeur de bordure OpenThread et diffusez-les à l'extérieur du réseau Thread pour allumer les voyants.

Documentation complémentaire

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

Références :