Sviluppo con le API OpenThread

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 agli sviluppatori per accelerare lo sviluppo dei prodotti per la casa connessa.

La specifica Thread definisce un protocollo di comunicazione da dispositivo a dispositivo affidabile, sicuro e a basso consumo basato su IPv6 per le applicazioni per la casa. OpenThread implementa tutti i livelli di networking Thread, tra cui IPv6, 6LoWPAN, IEEE 802.15.4 con sicurezza MAC, Creazione link mesh e Routing mesh.

In questo codelab, utilizzerai le API OpenThread per avviare una rete Thread, monitorare e reagire ai cambiamenti nei ruoli dei dispositivi e inviare messaggi UDP, oltre a collegare queste azioni a pulsanti e LED su hardware reale.

2a6db2e258c32237.png

Cosa scoprirai

  • Come programmare pulsanti e LED su schede di sviluppo nordico 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 Makefiles

Che cosa ti serve

Hardware:

  • 3 schede per sviluppatori nRF52840 Nordic Semiconductor
  • 3 cavi da USB a micro USB per il collegamento di schede
  • Una macchina Linux con almeno 3 porte USB

Software:

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

Salvo diversa indicazione, i contenuti di questo codelab sono concessi in base allalicenza Creative Commons Attribuzione 3.0, mentre gli esempi di codice sono concessi in licenza laLicenza Apache 2.0.

2. Guida introduttiva

Completa il codelab sull'hardware

Prima di iniziare questo codelab, devi completare il codelab sulla creazione di una rete di thread con schede nRF52840 e OpenThread, che:

  • Specifica i software necessari per la creazione e le operazioni di flashing
  • Insegna come creare e usare OpenThread su schede nRF52840 nordiche
  • Dimostra le nozioni di base di una rete Thread

In questo codelab non è descritto in dettaglio nessuno degli ambienti configurati per creare OpenThread e flash, ma solo le istruzioni di base per eseguire il flashing delle lavagne. Si presume che tu abbia già completato il codelab sulla creazione di 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 di Thread. Tutti i passaggi sono stati testati su Ubuntu 14.04.5 LTS (Trusty Tahr).

Schede nordic Semiconductor nRF52840

Questo codelab utilizza tre schede nRF52840 PDK.

a6693da3ce213856.png

Installa software

Per creare e lanciare Flash Thread, devi installare SEGGER J-Link, gli strumenti a riga di comando nRF5x, lo strumento ARM GNU Toolchain e vari pacchetti Linux. Se hai completato la build del codelab sulla creazione di una rete Thread, avrai già tutto ciò che ti serve. In caso contrario, completa quel codelab prima di continuare a creare e implementare Flash Thread su schede per sviluppatori nRF52840.

3. Clonare il repository

OpenThread include un codice applicazione di esempio utilizzabile come punto di partenza per questo codelab.

Clonare il repository degli esempi OpenThread Nordic nRF528xx e creare 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 di OpenThread. Queste API forniscono l'accesso a una serie di caratteristiche e funzionalità di OpenThread sia a livello di thread che a livello di piattaforma per l'utilizzo nelle applicazioni:

  • Informazioni e controllo sulle istanze OpenThread
  • Servizi per le applicazioni come IPv6, UDP e CoAP
  • Gestione delle credenziali di rete, oltre ai ruoli di Commissioner e Joiner
  • Gestione dei router di confine
  • Funzionalità avanzate come la supervisione dei bambini e il rilevamento di Jam

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 che ti interessa.

Ad esempio, l'app dell'interfaccia a riga di comando 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 è una risorsa che utilizzerai spesso 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 dell'interfaccia a riga di comando di esempio:

./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, dichiarale prima nell'intestazione ./openthread/examples/platforms/openthread-system.h, utilizzando lo spazio dei nomi otSys per tutte le funzioni. Quindi 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 useremo per collegarsi ai pulsanti nRF52840 e i LED devono essere dichiarati 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);

Li implementeremo nel prossimo passaggio.

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 ne hai bisogno nell'implementazione della funzione, puoi utilizzare la macro OT_UNUSED_VARIABLE nell'API OpenThread per eliminare gli errori di generazione delle variabili inutilizzate per alcune toolchain. Ne vedremo 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 a pulsanti e LED sulle schede di sviluppo nRF52840, è necessario implementare tali funzioni per la piattaforma nRF52840. In questo codice, aggiungerai funzioni che:

  • Inizializza le modalità e i PIN GPIO
  • Controlla la tensione su un pin
  • Attiva interruzioni di 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.

Per definire le astrazioni, vengono utilizzati valori e 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 su pulsanti e LED nRF52840, consulta il centro informazioni sul semiconduttore norvegese.

