Sviluppo con le API OpenThread

1. Introduzione

26b7f4f6b3ea0700.png

OpenThread rilasciato da Nest è un'implementazione open source del protocollo di rete Thread®. Nest ha lanciato OpenThread per rendere la tecnologia utilizzata nei prodotti Nest a disposizione degli sviluppatori per accelerare lo sviluppo di prodotti per la casa connessa.

La specifica Thread definisce un protocollo di comunicazione wireless tra dispositivi affidabile, sicuro e a basso consumo basato su IPv6 per le applicazioni di casa. OpenThread implementa tutti i livelli di networking Thread inclusi IPv6, 6LoWPAN, IEEE 802.15.4 con sicurezza MAC, Mesh Link ment e il routing della rete 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, nonché collegare queste azioni a pulsanti e LED su hardware reale.

2a6db2e258c32237.png

Obiettivi didattici

  • Come programmare i pulsanti e i LED sulle bacheche Nordic nRF52840
  • Come utilizzare le API OpenThread comuni e la classe otInstance
  • Come monitorare e reagire ai cambiamenti di stato di OpenThread
  • Come inviare messaggi UDP a tutti i dispositivi in una rete Thread
  • Come modificare i file di marca

Che cosa ti serve

Hardware:

  • 3 schede di sviluppo Nordic Semiconductor nRF52840
  • 3 cavi USB da micro USB per collegare le schede
  • Una macchina Linux con almeno tre porte USB

Software:

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

Salvo diversamente indicato, i contenuti di questo codelab sono concessi in base alla licenza Creative Commons Attribution 3.0, mentre gli esempi di codice sono disponibili in base alla licenza Apache 2.0.

2. Per cominciare

Completa il codelab sull'hardware

Prima di iniziare questo codelab, devi completare il codelab su Build a Thread Network with nRF52840 Boards e OpenThread, che:

  • Dettagli di tutto il software necessario per la creazione e il flashing
  • Ti insegna a creare OpenThread e lampeggiare sulle schede Nordic nRF52840
  • Dimostra le nozioni di base di una rete Thread

In questo codelab non sono richiesti ambienti di configurazione per la creazione di OpenThread e Flash, ma solo istruzioni di base per eseguire il flashing delle lavagne. Si presume che tu abbia già completato il codelab per la 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 bacheche 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 nRF52840 PDK.

a6693da3ce213856.png

Installa software

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

3. Clona il repository

OpenThread fornisce un esempio di codice dell'applicazione che puoi utilizzare come punto di partenza per questo codelab.

Clona il repository OpenThread Nordic nRF528xx di esempio e crea 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 caratteristiche e funzionalità di OpenThread sia a livello di thread che di piattaforma per l'utilizzo nelle applicazioni:

  • Informazioni e controllo delle istanze OpenThread
  • Servizi per applicazioni come IPv6, UDP e CoAP
  • Gestione delle credenziali di rete, insieme ai ruoli di Commissioner e Joiner
  • Gestione dei router di confine
  • Funzionalità avanzate come Supervisione dei bambini e 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 file di intestazione in uno dei file dell'applicazione. Quindi, chiama la funzione desiderata.

Ad esempio, l'app di esempio dell'interfaccia a riga di comando inclusa con 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 di otInstance è un aspetto 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 API OpenThread.

Ad esempio, l'istanza OpenThread è 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 per piattaforma

Se vuoi aggiungere funzioni specifiche per la 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. 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 utilizzeremo per agganciare i 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 le seguenti dichiarazioni di funzioni 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 prossimo passaggio.

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. Dipende dalle esigenze della tua applicazione. Se non ne hai bisogno nell'implementazione della funzione, puoi utilizzare la macro OT_UNUSED_VARIABLE dell'API OpenThread per eliminare gli errori di build relativi alle variabili inutilizzate per alcune catene di strumenti. Ne vedremo alcuni esempi in seguito.

5. Implementare 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 queste funzioni per la piattaforma nRF52840. In questo codice dovrai aggiungere funzioni che:

  • Inizializza PIN e modalità di GPIO
  • Controllare la tensione di un pin
  • Attivare le interruzioni GPIO e registrare un callback

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

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

AZIONE: Aggiungi le definizioni.

Questi valori fungono da astrazioni tra valori specifici di nRF52840 e variabili utilizzate 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 nRF52840, consulta il nostro centro informazioni di Norddic Semiconductor.

AZIONE: aggiunge l'intestazione include.

Ora aggiungi l'intestazione 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 il codice qui sotto. La funzione in_pin1_handler è il callback che viene registrato quando la funzionalità di pressione del pulsante viene inizializzata (più avanti in questo file).

Nota come questo callback utilizza la macro OT_UNUSED_VARIABLE, in quanto 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;
}

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 viene 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 lavagna per premere un pulsante, mentre 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 gpio.c file.

6. API: reazione alle modifiche dei ruoli del dispositivo

Nella nostra applicazione, vogliamo che si accendano diversi LED a seconda del ruolo del dispositivo. Tracciamo i seguenti ruoli: leader, router, dispositivo finale. Possiamo assegnarli a LED così:

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

Per abilitare questa funzionalità, l'applicazione deve sapere quando è stato modificato il ruolo del dispositivo e come attivare il LED corretto. Useremo 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: aggiunge l'intestazione include.

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>

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

Aggiungi questa dichiarazione a main.c, dopo l'inclusione 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 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 di OpenThread cambia.

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

ACTION: 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 di OpenThread e, se è cambiata, attiva/disattiva i LED, se necessario.

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

Nella nostra applicazione, vogliamo anche inviare messaggi UDP a tutti gli altri dispositivi della rete quando Pulsante1 è premuto su una lavagna. Per confermare la ricezione del messaggio, risponderemo a LED4 sulle altre bacheche.

Per abilitare questa funzionalità, l'applicazione deve:

  • Inizializza una connessione UDP all'avvio
  • Essere in grado di inviare un messaggio UDP all'indirizzo multicast mesh-locale
  • Gestisci 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: aggiunge l'intestazione include.

Nella sezione "Includi" nella parte superiore del file main.c, aggiungi i file di intestazione dell'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 correttamente gli errori.

AZIONE: Aggiungi le definizioni e le costanti:

Nel file main.c, dopo la sezione "Includi" e prima di eventuali istruzioni #if, aggiungi costanti specifiche dell'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. Tutti i messaggi inviati a questo indirizzo verranno inviati a tutti i dispositivi con thread completo della rete. Per ulteriori informazioni sul supporto 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 le funzioni specifiche dell'UDP, nonché 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;

AZIONE: Aggiungi chiamate per inizializzare i pulsanti e i LED GPIO.

In main.c, aggiungi queste chiamate 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 push del pulsante.

/* 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. Senza questo metodo, il dispositivo non può 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 loop di while. Questa funzione, dichiarata in gpio.c, controlla se il pulsante è stato premuto e, in questo caso, chiama il gestore (handleButtonInterrupt) impostato nel passaggio precedente.

otSysButtonProcess(instance);

AZIONE: Implementa il gestore di interruzioni dei pulsanti.

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

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

AZIONE: Implementare l'inizializzazione di 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 definita in precedenza (1212). La funzione otUdpOpen apre il socket e registra una funzione di callback (handleUdpReceive) per la ricezione di un messaggio UDP. otUdpBind associa il socket all'interfaccia di rete Thread passando OT_NETIF_THREAD. Per altre opzioni di interfaccia di rete, fai riferimento all'enumerazione di otNetifIdentifier nella sezione Riferimento API UDP.

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

Osserva le macro otEXPECT e otEXPECT_ACTION. Questi messaggi assicurano che il messaggio UDP sia valido e allocato correttamente nel buffer; in caso contrario, la funzione gestisce agevolmente gli errori passando al blocco exit, che libera 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 che hai appena aggiunto. Questa funzione attiva/disattiva semplicemente 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 immediatamente Thread e si connettano a una rete quando sono accesi. Per farlo, utilizzeremo la struttura otOperationalDataset. Questa struttura contiene tutti i parametri necessari per trasmettere le credenziali della rete Thread a un dispositivo.

L'uso di questa struttura sostituirà le impostazioni predefinite di rete integrate in OpenThread, per rendere la nostra applicazione più sicura e limitare i nodi Thread della 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

ACTION: aggiungi intestazione include.

Nella sezione "Includi" della parte superiore del file main.c, aggiungi il file di intestazione API che dovrai configurare per la rete Thread:

#include <openthread/dataset_ftd.h>

ACTION: aggiungi una dichiarazione di funzione per impostare la configurazione di rete.

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

static void setNetworkConfiguration(otInstance *aInstance);

ACTION: aggiungi la chiamata di configurazione della rete.

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

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

AZIONE: aggiungi le chiamate per abilitare l'interfaccia e lo stack della rete Thread.

In main.c, aggiungi queste chiamate 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: Implementare 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 in dettaglio nella funzione, i parametri della rete Thread che usiamo 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 trentolio dei router, così i nostri dispositivi cambiano ruolo più velocemente per scopi dimostrativi. Tieni presente che questa operazione viene eseguita solo se il nodo è FTD (Full Thread Device). Trovi ulteriori informazioni nel prossimo passaggio.

9. API: funzioni limitate

Alcune API di OpenThread's 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 usa OpenThread.

Ad esempio, la funzione otThreadSetRouterSelectionJitter regola il tempo (in secondi) necessario per la promozione di un Dispositivo finale a un Router. Il valore predefinito di questo valore, secondo la specifica del thread, è 120. Per facilità d'uso in questo codelab, dovremo cambiarlo in 20, per cui non dovrai aspettare molto tempo prima che un nodo Thread cambi ruolo.

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

Puoi verificarlo esaminando la definizione della funzione otThreadSetRouterSelectionJitter, contenuta all'interno di un'istruzione di pre-responsabile 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. Aggiorna

Prima di creare l'applicazione, sono necessari alcuni aggiornamenti minori per tre file CMake. che 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 i flag al CMakeLists.txt file.

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 di gpio.c:

ACTION: Aggiungi la fonte gpio al ./src/CMakeLists.txt file.

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 NordicSemiconductor, in modo che sia incluso nella build della libreria dei driver Nordic.

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

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. Configurare i dispositivi

Dopo aver completato tutti gli aggiornamenti del codice, puoi creare l'applicazione e eseguirne il flashing su tutte e tre le schede di sviluppo Nordic nRF52840. Ciascun dispositivo funzionerà come un dispositivo FTD (Full Thread Device).

Crea OpenThread

Crea i programmi binari FTD OpenThread per la piattaforma nRF52840.

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

Passa alla directory con il file binario dell'interfaccia a riga di comando OpenThread FTD e convertila in formato esadecimale con ARM Embedded Toolchain:

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

Lavagne lampeggianti

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. Impostato correttamente, il 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 lampeggia il file esadecimale CLI FTD 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

La spia LED5 si spegne brevemente durante il lampeggiamento. 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 questa operazione "Flash: le bacheche" passaggio per le altre due bacheche. Ogni scheda deve essere collegata alla macchina Linux allo stesso modo e il comando per lampeggiare è lo stesso, tranne per il numero di serie della lavagna. Assicurati di utilizzare il numero di serie univoco di ogni lavagna nel

nrfjprog Comando lampeggiante.

In caso di esito positivo, su ogni lavagna verrà acceso il LED1, il LED2 o il LED3. Potresti anche vedere l'interruttore LED illuminato da 3 a 2 (o 2 a 1) subito dopo aver lampeggiato (la funzionalità di modifica del ruolo del dispositivo).

12. Funzionalità dell'applicazione

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

Indicatori del ruolo dei dispositivi

Il LED illuminato su ogni lavagna riflette il ruolo attuale del nodo Thread:

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

Man mano che cambia il ruolo, cambia anche il LED illuminato. Dovresti aver già visto queste modifiche su una o due schede entro 20 secondi dall'accensione del dispositivo.

Multicast UDP

Quando viene premuto il 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 alla ricezione di questo messaggio, il servizio LED4 in tutte le altre schede si attiva o disattiva. LED4 rimane attivo o disattivato per ogni lavagna fino a quando non riceve un altro messaggio UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demo: Osservare le modifiche ai ruoli del dispositivo

I dispositivi che hai lampeggiato sono un tipo specifico di dispositivo Full Thread (FTD) chiamato dispositivo idoneo del router (REED). Ciò significa che possono funzionare come router o dispositivo finale e possono promuoversi da dispositivo finale a router.

Il 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 al router. Questa modifica deve essere apportata in modo casuale entro il numero di secondi indicato nell'applicazione per cui hai impostato il valore otThreadSetRouterSelectionJitter (20 secondi).

Ogni rete Thread dispone anche di 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 loro dovrebbe essere un Leader (LED1 acceso) e gli altri due dovrebbero essere Router (LED2 accesi).

4e1e885861a66570.png

Rimuovi il leader

Se il leader viene rimosso dalla rete Thread, un altro router promuove il suo ruolo di Leader, per assicurarsi che la rete abbia ancora un leader.

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

4c57c87adb40e0e3.png

Riattiva la scheda leader originale. Dovrebbe riconnettersi automaticamente alla rete Thread come dispositivo finale (LED3 è acceso). Entro 20 secondi (il parametro di selezione del router) si promuove al router (il LED 2 è acceso).

5f40afca2dcc4b5b.png

Ripristina le tavole

Spegni tutte e tre le tavole, quindi riaccendile e osserva i LED. Il primo router alimentato deve avere il ruolo di Leader (LED1 è acceso): il primo router in una rete Thread diventa automaticamente il leader.

Le altre due schede si collegano inizialmente alla rete come dispositivi finali (LED3 è acceso) ma dovrebbero promuoversi ai router (LED2 è acceso) entro 20 secondi.

Partizioni di rete

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

Il thread è in riparazione automatica, quindi le partizioni devono unire di nuovo una singola partizione con un solo leader.

14. Demo: invio multicast UDP

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

Scegli una lavagna e premi il pulsante 1. Il LED4 su tutte le altre schede della rete Thread in esecuzione sull'applicazione dovrebbe attivarne lo stato. Se continui dall'esercizio precedente, dovrebbero essere attivi.

f186a2618fdbe3fd.png

Premi di nuovo Pulsante1 per la stessa lavagna. Su LED tutte le altre schede, il LED4 dovrebbe attivarsi di nuovo.

Premi Pulsante 1 su un'altra lavagna e osserva come LED4 si attiva sulle altre schede. Premi Pulsante 1 su una delle bacheche in cui il LED4 è attualmente acceso. LED4 rimane attivo per la scheda, ma attiva/disattiva l'altra.

f5865ccb8ab7aa34.png

Partizioni di rete

Se le tue bacheche sono partizionate e tra loro c'è più di un leader, il risultato del messaggio multicast sarà diverso tra le bacheche. Se premi Pulsante1 su una lavagna partizionata (e pertanto è l'unico membro della rete Thread partizionata), il LED4 sulle altre lavagne non si accenderà in risposta. In questo caso, reimposta le bacheche, idealmente queste riformeranno una singola rete Thread e i messaggi UDP dovrebbero funzionare correttamente.

15. Complimenti!

Hai creato un'applicazione che utilizza le API OpenThread.

Ora sai:

  • Come programmare i pulsanti e i LED sulle bacheche Nordic nRF52840
  • Come utilizzare le API OpenThread comuni e la classe otInstance
  • Come monitorare e reagire ai cambiamenti di stato di OpenThread
  • Come inviare messaggi UDP a tutti i dispositivi in una rete Thread
  • Come modificare i file di marca

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 connetti i LED RGB esterni che cambiano colore in base al ruolo del router
  • Aggiungi il supporto GPIO per un'altra piattaforma di esempio
  • Anziché utilizzare il multicast per inviare un ping a tutti i dispositivi premendo il tasto, usa l'API Router/Leader per individuare e inviare un ping ai singoli dispositivi
  • Connettere la tua rete mesh a Internet utilizzando un router di confine ThreadThread e moltiplicarli dall'esterno della rete Thread per illuminare i LED.

Per approfondire

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

Riferimento: