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 publié OpenThread afin de mettre la technologie utilisée dans les produits Nest à la disposition de tous les 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 d'appareil à appareil sans fil fiable, sécurisé et basse consommation 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, Mesh Link Establishment et Mailing Routing.

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

2a6db2e258c32237.png

Points abordés

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

Ce dont vous avez besoin

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

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

Terminer 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 créer et flasher
  • Découvrir comment créer OpenThread et la flasher sur les cartes nordiques nRF52840
  • Démonstration des principes de base des réseaux Thread

Aucun des environnements de configuration d'OpenThread et de flashage des cartes n'est requis. Cet atelier de programmation ne fournit que des instructions de base pour le flashage des cartes. Nous partons du principe que vous avez déjà terminé l'atelier de programmation Build a 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.

A6693D3CE213856.png

Installer le logiciel

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

3. Cloner le dépôt

OpenThread fournit 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 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 sous ./openthread/include/openthread dans le dépôt OpenThread. Ces API permettent d'accéder à différentes fonctionnalités OpenThread au niveau du thread et de la plate-forme, et ainsi de les utiliser 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 Commissaire et Joiner
  • Gestion des routeurs de bordure
  • Fonctionnalités amélioré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>

Instance OpenThread

Vous utiliserez fréquemment la structure otInstance lorsque vous utilisez 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 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 à la plate-forme à l'un des exemples d'applications inclus 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. Ensuite, implémentez-les dans un fichier source spécifique à la plate-forme. Vous pouvez utiliser les mêmes en-têtes de fonctions pour d'autres plates-formes d'exemple.

Par exemple, les fonctions GPIO que nous allons utiliser pour s'accrocher aux boutons nRF52840 et les LED 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 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 l'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 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 voyants de la carte 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 repères et les modes GPIO
  • Contrôler la tension au niveau d'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

Ils définissent des abstractions entre les valeurs et les 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 en savoir plus sur les boutons et les voyants nRF52840, consultez le Centre d'informations Nord-Semiconductor.

ACTION: Ajouter des en-têtes.

Ajoutez ensuite l'en-tête 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: 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 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 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 voyants.

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: ajoutez une fonction pour définir le mode d'une LED.

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

/**
 * @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 permet d'activer ou de désactiver la technologie 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: ajoutez des fonctions pour initialiser et traiter les pressions sur les boutons.

La première fonction initialise la carte lorsque vous appuyez sur un bouton, et la seconde envoie le message UDP de multidiffusion lorsque vous appuyez 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ôles sur les appareils

Dans notre application, nous souhaitons que différentes LED s'allument en fonction du rôle de l'appareil. Examinons les rôles suivants : "Leader", "Router" et "End Device". Nous pouvons les attribuer à des LED comme suit:

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

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 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: Ajouter des en-têtes.

Dans la section d'inclusion du fichier main.c, ajoutez les fichiers d'en-tête d'API dont vous avez 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 l'en-tête et avant les instructions #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 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, si elle a changé, allume ou éteint 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 la multidiffusion pour allumer un voyant

Dans notre application, nous voulons également envoyer des messages UDP à tous les autres appareils du réseau lorsque l'utilisateur appuie sur un bouton sur une carte. Pour confirmer la réception du message, nous activerons 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 en mesure d'envoyer un message UDP à l'adresse multicast locale du maillage
  • 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 des en-têtes.

Dans la section d'inclusion située 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: Ajouter des définitions et des constantes:

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

#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 multicast maillé local. Tous les messages envoyés à cette adresse seront envoyés à tous les appareils Thread sur le réseau. Pour en savoir plus sur la fonctionnalité Multicast sur 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 au protocole 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 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 de transmission de 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émenter le gestionnaire d'interruption des boutons

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 lie le socket à l'interface réseau Thread en transmettant OT_NETIF_THREAD. Pour les autres options d'interface réseau, consultez l'énumération otNetifIdentifier dans 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. Vous vous assurez ainsi que le message UDP est valide et alloué correctement dans le tampon. Si ce n'est pas le cas, la fonction gère correctement les erreurs en passant au bloc exit, où elle libère le tampon.

Consultez les documentations de référence 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 permet simplement d'activer ou de désactiver 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 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 réseau Thread à un appareil.

L'utilisation de cette structure remplacera les valeurs par défaut du réseau intégrées à OpenThread pour renforcer la sécurité de notre application et limitera les nœuds Thread de notre réseau aux seuls utilisateurs exécutant 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 "include".

Dans la section d'inclusion située 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: ajoutez une déclaration de fonction pour définir la configuration réseau.

Ajoutez cette déclaration à main.c, après l'en-tête et avant les instructions #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 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 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:

  • Chaîne = 15
  • ID PAN = 0x2222
  • ID de PAN étendu = C0DE1AB5C0DE1AB5
  • Clé réseau = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Nom du réseau : OTCodelab

De plus, nous réduisez la gigue de la sélection du routeur afin que nos appareils changent 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 (appareil à thread complet). Nous y reviendrons plus en détail à l'étape suivante.

9. API: fonctions limitées

Certaines API d'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 pour déployer en production une application utilisant OpenThread.

Par exemple, la fonction otThreadSetRouterSelectionJitter ajuste le temps (en secondes) nécessaire pour qu'un appareil final se transforme en routeur. La valeur par défaut pour cette valeur est de 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'avez donc pas besoin d'attendre très longtemps pour 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 comme otThreadSetRouterSelectionJitter n'est pas incluse dans un build MTD. Nous devons spécifier ultérieurement l'option CMake -DOT_MTD=OFF, sinon un échec de compilation sera rencontré.

Pour vous en assurer, reportez-vous à 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 apporter quelques modifications mineures à 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 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, 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 de pilote nrfx_gpiote.c au fichier CMakeLists.txt NordicSemiconductor afin qu'il soit inclus dans la version 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
  ...
)
...

PCI 11. Configurer les appareils

Une fois toutes les mises à jour de code terminées, vous êtes prêt à créer l'application et à la flasher sur les trois cartes de développement nordiques nRF52840. Chaque appareil fonctionne comme un appareil Thread complet.

Compiler OpenThread

Créez 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 binaire de la CLI OpenThread FTD, puis 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.

Branchez le câble USB au port de débogage micro USB à côté de la broche d'alimentation externe de la carte nRF52840, puis branchez-le à votre machine Linux. Réglez correctement le voyant 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, puis flashez le fichier hexadécimal FTD d'OpenThread CLI 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 un flash. 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 cette étape pour les deux autres tableaux. Chaque carte doit être connectée à la machine Linux de la même manière. La commande de flashage est la même, à l'exception du numéro de série de la carte. Veillez à utiliser le numéro de série de chaque carte dans le

nrfjprog commande flash.

Si l'opération réussit, les voyants LED1, LED2 ou LED3 sont allumés sur chaque carte. Il se peut même que le voyant allumé de trois à deux (ou deux à deux) s'allume peu après le clignotement (fonctionnalité changeant de rôle).

12. Fonctionnement de l'application

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

Indicateurs de rôle de l'appareil

Le voyant allumé sur chaque carte reflète le rôle actuel du nœud Thread:

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

Tout comme le rôle des caméras, les voyants s'allument. Vous devriez déjà avoir vu ces modifications sur un tableau ou deux dans les 20 secondes qui suivent la mise sous tension de chaque appareil.

Multicast UDP

Lorsque vous appuyez sur le bouton 1 d'un tableau, un message UDP est envoyé à l'adresse multicast locale du maillage, qui inclut tous les autres nœuds du réseau Thread. En réponse à ce message, le LED4 est activé ou désactivé sur toutes les autres cartes. Celle-ci reste allumée ou éteinte pour chaque carte jusqu'à ce qu'elle reçoive un autre message UDP.

203jj094acca1f99.png

9bbd96d9b1c62666.png

13. Démonstration: Observez les changements de rôles sur les appareils

Les appareils que vous avez flashés sont un type d'appareil Thread complet (FTD, Full Thread Device) appelé appareil de fin de routeur éligible (REED). Cela signifie qu'ils peuvent servir de routeur ou d'appareil de fin et peuvent être promus d'un appareil final à un routeur.

Thread peut accepter jusqu'à 32 routeurs, mais tente de conserver un nombre de routeurs compris entre 16 et 23. Si un appareil REED est connecté en tant qu'appareil final et que le nombre de routeurs est inférieur à 16, il est automatiquement promu en tant que routeur. Cette modification doit se produire de manière aléatoire au cours du nombre de secondes pendant lequel vous définissez la valeur otThreadSetRouterSelectionJitter dans l'application (20 secondes).

Chaque réseau Thread possède également un leader, c'est-à-dire un routeur chargé de gérer l'ensemble des routeurs d'un réseau Thread. Lorsque tous les appareils sont allumés, au bout de 20 secondes, l'un d'eux doit être un leader (LED 1 activé), et les deux autres doivent être des routeurs (LED 2 activé).

4e1e885861a66570.png

Supprimer la variante optimale

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

Éteignez la carte mère (celle dont le voyant est allumé 1) à l'aide du bouton Marche/Arrêt. Patientez environ 20 secondes. Sur l'une des deux cartes restantes, le voyant LED2 (routeur) est éteint et le voyant LED (Leader) s'allume. Cet appareil est maintenant le leader du réseau Thread.

4c57c87adb40e0e3.png

Rétablissez le panneau d'origine. Elle devrait se reconnecter automatiquement au réseau Thread en tant qu'appareil final (le voyant LED3 est allumé). Dans un délai de 20 secondes (la gigue de la sélection du routeur), elle se convertit en routeur (le voyant LED2 est allumé).

5f40afca2dcc4b5b.png

Réinitialiser les tableaux

Éteignez les trois plaques, puis rallumez-les et observez les voyants. La première carte alimentée doit commencer par le rôle de leader (le voyant est éclairé) ; le premier routeur d'un réseau Thread devient automatiquement le leader.

Dans un premier temps, les deux autres cartes se connectent au réseau en tant qu'appareils finaux (le voyant LED3 est allumé), mais ils doivent se promouvoir en tant que routeurs (le voyant LED2 est allumé) dans les 20 secondes.

Partitions réseau

Si vos cartes ne reçoivent pas suffisamment d'alimentation ou si la connexion radio entre elles est faible, le réseau Thread peut être divisé en partitions et il se peut que plusieurs appareils s'affichent en tant que leader.

Thread s'auto-répare. Les partitions doivent donc à nouveau fusionner en une seule partition avec une seule variante optimale.

14. Démonstration: Envoyer une multidiffusion UDP

Si vous poursuivez l'exercice précédent, le voyant LED4 ne doit pas être allumé sur les appareils.

Sélectionnez une carte et appuyez sur Button1. L'état LED4 de toutes les autres cartes du réseau Thread exécutant l'application doit changer d'état. Si vous avez suivi l'exercice précédent, ils devraient maintenant être activés.

f186a2618fdbe3fd.png

Appuyez de nouveau sur le bouton 1 pour la même carte. Sur tous les autres tableaux, le voyant LED4 devrait s'allumer à nouveau.

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

f5865ccb8ab7aa34.png

Partitions réseau

Si vos tableaux sont partitionnés et qu'il y a plusieurs variantes principales, le résultat du message de multidiffusion différera entre les tableaux. Si vous appuyez sur Button1 sur une carte partitionnée (et donc le seul membre du réseau Thread partitionné), la LED4 des autres cartes ne s'allume pas en réponse. Le cas échéant, réinitialisez les tableaux. Dans l'idéal, 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 des cartes de développement nordiques nRF52840
  • Utiliser les API OpenThread courantes et la classe otInstance
  • Surveiller et réagir aux changements d'état d'OpenThread
  • Envoyer des messages UDP à tous les appareils d'un réseau Thread
  • Modifier des fichiers Makefile

Étapes suivantes

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

  • Modifiez le module GPIO pour utiliser des broches GPIO au lieu des LED à bord 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 la fonctionnalité Multicast pour pinguer tous les appareils à l'aide d'un bouton, utilisez l'API Router/Leader pour localiser et pinguer un appareil.
  • Connectez votre réseau maillé à Internet à l'aide d'un routeur de bordure OpenThread, et diffusez-les en dehors du réseau Thread pour allumer les voyants.

Documentation complémentaire

Rendez-vous sur openthread.io et GitHub pour accéder à diverses ressources OpenThread, y compris:

Références :