AZIONE: Aggiungi intestazione inclusa.

Dopodiché, aggiungi l'intestazione che include 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"

ACTION: Aggiungi funzioni di callback e interruzione per il pulsante 1.

Aggiungi questo codice in seguito. La funzione in_pin1_handler è il callback registrato quando viene attivata la funzionalità di pressione dei pulsanti (più avanti nel file).

Ricorda che questo callback utilizza la macro OT_UNUSED_VARIABLE, poiché le variabili trasmesse 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;
}

ACTION: 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);
}

ACTION: 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;
    }
}

ACTION: Aggiungi una funzione per attivare o disattivare la modalità di un LED.

Questa funzione sarà utilizzata per attivare/disattivare 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 multicast UDP 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 filegpio.c .

6. API: reagire ai cambiamenti dei ruoli del dispositivo

Nella nostra applicazione, vogliamo che diversi LED si illuminano in base al ruolo del dispositivo. Monitoriamo i seguenti ruoli: Leader, Router, Dispositivo finale. Possiamo assegnarli ai LED in questo modo:

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

Per abilitare 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 GPIO della piattaforma 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 intestazione inclusa.

Nella sezione Includi 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>

ACTION: aggiungi la dichiarazione della funzione Handler per la modifica dello stato dell'istanza OpenThread.

Aggiungi questa dichiarazione a main.c, dopo che l'intestazione include e prima di eventuali istruzioni #if. Questa funzione sarà definita dopo l'applicazione principale.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

ACTION: aggiungi una registrazione di callback per la funzione di gestione dello stato.

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

/* 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 è cambiata, attiva o disattiva i LED in base alle necessità.

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 inoltre inviare messaggi UDP a tutti gli altri dispositivi nella rete quando il pulsante1 viene premuto su una scheda. Per confermare la ricezione del messaggio, imposteremo LED4 sulle altre lavagne in risposta.

Per attivare questa funzionalità, l'applicazione deve:

  • Inizializza una connessione UDP all'avvio
  • Avere la possibilità di inviare un messaggio UDP all'indirizzo multicast mesh locale
  • 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 intestazione inclusa.

Nella sezione include 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 agevolmente gli errori.

AZIONE: aggiungi definizioni e costanti:

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

#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 mesh locale. I messaggi inviati a questo indirizzo verranno inviati a tutti i dispositivi Thread completi della rete. Consulta Multicast su openthread.io per ulteriori informazioni sul supporto multicast in OpenThread.

ACTION: Aggiungi dichiarazioni di funzione.

Nel file main.c, dopo la definizione di otTaskletsSignalPending e prima della funzione main(), aggiungi funzioni specifiche per UDP e una variabile statica per rappresentare 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: Aggiungi chiamate per inizializzare LED e 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 di pulsanti per gestire gli eventi di pressione dei pulsanti.

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

ACTION: Aggiungi la chiamata di inizializzazione UDP.

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

initUdp(instance);

Questa chiamata assicura che un socket UDP venga inizializzato all'avvio dell'applicazione. In caso contrario, il dispositivo non potrà inviare o ricevere messaggi UDP.

AZIONE: Aggiungi una chiamata per elaborare l'evento del pulsante GPIO.

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

otSysButtonProcess(instance);

AZIONE: implementa il gestore per l'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 di UDP.

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

/**
 * 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 definita 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 il socket all'interfaccia di rete di Thread passando OT_NETIF_THREAD. Per altre opzioni dell'interfaccia di rete, fai riferimento all'enumerazione di otNetifIdentifier nella sezione Riferimento API UDP.

AZIONE: implementa i messaggi UDP.

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

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

Osserva le macro otEXPECT e otEXPECT_ACTION. Queste assicurano che il messaggio UDP sia valido e che sia stato allocato correttamente nel buffer; in caso contrario, la funzione gestisce correttamente gli errori passando al blocco exit, dove consente di liberare il buffer.

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

AZIONE: implementa la gestione dei messaggi UDP.

In main.c, aggiungi l'implementazione della funzione handleUdpReceive dopo la funzione sendUdp appena aggiunta. Questa funzione attiva o disattiva 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: configurare la rete Thread

Per facilitare la dimostrazione, vogliamo che i nostri dispositivi avviino subito Thread e si connettano a una rete quando sono accesi. A questo scopo, utilizzeremo la struttura otOperationalDataset. Questa struttura contiene tutti i parametri necessari per trasmettere le credenziali di rete di Thread a un dispositivo.

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

Anche in questo caso, apri il file ./openthread/examples/apps/cli/main.c nell'editor di testo che preferisci.

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

AZIONE: Aggiungi intestazione inclusa.

All'interno della sezione include nella parte superiore del file main.c, aggiungi il file di intestazione API necessario per configurare la rete Thread:

#include <openthread/dataset_ftd.h>

ACTION: Aggiungi la dichiarazione della funzione per impostare la configurazione di rete.

Aggiungi questa dichiarazione a main.c, dopo che l'intestazione include e prima di eventuali istruzioni #if. Questa funzione sarà definita dopo la funzione principale dell'applicazione.

static void setNetworkConfiguration(otInstance *aInstance);

ACTION: Aggiungi la chiamata per la configurazione della rete.

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

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

AZIONE: aggiungi le chiamate per abilitare l'interfaccia e lo stack di rete 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                      = 1;
    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 illustrato in dettaglio nella funzione, i parametri della rete Thread che utilizziamo per questa applicazione sono:

  • Canale = 15
  • ID PAN = 0x2222
  • ID PAN esteso = C0DE1AB5C0DE1AB5
  • Chiave di rete = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Nome rete = OTCodelab

Inoltre, è qui che riduciamo il tremolio della selezione di router, così i nostri dispositivi cambiano ruolo più velocemente a scopo dimostrativo. Tieni presente che questa operazione viene eseguita solo se il nodo è un FTD (Full Thread Device). Scopri di più nel prossimo passaggio.

9. API: funzioni limitate

Alcune API di OpenThread modificano le impostazioni che devono essere modificate solo a scopo dimostrativo o di 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) impiegato da un dispositivo finale per promuoversi a un router. Il valore predefinito di questo valore è 120, in base alla specifica del thread. Per facilitare l'uso di questo codelab, lo cambieremo in 20, quindi non dovrai attendere molto per che un nodo Thread cambi i ruoli.

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

Puoi averne conferma consultando la definizione della funzione otThreadSetRouterSelectionJitter, contenuta in un'istruzione di pre-elaborazione 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 fai da te

Prima di creare l'applicazione, devi apportare alcuni piccoli aggiornamenti a tre file CMake. Questi vengono utilizzati dal sistema di compilazione per compilare e collegare l'applicazione.

./third_party/NordicSemiconductor/CMakeLists.txt

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

ACTION: aggiungi i flag alCMakeLists.txt file.

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

ACTION: Aggiungi l'origine gpio al file./src/CMakeLists.txt .

Apri ./src/CMakeLists.txt nell'editor di testo che preferisci 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 CMakeLists.txt del NordicSemiconductor in modo che sia incluso nella build della libreria dei driver nordici.

AZIONE: Aggiungi il driver gpio al fileCMakeLists.txt Nord Semiconductor.

Apri ./third_party/NordicSemiconductor/CMakeLists.txt nell'editor di testo che preferisci e aggiungi il file alla sezione COMMON_SOURCES.

...

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

11 Configura i dispositivi

Completati gli aggiornamenti del codice, puoi creare e integrare l'applicazione in tutte e tre le schede di sviluppo nRF52840 del Nordic. Ogni dispositivo funziona come un dispositivo Full Thread (FTD).

Crea OpenThread

Crea i programmi binari 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 dell'interfaccia a riga di comando FTD OpenThread e convertilo in formato esadecimale con ARM Embedded Toolchain:

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

Lavagne flash

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 esterno della scheda nRF52840, quindi collegalo alla macchina Linux. Se l'impostazione è corretta, LED5 è attivo.

20a3b4b480356447.png

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

c00d519ebec7e5f0.jpeg

Passa alla posizione degli strumenti a riga di comando nRFx e invia il file esadecimale OpenThread CLI FTD 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 per un breve intervallo di tempo. Il seguente output viene generato correttamente.

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 relativo a "Flash the boards" per le altre due lavagne. Ogni scheda deve essere collegata alla macchina Linux allo stesso modo e il comando da lampeggiare è lo stesso, tranne il numero di serie della scheda. Assicurati di utilizzare il numero di serie univoco di ciascuna lavagna nella

nrfjprog comando lampeggiante.

Se riuscito, LED1, LED2 o LED3 si accendono su ciascuna scheda. Potresti anche notare l'interruttore LED illuminato da 3 a 2 (o da 2 a 1) subito dopo il lampeggiamento (la funzionalità di modifica del ruolo del dispositivo).

12 Funzionalità dell'applicazione

Tutte e tre le schede nRF52840 ora dovrebbero essere alimentate e in esecuzione nella nostra applicazione OpenThread. Come spiegato in precedenza, questa applicazione ha due funzionalità principali.

Indicatori del ruolo del dispositivo

Il LED acceso su ciascuna lavagna riflette il ruolo corrente del nodo Thread:

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

Il ruolo cambia a mano a mano che il ruolo cambia. Dovresti avere già visto queste modifiche su una o due schede entro 20 secondi dall'accensione di ogni dispositivo.

Multicast UDP

Quando viene premuto Pulsante 1 su una lavagna, viene inviato un messaggio UDP all'indirizzo multicast mesh locale, che include tutti gli altri nodi della rete Thread. In risposta a questo messaggio, LED4 su tutte le altre lavagne si attiva o disattiva. LED4 rimane acceso o spento per ogni scheda finché non riceve un altro messaggio UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demo: osservare le modifiche dei ruoli del dispositivo

I dispositivi lampeggiati sono un tipo specifico di dispositivo completo del thread (FTD) che si chiama dispositivo finale idoneo del router (REED). Ciò significa che possono funzionare come router o dispositivo finale e possono promuoversi da un dispositivo finale a un router.

Thread può supportare fino a 32 router, ma tenta di mantenere il numero di router tra 16 e 23. Se un REED si collega come Dispositivo finale e il numero di router è inferiore a 16, si promuove automaticamente a un router. Questo cambiamento dovrebbe avvenire in modo casuale entro il numero di secondi impostato nel valore otThreadSetRouterSelectionJitter nell'applicazione (20 secondi).

Ogni rete Thread ha anche un Leader, che è un router responsabile della gestione del set di router in una rete Thread. Con tutti i dispositivi accesi, dopo 20 secondi uno di questi dovrebbe essere Leader (LED1 attivo) e gli altri due dovrebbero essere Router (LED2 attivi).

4e1e885861a66570.png

Rimuovi il leader

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

Spegni la scheda Leader (quella con LED1 accesa) utilizzando l'interruttore di accensione. Attendi circa 20 secondi. Su una delle due schede rimanenti, il LED2 (Router) si spegne e il LED1 (Leader) si accende. Questo dispositivo è ora il leader della rete Thread.

4c57c87adb40e0e3.png

Riattiva la lavagna originale. Dovrebbe rientrare automaticamente nella rete Thread come dispositivo finale (il LED3 è acceso). Entro 20 secondi, il tremolio di selezione del router si promuove a un router (il LED2 è acceso).

5f40afca2dcc4b5b.png

Ripristina le lavagne

Spegni tutte e tre le schede, quindi riaccendile e osserva i LED. La prima scheda accesa dovrebbe iniziare nel 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 finali (il LED3 è acceso), ma dovrebbero promuoversi ai router (il LED2 è acceso) entro 20 secondi.

Partizioni di rete

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

Il thread si ripristina automaticamente, quindi le partizioni devono essere unite in un'unica partizione.

14. Demo: invio multicast UDP

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

Scegli una lavagna e premi il tasto Pulsante 1. LED4 su tutte le altre schede nella rete Thread che eseguono l'applicazione devono attivare o disattivare il loro stato. Se continui dall'esercizio precedente, ora dovrebbero essere attivi.

f186a2618fdbe3fd.png

Premi di nuovo il pulsante 1 per la stessa lavagna. Il LED4 su tutte le altre lavagne dovrebbe riattivarsi.

Premi il pulsante 1 su una scheda diversa e osserva come la spia LED 4 si attiva o disattiva sulle altre schede. Premi il pulsante 1 su una delle schede in cui è attualmente attivo il LED4. LED4 rimane attivo per quella lavagna, ma si attiva con le altre.

f5865ccb8ab7aa34.png

Partizioni di rete

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

15. Complimenti!

Hai creato un'applicazione che utilizza API OpenThread!

Ora sai:

  • Come programmare pulsanti e LED su schede di sviluppo nordico 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 Makefiles

Passaggi successivi

Partendo da questo codelab, prova i seguenti esercizi:

  • Modifica il modulo GPIO in modo da utilizzare i pin GPIO anziché i LED integrati e collega i LED RGB esterni che cambiano colore in base al ruolo del router
  • Aggiungi il supporto GPIO per una piattaforma di esempio diversa
  • Anziché utilizzare il multicast per eseguire il ping di tutti i dispositivi con la semplice pressione di un pulsante, usa l'API Routeer/Leader per individuare e inviare un ping a un singolo dispositivo
  • Connetti la tua rete mesh a Internet utilizzando un router perimetrale OpenThread e multicast da all'esterno della rete Thread per accendere i LED

Per approfondire

Dai un'occhiata a openthread.io e GitHub per consultare una serie di risorse di OpenThread, tra cui:

Riferimento: