Sviluppo con le API OpenThread

Informazioni su questo codelab
schedule60 minuti
subjectUltimo aggiornamento: 5 maggio 2025
account_circleScritto da: Jeff Bumgardner

1. Introduzione

26b7f4f6b3ea0700.png

OpenThread rilasciato da Nest è un'implementazione open source del protocollo di rete Thread®. Nest ha rilasciato OpenThread per rendere la tecnologia utilizzata nei prodotti Nest ampiamente disponibile per gli sviluppatori, in modo da accelerare lo sviluppo di prodotti per la casa connessa a internet.

La specifica Thread definisce un protocollo di comunicazione tra dispositivi wireless affidabile, sicuro e a basso consumo basato su IPv6 per le applicazioni per la casa. OpenThread implementa tutti i livelli di rete Thread, tra cui IPv6, 6LoWPAN, IEEE 802.15.4 con sicurezza MAC, Mesh Link Establishment e Mesh Routing.

In questo codelab, utilizzerai le API OpenThread per avviare una rete Thread, monitorare e reagire alle modifiche dei ruoli dei dispositivi, inviare messaggi UDP e associare queste azioni a pulsanti e LED su hardware reale.

2a6db2e258c32237.png

Obiettivi didattici

  • Come programmare i pulsanti e i LED sulle schede di sviluppo Nordic nRF52840
  • Come utilizzare le API OpenThread comuni e la classe otInstance
  • Come monitorare e reagire alle modifiche dello stato di OpenThread
  • Come inviare messaggi UDP a tutti i dispositivi di una rete Thread
  • Come modificare i file Makefile

Che cosa ti serve

Hardware:

  • 3 schede di sviluppo Nordic Semiconductor nRF52840
  • 3 cavi USB a micro USB per collegare le schede
  • Un computer Linux con almeno 3 porte USB

Software:

  • GNU Toolchain
  • Strumenti a riga di comando Nordic nRF5x
  • Software Segger J-Link
  • OpenThread
  • Git

Salvo diverse indicazioni, i contenuti di questo Codelab sono concessi in licenza ai sensi della licenza Creative Commons Attribution 3.0, mentre gli esempi di codice sono concessi in licenza ai sensi della licenza Apache 2.0.

2. Per iniziare

Completa il codelab sull'hardware

Prima di iniziare questo codelab, devi completare il codelab Crea una rete Thread con schede nRF52840 e OpenThread, che:

  • Dettagli su tutto il software necessario per la compilazione e il flashing
  • Ti insegna a creare OpenThread e a eseguirlo su schede Nordic nRF52840
  • Mostra le nozioni di base di una rete Thread

In questo Codelab non viene descritto nessuno degli ambienti di configurazione necessari per compilare OpenThread e eseguire il flashing delle schede, ma solo le istruzioni di base per il flashing delle schede. Si presume che tu abbia già completato il codelab Crea una rete Thread.

Macchina Linux

Questo Codelab è stato progettato per utilizzare una macchina Linux basata su i386 o x86 per eseguire il flashing di tutte le schede di sviluppo Thread. Tutti i passaggi sono stati testati su Ubuntu 14.04.5 LTS (Trusty Tahr).

Schede Nordic Semiconductor nRF52840

Questo codelab utilizza tre schede PDK nRF52840.

a6693da3ce213856.png

Installa software

Per compilare e eseguire il flashing di OpenThread, devi installare SEGGER J-Link, gli strumenti a riga di comando nRF5x, la suite ARM GNU Toolchain e vari pacchetti Linux. Se hai completato il Codelab Crea una rete Thread come richiesto, avrai già installato tutto ciò che ti serve. In caso contrario, completa il Codelab prima di continuare per assicurarti di poter compilare e eseguire il flashing di OpenThread sulle schede di sviluppo nRF52840.

3. Clona il repository

OpenThread è dotato di codice di applicazione di esempio che puoi utilizzare come punto di partenza per questo Codelab.

Clona il repository di esempi OpenThread Nordic nRF528xx e compila OpenThread:

$ git clone --recursive https://github.com/openthread/ot-nrf528xx
$ cd ot-nrf528xx
$ ./script/bootstrap

4. Nozioni di base sull'API OpenThread

Le API pubbliche di OpenThread si trovano all'indirizzo ./openthread/include/openthread nel repository OpenThread. Queste API forniscono l'accesso a una serie di funzionalità e componenti OpenThread sia a livello di Thread che di piattaforma per l'utilizzo nelle tue applicazioni:

  • Informazioni e controllo delle istanze OpenThread
  • Servizi di applicazione come IPv6, UDP e CoAP
  • Gestione delle credenziali di rete, nonché dei ruoli Commissioner e Joiner
  • Gestione del router di confine
  • Funzionalità avanzate come la supervisione dei bambini e il rilevamento di interferenze

Le informazioni di riferimento su tutte le API OpenThread sono disponibili all'indirizzo openthread.io/reference.

Utilizzo di un'API

Per utilizzare un'API, includi il relativo file di intestazione in uno dei file dell'applicazione. Quindi, chiama la funzione desiderata.

Ad esempio, l'app di esempio CLI inclusa in OpenThread utilizza le seguenti intestazioni API:

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

#include <openthread/config.h>
#include <openthread/cli.h>
#include <openthread/diag.h>
#include <openthread/tasklet.h>
#include <openthread/platform/logging.h>

L'istanza OpenThread

La struttura otInstance è un elemento che utilizzerai di frequente quando lavori con le API OpenThread. Una volta inizializzata, questa struttura rappresenta un'istanza statica della libreria OpenThread e consente all'utente di effettuare chiamate all'API OpenThread.

Ad esempio, l'istanza OpenThread viene inizializzata nella funzione main() dell'app di esempio 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;
}

Funzioni specifiche della piattaforma

Se vuoi aggiungere funzioni specifiche della piattaforma a una delle applicazioni di esempio incluse in OpenThread, devi prima dichiararle nell'intestazione ./openthread/examples/platforms/openthread-system.h, utilizzando lo spazio dei nomi otSys per tutte le funzioni. Poi implementali in un file di origine specifico per la piattaforma. In questo modo, puoi utilizzare le stesse intestazioni di funzione per altre piattaforme di esempio.

Ad esempio, le funzioni GPIO che utilizzeremo per collegarci ai pulsanti e ai LED di nRF52840 devono essere dichiarate in openthread-system.h.

Apri il file ./openthread/examples/platforms/openthread-system.h nell'editor di testo che preferisci.

./openthread/examples/platforms/openthread-system.h

AZIONE: aggiungi dichiarazioni di funzioni GPIO specifiche della piattaforma.

Aggiungi queste dichiarazioni di funzione dopo #include per l'intestazione 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);

Le implementeremo nel passaggio successivo.

Tieni presente che la dichiarazione della funzione otSysButtonProcess utilizza un otInstance. In questo modo, l'applicazione può accedere alle informazioni sull'istanza OpenThread quando viene premuto un pulsante, se necessario. Tutto dipende dalle esigenze della tua applicazione. Se non ti serve nell'implementazione della funzione, puoi utilizzare la macro OT_UNUSED_VARIABLE dell'API OpenThread per eliminare gli errori di compilazione relativi alle variabili inutilizzate per alcune toolchain. Vedremo degli esempi in seguito.

5. Implementa l'astrazione della piattaforma GPIO

Nel passaggio precedente abbiamo esaminato le dichiarazioni di funzioni specifiche della piattaforma in ./openthread/examples/platforms/openthread-system.h che possono essere utilizzate per GPIO. Per accedere ai pulsanti e ai LED sulle schede di sviluppo nRF52840, devi implementare queste funzioni per la piattaforma nRF52840. In questo codice, aggiungerai funzioni che:

  • Inizializza i pin e le modalità GPIO
  • Controllare la tensione su un pin
  • Attiva le interruzioni GPIO e registra un callback

Nella directory ./src/src, crea un nuovo file denominato gpio.c. In questo nuovo file, aggiungi i seguenti contenuti.

./src/src/gpio.c (nuovo file)

AZIONE: aggiungi definizioni.

Queste definizioni fungono da astrazioni tra i valori e le variabili specifici di nRF52840 utilizzati a livello di applicazione 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

Per ulteriori informazioni sui pulsanti e sui LED di nRF52840, consulta l'Infocenter di Nordic Semiconductor.

AZIONE: aggiungi gli elementi inclusi nell'intestazione.

Aggiungi poi le intestazioni necessarie per la funzionalità 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"

AZIONE: aggiungi le funzioni di callback e interruzione per il pulsante 1.

Aggiungi questo codice. La funzione in_pin1_handler è il callback registrato quando viene inizializzata la funzionalità di pressione del pulsante (più avanti in questo file).

Nota come questo callback utilizza la macro OT_UNUSED_VARIABLE, poiché le variabili passate a in_pin1_handler non vengono effettivamente utilizzate nella funzione.

/* 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;
}

AZIONE: aggiungi una funzione per configurare i LED.

Aggiungi questo codice per configurare la modalità e lo stato di tutti i LED durante l'inizializzazione.

/**
 * @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);
}

AZIONE: aggiungi una funzione per impostare la modalità di un LED.

Questa funzione verrà utilizzata quando il ruolo del dispositivo cambia.

/**
 * @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;
    }
}

AZIONE: aggiungi una funzione per attivare/disattivare la modalità di un LED.

Questa funzione verrà utilizzata per attivare/disattivare il LED4 quando il dispositivo riceve un messaggio 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;
    }
}

AZIONE: aggiungi funzioni per inizializzare ed elaborare le pressioni dei pulsanti.

La prima funzione inizializza la scheda per la pressione di un pulsante e la seconda invia il messaggio UDP multicast quando viene premuto il pulsante 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);
    }
}

AZIONE: salva e chiudi il file gpio.c.

6. API: reagire alle modifiche del ruolo del dispositivo

Nella nostra applicazione vogliamo che si accendano LED diversi a seconda del ruolo del dispositivo. Monitoriamo i seguenti ruoli: Leader, Router, Dispositivo finale. Possiamo assegnarli ai LED come segue:

  • LED1 = Leader
  • LED2 = Router
  • LED3 = Dispositivo finale

Per attivare questa funzionalità, l'applicazione deve sapere quando il ruolo del dispositivo è cambiato e come attivare il LED corretto in risposta. Utilizzeremo l'istanza OpenThread per la prima parte e l'astrazione della piattaforma GPIO per la seconda.

Apri il file ./openthread/examples/apps/cli/main.c nell'editor di testo che preferisci.

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

AZIONE: aggiungi gli elementi inclusi nell'intestazione.

Nella sezione includes del file main.c, aggiungi i file di intestazione API necessari per la funzionalità di modifica del ruolo.

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

AZIONE: aggiungi la dichiarazione della funzione di gestore per la modifica dello stato dell'istanza OpenThread.

Aggiungi questa dichiarazione a main.c, dopo le inclusioni dell'intestazione e prima di eventuali istruzioni #if. Questa funzione verrà definita dopo l'applicazione principale.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

AZIONE: aggiungi una registrazione di callback per la funzione di gestore della modifica dello stato.

In main.c, aggiungi questa funzione alla funzione main() dopo la chiamata a otAppCliInit. Questa registrazione del callback indica a OpenThread di chiamare la funzione handleNetifStateChange ogni volta che cambia lo stato dell'istanza OpenThread.

/* Register Thread state change handler */
otSetStateChangedCallback(instance, handleNetifStateChanged, instance);

AZIONE: aggiungi l'implementazione della modifica dello stato.

In main.c, dopo la funzione main(), implementa la funzione handleNetifStateChanged. Questa funzione controlla il flag OT_CHANGED_THREAD_ROLE dell'istanza OpenThread e, se è cambiato, accende/spegne i LED in base alle esigenze.

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: utilizza il multicast per accendere un LED

Nella nostra applicazione vogliamo anche inviare messaggi UDP a tutti gli altri dispositivi della rete quando il pulsante 1 viene premuto su una scheda. Per confermare la ricezione del messaggio, attiveremo e disattiviamo il LED4 sulle altre schede in risposta.

Per attivare questa funzionalità, l'applicazione deve:

  • Inizializzare una connessione UDP all'avvio
  • Essere in grado di inviare un messaggio UDP all'indirizzo multicast locale del mesh
  • Gestire i messaggi UDP in arrivo
  • Attiva/disattiva LED4 in risposta ai messaggi UDP in arrivo

Apri il file ./openthread/examples/apps/cli/main.c nell'editor di testo che preferisci.

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

AZIONE: aggiungi gli elementi inclusi nell'intestazione.

Nella sezione includes nella parte superiore del file main.c, aggiungi i file di intestazione API necessari per la funzionalità UDP multicast.

#include <string.h>

#include <openthread/message.h>
#include <openthread/udp.h>

#include "utils/code_utils.h"

L'intestazione code_utils.h viene utilizzata per le macro otEXPECT e otEXPECT_ACTION che convalidano le condizioni di runtime e gestiscono in modo corretto gli errori.

AZIONE: aggiungi definizioni e costanti

Nel file main.c, dopo la sezione includes e prima di eventuali istruzioni #if, aggiungi costanti e definizioni specifiche per UDP:

#define UDP_PORT 1212

static const char UDP_DEST_ADDR[] = "ff03::1";
static const char UDP_PAYLOAD[]   = "Hello OpenThread World!";

ff03::1 è l'indirizzo multicast locale del mesh. Tutti i messaggi inviati a questo indirizzo verranno inviati a tutti i dispositivi Full Thread della rete. Per ulteriori informazioni sul supporto del multicast in OpenThread, consulta Multicast su openthread.io.

AZIONE: aggiungi le dichiarazioni di funzione.

Nel file main.c, dopo la definizione di otTaskletsSignalPending e prima della funzione main(), aggiungi funzioni specifiche per UDP, nonché una variabile statica per rappresentare una 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;

AZIONE: aggiungi chiamate per inizializzare i LED e il pulsante GPIO.

In main.c, aggiungi queste chiamate di funzione alla funzione main() dopo la chiamata otSetStateChangedCallback. Queste funzioni inizializzano i pin GPIO e GPIOTE e impostano un gestore dei pulsanti per gestire gli eventi di pressione dei pulsanti.

/* init GPIO LEDs and button */
otSysLedInit();
otSysButtonInit(handleButtonInterrupt);

AZIONE: aggiungi la chiamata di inizializzazione UDP.

In main.c, aggiungi questa funzione alla funzione main() dopo la chiamata otSysButtonInit che hai appena aggiunto:

initUdp(instance);

Questa chiamata garantisce l'inizializzazione di una socket UDP all'avvio dell'applicazione. Senza questo, il dispositivo non può inviare o ricevere messaggi UDP.

AZIONE: aggiungi la chiamata per elaborare l'evento del pulsante GPIO.

In main.c, aggiungi questa chiamata di funzione alla funzione main() dopo la chiamata otSysProcessDrivers, nel loop while. Questa funzione, dichiarata in gpio.c, controlla se il pulsante è stato premuto e, in caso affermativo, chiama il gestore (handleButtonInterrupt) impostato nel passaggio precedente.

otSysButtonProcess(instance);

AZIONE: implementa l'handler di interruzione del pulsante.

In main.c, aggiungi l'implementazione della funzione handleButtonInterrupt dopo la funzione handleNetifStateChanged aggiunta nel passaggio precedente:

/**
 * Function to handle button push event
 */
void handleButtonInterrupt(otInstance *aInstance)
{
    sendUdp(aInstance);
}

AZIONE: implementa l'inizializzazione UDP.

In main.c, aggiungi l'implementazione della funzione initUdp dopo la funzione handleButtonInterrupt che hai appena aggiunto:

/**
 * 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 è la porta che hai definito in precedenza (1212). La funzione otUdpOpen apre il socket e registra una funzione di callback (handleUdpReceive) per quando viene ricevuto un messaggio UDP. otUdpBind associa la socket all'interfaccia di rete Thread passando OT_NETIF_THREAD. Per altre opzioni di interfaccia di rete, consulta l'enumerazione otNetifIdentifier nel riferimento all'API UDP.

AZIONE: implementa la messaggistica UDP.

In main.c, aggiungi l'implementazione della funzione sendUdp dopo la funzione initUdp che hai appena aggiunto:

/**
 * 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);
    }
}

Prendi nota delle macro otEXPECT e otEXPECT_ACTION. Questi assicurano che il messaggio UDP sia valido e allocato correttamente nel buffer e, in caso contrario, la funzione gestisce gli errori in modo elegante passando al blocco exit, dove libera il buffer.

Per ulteriori informazioni sulle funzioni utilizzate per inizializzare UDP, consulta i riferimenti IPv6 e UDP su openthread.io.

AZIONE: implementa la gestione dei messaggi UDP.

In main.c, aggiungi l'implementazione della funzione handleUdpReceive dopo la funzione sendUdp che hai appena aggiunto. Questa funzione attiva/disattiva semplicemente il 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: configura la rete Thread

Per semplificare la dimostrazione, vogliamo che i nostri dispositivi avviino immediatamente Thread e si uniscano a una rete quando vengono accesi. A questo scopo, utilizzeremo la struttura otOperationalDataset. Questa struttura contiene tutti i parametri necessari per trasmettere le credenziali della rete Thread a un dispositivo.

L'utilizzo di questa struttura sostituirà i valori predefiniti della rete integrati in OpenThread, per rendere la nostra applicazione più sicura e limitare i nodi Thread nella nostra rete solo a quelli che eseguono l'applicazione.

Apri di nuovo il file ./openthread/examples/apps/cli/main.c nell'editor di testo che preferisci.

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

AZIONE: aggiungi inclusione intestazione.

Nella sezione includes nella parte superiore del file main.c, aggiungi il file di intestazione dell'API necessario per configurare la rete Thread:

#include <openthread/dataset_ftd.h>

AZIONE: aggiungi la dichiarazione di funzione per l'impostazione della configurazione di rete.

Aggiungi questa dichiarazione a main.c, dopo le inclusioni dell'intestazione e prima di eventuali istruzioni #if. Questa funzione verrà definita dopo la funzione dell'applicazione principale.

static void setNetworkConfiguration(otInstance *aInstance);

AZIONE: aggiungi la chiamata di configurazione di rete.

In main.c, aggiungi questa chiamata di funzione alla funzione main() dopo la chiamata a otSetStateChangedCallback. Questa funzione configura il set di dati della rete Thread.

/* Override default network credentials */
setNetworkConfiguration(instance);

AZIONE: aggiungi chiamate per attivare l'interfaccia di rete e lo stack Thread.

In main.c, aggiungi queste chiamate di funzione alla funzione main() dopo la chiamata otSysButtonInit.

/* Start the Thread network interface (CLI cmd > ifconfig up) */
otIp6SetEnabled(instance, true);

/* Start the Thread stack (CLI cmd > thread start) */
otThreadSetEnabled(instance, true);

AZIONE: implementa la configurazione della rete Thread.

In main.c, aggiungi l'implementazione della funzione setNetworkConfiguration dopo la funzione 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);
}

Come descritto nella funzione, i parametri di rete Thread che utilizziamo per questa applicazione sono:

  • Canale = 15
  • PAN ID = 0x2222
  • ID PAN esteso = C0DE1AB5C0DE1AB5
  • Network Key = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Nome della rete = OTCodelab

Inoltre, è qui che riduciamo il jitter di selezione del router, in modo che i nostri dispositivi cambino ruolo più rapidamente a scopo dimostrativo. Tieni presente che questa operazione viene eseguita solo se il nodo è un FTD (Full Thread Device). Scopri di più nel passaggio successivo.

9. API: funzioni limitate

Alcune API di OpenThread modificano impostazioni che devono essere modificate solo a scopo di dimostrazione o test. Queste API non devono essere utilizzate in un deployment di produzione di un'applicazione che utilizza OpenThread.

Ad esempio, la funzione otThreadSetRouterSelectionJitter regola il tempo (in secondi) necessario a un dispositivo di destinazione per promuovere se stesso a un router. Il valore predefinito è 120, in base alla specifica Thread. Per praticità d'uso in questo Codelab, lo cambieremo in 20, in modo da non dover attendere molto tempo prima che un nodo Thread cambi ruolo.

Nota: i dispositivi MTD non diventano router e il supporto di una funzione come otThreadSetRouterSelectionJitter non è incluso in una build MTD. In seguito dobbiamo specificare l'opzione CMake -DOT_MTD=OFF, altrimenti si verificherà un errore di compilazione.

Puoi confermarlo esaminando la definizione della funzione otThreadSetRouterSelectionJitter, contenuta in una direttiva del preprocessore di 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. Aggiornamenti di CMake

Prima di compilare l'applicazione, sono necessari alcuni aggiornamenti minori per tre file CMake. Vengono utilizzati dal sistema di compilazione per compilare e collegare l'applicazione.

./third_party/NordicSemiconductor/CMakeLists.txt

Ora aggiungi alcuni flag a NordicSemiconductor CMakeLists.txt per assicurarti che le funzioni GPIO siano definite nell'applicazione.

AZIONE: aggiungi indicatori al file CMakeLists.txt.

Apri ./third_party/NordicSemiconductor/CMakeLists.txt nell'editor di testo che preferisci e aggiungi le seguenti righe nella sezione 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

Modifica il file ./src/CMakeLists.txt per aggiungere il nuovo file di origine gpio.c:

AZIONE: aggiungi l'origine gpio al file ./src/CMakeLists.txt.

Apri ./src/CMakeLists.txt nel tuo editor di testo preferito e aggiungi il file alla sezione NRF_COMM_SOURCES.

...

set(NRF_COMM_SOURCES
  ...
  src/gpio.c
  ...
)

...

./third_party/NordicSemiconductor/CMakeLists.txt

Infine, aggiungi il file del driver nrfx_gpiote.c al file NordicSemiconductor CMakeLists.txt in modo che sia incluso nella compilazione della libreria dei driver Nordic.

AZIONE: aggiungi il driver gpio al file NordicSemiconductor CMakeLists.txt .

Apri ./third_party/NordicSemiconductor/CMakeLists.txt nel tuo editor di testo preferito e aggiungi il file alla sezione COMMON_SOURCES.

...

set(COMMON_SOURCES
  ...
  nrfx/drivers/src/nrfx_gpiote.c
  ...
)
...

11. Configura i dispositivi

Una volta completati tutti gli aggiornamenti del codice, puoi compilare e eseguire il flashing dell'applicazione su tutte e tre le schede di sviluppo Nordic nRF52840. Ogni dispositivo funzionerà come dispositivo Thread completo (FTD).

Creare OpenThread

Compila i file binari di OpenThread FTD per la piattaforma nRF52840.

$ cd ~/ot-nrf528xx
$ ./script/build nrf52840 UART_trans -DOT_MTD=OFF -DOT_APP_RCP=OFF -DOT_RCP=OFF

Vai alla directory con il file binario della CLI di OpenThread FTD e convertilo in formato esadecimale con la suite di strumenti ARM Embedded:

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

Illuminare le schede

Esegui il flashing del file ot-cli-ftd.hex su ogni scheda nRF52840.

Collega il cavo USB alla porta di debug micro USB accanto al pin di alimentazione esterna sulla scheda nRF52840 e poi alla tua macchina Linux. Se è impostato correttamente, LED5 è acceso.

20a3b4b480356447.png

Come prima, prendi nota del numero di serie della scheda nRF52840:

c00d519ebec7e5f0.jpeg

Vai alla posizione degli strumenti a riga di comando nRFx e esegui il flashing del file esadecimale FTD della CLI OpenThread sulla scheda nRF52840 utilizzando il numero di serie della scheda:

$ cd ~/nrfjprog
$ ./nrfjprog -f nrf52 -s 683704924 --verify --chiperase --program \
       ~/openthread/output/nrf52840/bin/ot-cli-ftd.hex --reset

Il LED5 si spegne brevemente durante il lampeggio. Se l'operazione è riuscita, viene generato il seguente output:

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.

Ripeti questo passaggio "Esegui il flashing delle schede" per le altre due schede. Ogni scheda deve essere connessa alla macchina Linux nello stesso modo e il comando per il flashing è lo stesso, tranne per il numero di serie della scheda. Assicurati di utilizzare il numero di serie univoco di ogni scheda in

nrfjprog Comando lampeggiante.

In caso di esito positivo, LED1, LED2 o LED3 si accenderà su ogni scheda. Potresti anche vedere il passaggio del LED acceso da 3 a 2 (o da 2 a 1) subito dopo il lampeggio (la funzionalità di modifica del ruolo del dispositivo).

12. Funzionalità dell'applicazione

Tutte e tre le schede nRF52840 dovrebbero ora essere alimentate ed eseguire la nostra applicazione OpenThread. Come descritto in precedenza, questa applicazione ha due funzionalità principali.

Indicatori del ruolo del dispositivo

Il LED acceso su ogni scheda riflette il ruolo corrente del nodo Thread:

  • LED1 = Leader
  • LED2 = Router
  • LED3 = Dispositivo finale

Con il cambio di ruolo cambia anche il LED illuminato. Dovresti aver già notato queste modifiche su una o due schede entro 20 secondi dall'accensione di ciascun dispositivo.

Multicast UDP

Quando il pulsante 1 viene premuto su una scheda, viene inviato un messaggio UDP all'indirizzo multicast locale della mesh, che include tutti gli altri nodi della rete Thread. In risposta alla ricezione di questo messaggio, il LED4 su tutte le altre schede si accende o si spegne. Il LED4 rimane acceso o spento per ogni scheda finché non riceve un altro messaggio UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demo: osserva le modifiche ai ruoli dei dispositivi

I dispositivi su cui hai eseguito il flashing sono un tipo specifico di dispositivo Full Thread (FTD) chiamato dispositivo finale idoneo per il router (REED). Ciò significa che possono funzionare come router o come dispositivo di destinazione e possono passare da un dispositivo di destinazione a un router.

Thread può supportare fino a 32 router, ma cerca di mantenere il numero di router compreso tra 16 e 23. Se un REED si connette come dispositivo di destinazione e il numero di router è inferiore a 16, si promuove automaticamente a router. Questa modifica deve avvenire in un momento casuale entro il numero di secondi impostato per il valore otThreadSetRouterSelectionJitter nell'applicazione (20 secondi).

Ogni rete Thread ha anche un leader, ovvero un router responsabile della gestione dell'insieme di router in una rete Thread. Con tutti i dispositivi accesi, dopo 20 secondi uno di questi dovrebbe essere un Leader (LED1 acceso) e gli altri due dovrebbero essere Router (LED2 acceso).

4e1e885861a66570.png

Rimuovere il leader

Se il leader viene rimosso dalla rete Thread, un altro router si promuove a leader per garantire che la rete ne abbia sempre uno.

Disattiva la classifica (quella con il LED1 acceso) utilizzando l'interruttore Power. Attendi circa 20 secondi. Su una delle due schede rimanenti, il LED2 (router) si spegne e si accende il LED1 (leader). Questo dispositivo è ora il leader della rete Thread.

4c57c87adb40e0e3.png

Riattiva la classifica originale. Dovrebbe ricollegarsi automaticamente alla rete Thread come dispositivo finale (LED3 è acceso). Entro 20 secondi (il jitter di selezione del router), si autopromuove a router (LED2 è acceso).

5f40afca2dcc4b5b.png

Ripristina le schede

Spegni tutte e tre le schede, quindi riattivale e osserva i LED. La prima scheda accesa dovrebbe iniziare con il ruolo di Leader (LED1 è acceso): il primo router in una rete Thread diventa automaticamente il Leader.

Le altre due schede si connettono inizialmente alla rete come dispositivi di endpoint (LED3 è acceso), ma dovrebbero promuovere se stesse come router (LED2 è acceso) entro 20 secondi.

Partizioni di rete

Se le schede non ricevono energia sufficiente o la connessione radio tra di loro è debole, la rete Thread potrebbe essere suddivisa in partizioni e potresti avere più di un dispositivo visualizzato come Leader.

Il thread è auto-riparabile, quindi le partizioni dovrebbero ricongiungersi in una singola partizione con un leader.

14. Demo: invia multicast UDP

Se continui dall'esercizio precedente, il LED4 non deve essere acceso su nessun dispositivo.

Scegli una scheda e premi il tasto 1. Il LED4 su tutte le altre schede della rete Thread su cui è in esecuzione l'applicazione deve attivare/disattivare il proprio stato. Se continui dall'esercizio precedente, ora dovrebbero essere attivi.

f186a2618fdbe3fd.png

Premi di nuovo il pulsante 1 per la stessa scheda. Il LED4 su tutte le altre schede dovrebbe lampeggiare di nuovo.

Premi il pulsante 1 su una scheda diversa e osserva come il LED4 si attiva e disattiva sulle altre schede. Premi il pulsante 1 su una delle schede in cui il LED4 è attualmente acceso. Il LED4 rimane acceso per la scheda, ma si attiva e disattiva per le altre.

f5865ccb8ab7aa34.png

Partizioni di rete

Se le tue schede sono partizionate e sono presenti più leader, il risultato del messaggio multicast sarà diverso da una scheda all'altra. Se premi il pulsante 1 su una scheda con partizione (e quindi è l'unico membro della rete Thread partizionata), il LED4 sulle altre schede non si accende in risposta. In questo caso, reimposta le schede: idealmente, riformeranno una singola rete Thread e la messaggistica UDP dovrebbe funzionare correttamente.

15. Complimenti!

Hai creato un'applicazione che utilizza le API OpenThread.

Ora sai:

  • Come programmare i pulsanti e i LED sulle schede di sviluppo Nordic nRF52840
  • Come utilizzare le API OpenThread comuni e la classe otInstance
  • Come monitorare e reagire alle modifiche dello stato di OpenThread
  • Come inviare messaggi UDP a tutti i dispositivi di una rete Thread
  • Come modificare i file Makefile

Passaggi successivi

A partire da questo codelab, prova i seguenti esercizi:

  • Modifica il modulo GPIO in modo da utilizzare i pin GPIO anziché i LED di bordo e collega i LED RGB esterni che cambiano colore in base al ruolo del router
  • Aggiungere il supporto GPIO per una piattaforma di esempio diversa
  • Anziché utilizzare il multicast per eseguire il ping di tutti i dispositivi premendo un pulsante, utilizza l'API Router/Leader per individuare e eseguire il ping di un singolo dispositivo
  • Collega la tua rete mesh a internet utilizzando un router di confine OpenThread e trasmetti in multicast dall'esterno della rete Thread per illuminare i LED

Per approfondire

Visita openthread.io e GitHub per una serie di risorse OpenThread, tra cui:

Riferimento: