1. Introduction
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.
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.
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.
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 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.
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é).
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.
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é).
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.
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.
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:
- Plates-formes compatibles : découvrez toutes les plates-formes compatibles avec OpenThread.
- Build OpenThread : informations supplémentaires sur la création et la configuration d'OpenThread
- Thread Prime : une excellente référence pour les concepts de Thread
Références :