1. Introduction
OpenThread, publié par Nest, est une implémentation Open Source du protocole réseau Thread®. Nest a lancé OpenThread pour rendre la technologie utilisée dans les produits Nest largement disponible auprès des développeurs afin d'accélérer le développement de produits pour la maison connectée.
La spécification Thread définit un protocole de communication sans fil d'appareil à appareil fiable, sécurisé et basse consommation basé sur l'IPv6 pour les applications domestiques. OpenThread implémente toutes les couches réseau Thread, y compris IPv6, 6LoWPAN, IEEE 802.15.4 avec la sécurité MAC, l'établissement de liaisons en réseau maillé et le routage en réseau maillé.
Dans cet atelier de programmation, vous allez utiliser les 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.
Points abordés
- Programmer les boutons et les LED sur les cartes de développement Nordic nRF52840
- Utiliser les API OpenThread courantes et la classe
otInstance
- Surveiller et réagir aux changements d'état OpenThread
- Envoyer des messages UDP à tous les appareils d'un réseau Thread
- Modifier des Makefiles
Prérequis
Matériel :
- 3 cartes de développement nRF52840 de Nordic Semiconductor
- 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 régis par la licence Apache 2.0.
2. Premiers pas
Suivre l'atelier de programmation sur le matériel
Avant de commencer cet atelier de programmation, vous devez suivre l'atelier Créer un réseau Thread avec des cartes nRF52840 et OpenThread, qui:
- Détail de tous les logiciels dont vous avez besoin pour la compilation et le flash
- Vous explique comment créer OpenThread et le flasher sur des cartes Nordic nRF52840
- Présente les principes de base d'un réseau Thread
Aucune configuration d'environnement requise pour créer OpenThread et flasher les cartes n'est détaillée dans cet atelier de programmation. Seules les instructions de base pour flasher les cartes sont fournies. 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 une utilisation sur une machine Linux i386 ou x86 pour le flashage de toutes les cartes de développement Thread. Toutes les étapes ont été testées sur Ubuntu 14.04.5 LTS (Trusty Tahr).
Cartes nRF52840 de Nordic Semiconductor
Cet atelier de programmation utilise trois cartes PDK nRF52840.
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 GNU ARM et divers paquets Linux. Si vous avez terminé l'atelier de programmation Créer un réseau Thread comme demandé, vous avez déjà tout ce dont vous avez besoin installé. Si ce n'est pas le cas, suivez cet atelier de programmation avant de continuer pour vous assurer de pouvoir compiler et flasher OpenThread sur les cartes de développement nRF52840.
3. Cloner le dépôt
OpenThread est fourni avec un exemple de code d'application que vous pouvez utiliser comme point de départ pour cet atelier de programmation.
Clonez le dépôt d'exemples OpenThread Nordic nRF528xx et 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
du dépôt OpenThread. Ces API permettent d'accéder à diverses fonctionnalités OpenThread au niveau de Thread et de la plate-forme pour les utiliser dans vos applications:
- Informations et commandes sur les instances OpenThread
- Services d'application tels que IPv6, UDP et CoAP
- Gestion des identifiants réseau, ainsi que des rôles de commissaire et de participant
- Gestion du routeur de bordure
- Fonctionnalités améliorées, comme la supervision parentale et la détection de brouillage
Vous trouverez des informations de référence sur toutes les API OpenThread 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 de CLI incluse avec OpenThread utilise les en-têtes d'API suivants:
./openthread/examples/apps/cli/main.c
#include <openthread/config.h> #include <openthread/cli.h> #include <openthread/diag.h> #include <openthread/tasklet.h> #include <openthread/platform/logging.h>
Instance OpenThread
La structure otInstance
est fréquemment utilisée 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 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. Implémentez-les ensuite 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 plates-formes.
Par exemple, les fonctions GPIO que nous allons utiliser pour connecter les boutons et les LED du nRF52840 doivent être déclarées dans openthread-system.h
.
Ouvrez le fichier ./openthread/examples/platforms/openthread-system.h
dans l'éditeur de texte de votre choix.
./openthread/examples/platforms/openthread-system.h
ACTION: Ajoutez des déclarations de fonction GPIO spécifiques à la plate-forme.
Ajoutez ces déclarations de fonction après #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 fonction otSysButtonProcess
utilise un otInstance
. Ainsi, l'application peut accéder aux informations sur l'instance OpenThread lorsqu'un bouton est enfoncé, si nécessaire. Tout dépend des besoins de votre application. Si vous n'en avez pas besoin dans 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
Dans 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 le GPIO. Pour accéder aux boutons et aux LED des cartes de développement nRF52840, vous devez implémenter ces fonctions pour la plate-forme nRF52840. Dans ce code, vous allez ajouter des fonctions qui:
- Initialiser les broches et les modes GPIO
- Contrôler la tension sur une broche
- Activer les interruptions GPIO et enregistrer un rappel
Dans le répertoire ./src/src
, créez un fichier nommé gpio.c
. Dans ce nouveau fichier, ajoutez le contenu suivant.
./src/src/gpio.c (nouveau fichier)
ACTION: Ajouter des définitions.
Ces définitions servent d'abstractions entre les valeurs spécifiques au 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 du nRF52840, consultez l'Infocenter Nordic Semiconductor.
ACTION: Ajoutez des inclusions d'en-tête.
Ensuite, ajoutez les en-têtes d'inclusion 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 le code suivant. La fonction in_pin1_handler
est le rappel enregistré lorsque la fonctionnalité de pression sur le bouton est initialisée (plus loin dans ce fichier).
Notez que ce rappel utilise la macro OT_UNUSED_VARIABLE
, car les variables transmises à in_pin1_handler
ne sont pas réellement utilisées dans la fonction.
/* Declaring callback function for button 1. */ static otSysButtonCallback sButtonHandler; static bool sButtonPressed; /** * @brief Function to receive interrupt and call back function * set by the application for button 1. * */ static void in_pin1_handler(uint32_t pin, nrf_gpiote_polarity_t action) { OT_UNUSED_VARIABLE(pin); OT_UNUSED_VARIABLE(action); sButtonPressed = true; }
ACTION: Ajoutez une fonction pour configurer les LED.
Ajoutez ce code pour configurer le mode et l'état de toutes les LED lors de l'initialisation.
/** * @brief Function for configuring: PIN_IN pin for input, PIN_OUT pin for output, * and configures GPIOTE to give an interrupt on pin change. */ void otSysLedInit(void) { /* Configure GPIO mode: output */ nrf_gpio_cfg_output(LED_1_PIN); nrf_gpio_cfg_output(LED_2_PIN); nrf_gpio_cfg_output(LED_3_PIN); nrf_gpio_cfg_output(LED_4_PIN); /* Clear all output first */ nrf_gpio_pin_write(LED_1_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_2_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_3_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_4_PIN, GPIO_LOGIC_LOW); /* Initialize gpiote for button(s) input. Button event handlers are set in the application (main.c) */ ret_code_t err_code; err_code = nrfx_gpiote_init(); APP_ERROR_CHECK(err_code); }
ACTION: Ajoutez une fonction pour définir le mode d'une LED.
Cette fonction est utilisée lorsque le rôle de l'appareil change.
/** * @brief Function to set the mode of an LED. */ void otSysLedSet(uint8_t aLed, bool aOn) { switch (aLed) { case 1: nrf_gpio_pin_write(LED_1_PIN, (aOn == GPIO_LOGIC_HI)); break; case 2: nrf_gpio_pin_write(LED_2_PIN, (aOn == GPIO_LOGIC_HI)); break; case 3: nrf_gpio_pin_write(LED_3_PIN, (aOn == GPIO_LOGIC_HI)); break; case 4: nrf_gpio_pin_write(LED_4_PIN, (aOn == GPIO_LOGIC_HI)); break; } }
ACTION: Ajoutez une fonction pour activer/désactiver le mode d'une LED.
Cette fonction permet d'activer/de désactiver la LED4 lorsque l'appareil reçoit 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 sur les boutons.
La première fonction initialise la carte pour une pression sur un bouton, et la seconde envoie le message UDP multicast lorsque le bouton 1 est enfoncé.
/** * @brief Function to initialize the button. */ void otSysButtonInit(otSysButtonCallback aCallback) { nrfx_gpiote_in_config_t in_config = NRFX_GPIOTE_CONFIG_IN_SENSE_LOTOHI(true); in_config.pull = NRF_GPIO_PIN_PULLUP; ret_code_t err_code; err_code = nrfx_gpiote_in_init(BUTTON_PIN, &in_config, in_pin1_handler); APP_ERROR_CHECK(err_code); sButtonHandler = aCallback; sButtonPressed = false; nrfx_gpiote_in_event_enable(BUTTON_PIN, true); } void otSysButtonProcess(otInstance *aInstance) { if (sButtonPressed) { sButtonPressed = false; sButtonHandler(aInstance); } }
ACTION: Enregistrez et fermez le fichier gpio.c
.
6. API: réagir aux modifications de rôle de l'appareil
Dans notre application, nous souhaitons que différentes LED s'allument en fonction du rôle de l'appareil. Suivons les rôles suivants: leader, routeur et appareil final. Nous pouvons les attribuer aux LED comme suit:
- LED1 = Leader
- LED2 = Routeur
- LED3 = Appareil final
Pour activer cette fonctionnalité, l'application doit savoir quand le rôle de l'appareil a changé et comment allumer la 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 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 "includes" du fichier main.c
, ajoutez les fichiers d'en-tête d'API dont vous avez besoin pour la fonctionnalité de modification de rôle.
#include <openthread/instance.h> #include <openthread/thread.h> #include <openthread/thread_ftd.h>
ACTION: Ajoutez une déclaration de fonction de gestionnaire pour le changement d'état de l'instance OpenThread.
Ajoutez cette déclaration à main.c
, après les inclusions d'en-tête et avant toute instruction #if
. Cette fonction sera définie après l'application principale.
void handleNetifStateChanged(uint32_t aFlags, void *aContext);
ACTION: Ajoutez un enregistrement de rappel pour la fonction de gestionnaire de changement d'état.
Dans main.c
, ajoutez cette fonction à la fonction main()
après l'appel otAppCliInit
. Cette inscription 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 de la modification 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 celui-ci a changé, allume/éteint les LED selon les besoins.
void handleNetifStateChanged(uint32_t aFlags, void *aContext) { if ((aFlags & OT_CHANGED_THREAD_ROLE) != 0) { otDeviceRole changedRole = otThreadGetDeviceRole(aContext); switch (changedRole) { case OT_DEVICE_ROLE_LEADER: otSysLedSet(1, true); otSysLedSet(2, false); otSysLedSet(3, false); break; case OT_DEVICE_ROLE_ROUTER: otSysLedSet(1, false); otSysLedSet(2, true); otSysLedSet(3, false); break; case OT_DEVICE_ROLE_CHILD: otSysLedSet(1, false); otSysLedSet(2, false); otSysLedSet(3, true); break; case OT_DEVICE_ROLE_DETACHED: case OT_DEVICE_ROLE_DISABLED: /* Clear LED4 if Thread is not enabled. */ otSysLedSet(4, false); break; } } }
7. API: utiliser le multicast pour allumer une LED
Dans notre application, nous souhaitons également envoyer des messages UDP à tous les autres appareils du réseau lorsque le bouton 1 est enfoncé sur une carte. Pour confirmer la réception du message, nous allons activer/désactiver la LED4 sur les autres cartes en réponse.
Pour activer cette fonctionnalité, l'application doit:
- Initialiser une connexion UDP au démarrage
- Envoyer un message UDP à l'adresse multicast locale du réseau maillé
- Gérer les messages UDP entrants
- Basculer la 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 "includes" (Inclus) en haut du fichier main.c
, ajoutez les fichiers d'en-tête d'API dont vous avez besoin pour la fonctionnalité multicast UDP.
#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: Ajoutez des définitions et des constantes:
Dans le fichier main.c
, après la section d'inclusions et avant toute instruction #if
, ajoutez des constantes et des définitions spécifiques à UDP:
#define UDP_PORT 1212 static const char UDP_DEST_ADDR[] = "ff03::1"; static const char UDP_PAYLOAD[] = "Hello OpenThread World!";
ff03::1
est l'adresse multicast locale du réseau maillé. Tous les messages envoyés à cette adresse seront envoyés à tous les appareils Thread complets du réseau. Pour en savoir plus sur la prise en charge du 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 de otTaskletsSignalPending
et avant la fonction main()
, ajoutez des fonctions spécifiques à UDP, ainsi qu'une variable statique pour représenter un socket UDP:
static void initUdp(otInstance *aInstance); static void sendUdp(otInstance *aInstance); static void handleButtonInterrupt(otInstance *aInstance); void handleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo); static otUdpSocket sUdpSocket;
ACTION: Ajoutez des appels pour initialiser les LED et le bouton GPIO.
Dans main.c
, ajoutez ces appels de fonction à la fonction main()
après l'appel otSetStateChangedCallback
. Ces fonctions initialisent les broches GPIO et GPIOTE, et définissent un gestionnaire de boutons pour gérer les événements de pression sur le bouton.
/* init GPIO LEDs and button */ otSysLedInit(); otSysButtonInit(handleButtonInterrupt);
ACTION: Ajoutez l'appel d'initialisation UDP.
Dans main.c
, ajoutez cette fonction à la fonction main()
après l'appel otSysButtonInit
que vous venez d'ajouter:
initUdp(instance);
Cet appel garantit qu'un socket UDP est initialisé au démarrage de l'application. Sans cela, l'appareil ne peut pas envoyer ni recevoir de messages UDP.
ACTION: Ajoutez un appel pour traiter l'événement du bouton GPIO.
Dans main.c
, ajoutez cet appel de fonction à la fonction main()
après l'appel otSysProcessDrivers
, dans la boucle while
. Cette fonction, déclarée dans gpio.c
, vérifie si le bouton a été enfoncé et, le cas échéant, appelle le gestionnaire (handleButtonInterrupt
) défini à l'étape précédente.
otSysButtonProcess(instance);
ACTION: Implémentez le gestionnaire d'interruption du bouton.
Dans main.c
, ajoutez l'implémentation de la fonction handleButtonInterrupt
après la fonction handleNetifStateChanged
que vous avez ajoutée à l'étape précédente:
/** * Function to handle button push event */ void handleButtonInterrupt(otInstance *aInstance) { sendUdp(aInstance); }
ACTION: Implémentez l'initialisation UDP.
Dans main.c
, ajoutez l'implémentation de la fonction initUdp
après la fonction handleButtonInterrupt
que vous venez d'ajouter:
/** * Initialize UDP socket */ void initUdp(otInstance *aInstance) { otSockAddr listenSockAddr; memset(&sUdpSocket, 0, sizeof(sUdpSocket)); memset(&listenSockAddr, 0, sizeof(listenSockAddr)); listenSockAddr.mPort = UDP_PORT; otUdpOpen(aInstance, &sUdpSocket, handleUdpReceive, aInstance); otUdpBind(aInstance, &sUdpSocket, &listenSockAddr, OT_NETIF_THREAD); }
UDP_PORT
correspond au port que vous avez défini précédemment (1212). La fonction otUdpOpen
ouvre le socket et enregistre une fonction de rappel (handleUdpReceive
) pour la réception d'un message UDP. otUdpBind
lie le socket à l'interface réseau Thread en transmettant OT_NETIF_THREAD
. Pour connaître 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
. Ils permettent de s'assurer que le message UDP est valide et correctement alloué dans le tampon. Si ce n'est pas le cas, la fonction gère correctement les erreurs en sautant vers le bloc exit
, où elle libère le tampon.
Pour en savoir plus sur les fonctions utilisées pour initialiser UDP, consultez les références IPv6 et UDP sur openthread.io.
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 souhaitons que nos appareils démarrent immédiatement Thread et se joignent à un réseau lorsqu'ils sont allumés. Pour ce faire, nous utiliserons la structure otOperationalDataset
. Cette structure contient tous les paramètres nécessaires pour transmettre les identifiants réseau Thread à un appareil.
L'utilisation de cette structure remplace les valeurs par défaut du réseau intégrées à OpenThread pour renforcer la sécurité de notre application et limiter les nœuds Thread de notre réseau à ceux qui exécutent l'application.
Ouvrez à nouveau le fichier ./openthread/examples/apps/cli/main.c
dans l'éditeur de texte de votre choix.
./openthread/examples/apps/cli/main.c
ACTION: Ajoutez une inclusion d'en-tête.
Dans la section "includes" (Inclus) en haut du fichier main.c
, ajoutez le fichier d'en-tête de l'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 les inclusions d'en-tête 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 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 de réseau Thread que nous utilisons pour cette application sont les suivants:
- Canal = 15
- PAN ID = 0x2222
- ID de PAN étendu = C0DE1AB5C0DE1AB5
- Clé réseau = 1234C0DE1AB51234C0DE1AB51234C0DE
- Nom du réseau = OTCodelab
De plus, c'est ici que nous diminuons le jitter de sélection du routeur afin que nos appareils changent de rôle plus rapidement à des fins de démonstration. Notez que cela n'est effectué que si le nœud est un appareil Thread complet (FTD). Nous y reviendrons à l'étape suivante.
9. API: fonctions limitées
Certaines des API OpenThread modifient des paramètres qui ne doivent être modifiés qu'à des fins de démonstration ou de test. Ces API ne doivent pas être utilisées dans le déploiement en production d'une application utilisant OpenThread.
Par exemple, la fonction otThreadSetRouterSelectionJitter
ajuste le temps (en secondes) nécessaire à un appareil final pour s'auto-promouvoir en tant que routeur. La valeur par défaut est 120, conformément à la spécification Thread. Pour simplifier cet atelier de programmation, nous allons le définir sur 20 afin que vous n'ayez 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 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 une erreur de compilation.
Vous pouvez le vérifier en examinant la définition de la fonction otThreadSetRouterSelectionJitter
, qui se trouve dans une directive de préprocesseur de OPENTHREAD_FTD
:
./openthread/src/core/api/thread_ftd_api.cpp
#if OPENTHREAD_FTD #include <openthread/thread_ftd.h> ... void otThreadSetRouterSelectionJitter(otInstance *aInstance, uint8_t aRouterJitter) { Instance &instance = *static_cast<Instance *>(aInstance); instance.GetThreadNetif().GetMle().SetRouterSelectionJitter(aRouterJitter); } ... #endif // OPENTHREAD_FTD
10. Mises à jour de CMake
Avant de compiler votre application, vous devez apporter quelques modifications mineures à trois fichiers CMake. Elles sont utilisées par le système de compilation pour compiler et lier 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
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
de NordicSemiconductor afin qu'il soit inclus dans la compilation de la bibliothèque des pilotes Nordic.
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 de code effectuées, vous êtes prêt à compiler et à flasher l'application sur les trois cartes de développement Nordic nRF52840. Chaque appareil fonctionnera comme un appareil Thread complet (FTD).
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 contenant le binaire de la CLI FTD OpenThread, puis convertissez-le au format hexadécimal avec l'outil de compilation ARM Embedded Toolchain:
$ cd build/bin $ arm-none-eabi-objcopy -O ihex ot-cli-ftd ot-cli-ftd.hex
Afficher les tableaux
Flashez le fichier ot-cli-ftd.hex
sur chaque carte nRF52840.
Connectez 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 sur votre machine Linux. Si vous avez correctement défini les paramètres, LED5 est allumée.
Comme précédemment, notez le numéro de série de la carte nRF52840:
Accédez à l'emplacement des outils de ligne de commande nRFx, puis flashez 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 5 s'éteint brièvement pendant le clignotement. En cas de réussite, 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 "Flasher les cartes" pour les deux autres cartes. Chaque carte doit être connectée à la machine Linux de la même manière, et la commande de flashage est la même, à 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
Commande de flashage nrfjprog
Si l'opération réussit, le voyant LED1, LED2 ou 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 le clignotement (fonctionnalité de modification du 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 comporte deux fonctionnalités principales.
Indicateurs de rôle de l'appareil
La LED allumée sur chaque carte reflète le rôle actuel du nœud Thread:
- LED1 = Leader
- LED2 = Routeur
- LED3 = Appareil final
Le voyant LED s'allume ou s'éteint en fonction du rôle. Vous devriez déjà avoir vu ces modifications sur une ou deux cartes dans les 20 secondes suivant la mise sous tension de chaque appareil.
Multicast UDP
Lorsque le bouton 1 est enfoncé sur une carte, un message UDP est envoyé à l'adresse multicast 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.
13. Démonstration: Observer les modifications de rôle des appareils
Les appareils que vous avez flashés sont un type spécifique d'appareil Thread complet (FTD) appelé appareil final éligible au routeur (REED). Cela signifie qu'ils peuvent fonctionner en tant que routeur ou appareil final, et peuvent passer d'un appareil final à un routeur.
Thread peut prendre en charge jusqu'à 32 routeurs, mais tente de maintenir le nombre de routeurs entre 16 et 23. Si un REED se connecte en tant qu'appareil final et que le nombre de routeurs est inférieur à 16, il devient automatiquement un routeur. Ce changement doit se produire à un moment aléatoire dans le délai de 20 secondes que vous avez défini pour la valeur otThreadSetRouterSelectionJitter
dans l'application.
Chaque réseau Thread dispose également d'un leader, qui est un routeur chargé de gérer l'ensemble des routeurs d'un réseau Thread. Lorsque tous les appareils sont allumés, au bout de 20 secondes, l'un d'eux doit être un leader (voyant 1 allumé) et les deux autres des routeurs (voyant 2 allumé).
Supprimer le dirigeant
Si le routeur principal est supprimé du réseau Thread, un autre routeur devient le routeur principal pour s'assurer que le réseau dispose toujours d'un routeur principal.
Éteignez le tableau des classements (celui dont la LED1 est allumée) à l'aide du bouton Marche/Arrêt. Attendez environ 20 secondes. Sur l'une des deux cartes restantes, le voyant 2 (routeur) s'éteint et le voyant 1 (leader) s'allume. Cet appareil est désormais le chef du réseau Thread.
Réactivez le classement d'origine. Il devrait automatiquement rejoindre le réseau Thread en tant qu'appareil final (la LED 3 est allumée). Dans un délai de 20 secondes (le jitter de sélection du routeur), il se promeut en routeur (le voyant 2 s'allume).
Réinitialiser les tableaux
Éteignez les trois cartes, puis rallumez-les et observez les voyants. La première carte mise sous tension doit démarrer avec le rôle de routeur principal (le voyant 1 est allumé). Le premier routeur d'un réseau Thread devient automatiquement le routeur principal.
Les deux autres cartes se connectent initialement au réseau en tant qu'appareils finaux (le voyant 3 est allumé), mais elles doivent se promouvoir en tant que routeurs (le voyant 2 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 se diviser en partitions et plusieurs appareils peuvent s'afficher comme leaders.
Le thread est auto-réparateur. Les partitions devraient donc finir par se fusionner en une seule partition avec un seul leader.
14. Démonstration: envoyer un multicast UDP
Si vous poursuivez à partir de l'exercice précédent, le voyant LED4 ne doit pas s'allumer sur aucun appareil.
Choisissez un tableau et appuyez sur le bouton 1. La LED4 de toutes les autres cartes du réseau Thread exécutant l'application doit changer d'état. Si vous poursuivez à partir de l'exercice précédent, ils devraient être activés.
Appuyez à nouveau sur le bouton 1 pour la même carte. Le voyant 4 de toutes les autres cartes devrait à nouveau clignoter.
Appuyez sur le bouton 1 d'une autre carte et observez comment la LED 4 s'allume et s'éteint sur les autres cartes. Appuyez sur le bouton 1 sur l'une des cartes où le voyant 4 est actuellement allumé. Le voyant LED4 reste allumé pour cette carte, mais s'allume et s'éteint sur les autres.
Partitions réseau
Si vos tableaux ont été partitionnés et qu'il y a plusieurs leaders parmi eux, le résultat du message multicast différera d'un tableau à l'autre. Si vous appuyez sur le bouton 1 d'une carte qui a été partitionnée (et qui est donc le seul membre du réseau Thread partitionné), le voyant 4 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 avez créé une application qui utilise les API OpenThread.
Vous savez maintenant:
- Programmer les boutons et les LED sur les cartes de développement Nordic nRF52840
- Utiliser les API OpenThread courantes et la classe
otInstance
- Surveiller et réagir aux changements d'état OpenThread
- Envoyer des messages UDP à tous les appareils d'un réseau Thread
- Modifier des Makefiles
Étapes suivantes
En complément de cet atelier de programmation, faites les exercices suivants:
- Modifier le module GPIO pour qu'il utilise des broches GPIO au lieu des LED intégrées, et connecter des LED RVB externes qui changent de couleur en fonction du rôle de routeur
- Ajout de la compatibilité avec GPIO pour un autre exemple de plate-forme
- Au lieu d'utiliser le multicast pour envoyer un ping à tous les appareils en appuyant sur un bouton, utilisez l'API Router/Leader pour localiser et envoyer un ping à un appareil spécifique.
- Connectez votre réseau maillé à Internet à l'aide d'un routeur de bordure OpenThread et diffusez-les en multicast en dehors du réseau Thread pour allumer les LED.
Documentation complémentaire
Consultez openthread.io et GitHub pour découvrir diverses ressources OpenThread, y compris les suivantes:
- Plates-formes compatibles : découvrez toutes les plates-formes compatibles avec OpenThread.
- Créer OpenThread : plus d'informations sur la création et la configuration d'OpenThread
- Présentation des threads : excellente référence sur les concepts de thread
Références :