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

La specifica Thread definisce un protocollo di comunicazione da dispositivo a dispositivo wireless affidabile, sicuro e a basso consumo per le applicazioni domestiche. OpenThread implementa tutti i livelli di networking 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 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 pulsanti e LED sulle schede di sviluppo 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 di una rete Thread
  • Come modificare i Makefile

Che cosa ti serve

Hardware:

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

Software:

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

Salvo quanto diversamente specificato, 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. Come iniziare

Completa il codelab sull'hardware

Prima di iniziare questo codelab, devi completare la procedura Build a Thread Network with nRF52840 Boards and OpenThread, che:

  • Dettagli di tutto il software necessario per la creazione e il flashing
  • Ti insegna come costruire OpenThread e Flash su schede Nordic nRF52840
  • Dimostra le nozioni di base di una rete Thread

In questo codelab non è richiesto nessuno degli ambienti configurati per la creazione di OpenThread e il flashing delle lavagne, 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 Thread. Tutti i passaggi sono stati testati su Ubuntu 14.04.5 LTS (Trusty Tahr).

Schede Nordic Semiconductor nRF52840

Questo codelab utilizza tre schede nPD52840 PDK.

a6693da3ce213856.png

Installare software

Per creare e flashare OpenThread, devi installare SEGGER J-Link, gli strumenti a riga di comando nRF5x, la toolchain GRM URM e vari pacchetti Linux. Se hai completato il codelab relativo alla creazione di una rete Thread, se necessario, avrai già tutto ciò che ti serve. In caso contrario, completa il codelab prima di continuare per assicurarti di poter creare e flashare OpenThread sulle schede di sviluppo nRF52840.

3. Clona il repository

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

Clona il repository di esempi OpenThread Nordic nRF528xx 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 in ./openthread/include/openthread nel repository OpenThread. Queste API forniscono l'accesso a una serie di caratteristiche e funzionalità di OpenThread sia a livello di piattaforma che a livello di piattaforma da utilizzare 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 del router di confine
  • Funzionalità avanzate come la supervisione dei minori e il rilevamento di Jam

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

usando 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 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 otInstance verrà utilizzata 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 viene inizializzata nella funzione main() dell'app di esempio dell'interfaccia a riga di comando:

./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 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 della piattaforma. In questo modo, puoi utilizzare le stesse intestazioni delle funzioni per altre piattaforme di esempio.

Ad esempio, le funzioni GPIO che utilizzeremo per collegare 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 queste dichiarazioni di funzioni dopo il #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 ne hai bisogno per l'implementazione della funzione, puoi usare la macro OT_UNUSED_VARIABLE dell'API OpenThread per eliminare gli errori di compilazione 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 delle 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. Nel codice aggiungerai le funzioni che:

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

che definiscono come 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 Centro informazioni per i semiconduttori nordici.

AZIONE: aggiungi intestazione.

Ora aggiungi 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 funzioni di callback e interruzione per il pulsante 1.

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

Nota come questo callback usa la macro OT_UNUSED_VARIABLE, dato che le variabili passate a in_pin1_handler non sono effettivamente usate 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 i pulsanti.

La prima funzione inizializza la lavagna per un tasto premuto e la seconda invia il messaggio UDP multicast quando si preme 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: reagisci alle modifiche dei ruoli dei dispositivi

Nella nostra applicazione vogliamo che si accendano LED diversi a seconda del ruolo del dispositivo. Tracciamo i seguenti ruoli: leader, router, dispositivo finale. Possiamo assegnare i LED in questo modo:

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

Per abilitare questa funzionalità, l'applicazione deve sapere quando il ruolo del dispositivo cambia e come si accende il LED corretto in risposta. Per la prima parte utilizzeremo l'istanza OpenThread e la seconda l'astrazione della piattaforma GPIO.

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

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

AZIONE: aggiungi intestazione.

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

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

AZIONE: aggiungi una dichiarazione di funzione gestore 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 verrà definita dopo l'applicazione principale.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

AZIONE: aggiungi una registrazione per il callback per la funzione del gestore di modifica 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, accende/spegne i LED a seconda delle 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 la tecnologia multicast per accendere un LED

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

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
  • 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.

Nella sezione Include, 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 include e prima di eventuali istruzioni #if, aggiungi costanti specifiche per 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-local. Tutti i messaggi inviati a questo indirizzo verranno inviati a tutti i dispositivi con thread completo nella rete. Per ulteriori informazioni sul supporto della tecnologia multicast in OpenThread, consulta la sezione Multicast su openthread.io.

AZIONE: aggiungi dichiarazioni di funzioni.

Nel file main.c, dopo la definizione del tag otTaskletsSignalPending e prima della funzione main(), aggiungi funzioni specifiche di 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 LED GPIO e il relativo pulsante.

In main.c, aggiungi queste chiamate 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 push 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 sia inizializzato all'avvio dell'applicazione. In caso contrario, 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 while. Questa funzione, dichiarata in gpio.c, controlla se è stato premuto un pulsante e, in questo caso, chiama il gestore (handleButtonInterrupt) che è stato 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: 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 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 in Riferimento API UDP.

ACTION: 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. Questi elementi assicurano che il messaggio UDP sia valido e allocato correttamente nel buffer e, in caso contrario, la funzione gestisce gli errori in modo controllato passando al blocco exit, dove 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: configura la rete Thread

Per semplificare la dimostrazione, vogliamo che i nostri dispositivi avviino immediatamente 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 Thread a un dispositivo.

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

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 Includi.

Nella sezione includi, 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>

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 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 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 chiamate per abilitare l'interfaccia e lo stack di rete Thread.

In main.c, aggiungi queste chiamate 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 di 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 di Thread che utilizziamo per questa applicazione sono:

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

Inoltre, è in questa sezione che diminuiamo il valore di Jitter della selezione del router, in modo che i nostri dispositivi cambino i ruoli più rapidamente per scopi demo. Tieni presente che questa operazione viene eseguita solo se il nodo è un FTD (Full Thread Device). Trovi ulteriori informazioni nel passaggio successivo.

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 utilizzando OpenThread.

Ad esempio, la funzione otThreadSetRouterSelectionJitter regola il tempo (in secondi) necessario per la promozione di un dispositivo finale su un router. Il valore predefinito per questo valore è 120, secondo le specifiche dei thread. Per facilitarne l'utilizzo in questo codelab, lo modificheremo a 20, in modo da non dover aspettare molto perché un nodo Thread modifichi i ruoli.

Nota: i dispositivi MTD non diventano router e il supporto per 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 build.

Puoi verificarlo esaminando la definizione della funzione otThreadSetRouterSelectionJitter, contenuta in 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. Aggiornamenti di CMake

Prima di creare l'applicazione, sono necessari alcuni aggiornamenti di minore entità per 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 a NordicSemiconductor CMakeLists.txt, per assicurarti che le funzioni GPIO siano definite nell'applicazione.

ACTION: aggiungi flag 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 GPI 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 NordicSemiconductor, in modo che venga incluso nella build della libreria dei driver Nordic.

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

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

Dopo aver completato tutti gli aggiornamenti del codice, puoi creare e collegare l'applicazione a tutte e tre le schede di sviluppo Nordic nRF52840. Ciascun dispositivo funzionerà come dispositivo FThread completo.

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

Vai alla directory con il programma binario dell'interfaccia a riga di comando OpenThread FTD e convertilo in formato esadecimale con la funzionalità Toolchain incorporata di ARM:

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

Fai lampeggiare le lavagne

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 sulla scheda nRF52840, quindi collegalo alla macchina Linux. Imposta correttamente il LED5 è acceso.

20a3b4b480356447.png

Come in precedenza, tieni presente il numero di serie della scheda nRF52840:

c00d519ebec7e5f0.jpeg

Vai alla posizione degli strumenti a riga di comando nRFx e inserisci il file esadecimale FLI di OpenThread CLI 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 spegnerà brevemente durante il lampeggiamento. Al termine dell'operazione viene generato l'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 lavagne" per le altre due lavagne. Ogni lavagna deve essere collegata alla macchina Linux allo stesso modo e il comando di Flash è lo stesso, ad eccezione del 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 ciascuna scheda si accenderanno LED1, LED2 o LED3. Potresti anche vedere l'interruttore LED acceso da 3 a 2 (o 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 devono essere alimentate e che eseguono la nostra applicazione OpenThread. Come descritto in precedenza, questa applicazione ha due funzionalità principali.

Indicatori dei ruoli del dispositivo

Il LED acceso su ogni scheda riflette l'attuale ruolo del nodo Thread:

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

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

Multicast UDP

Quando premi 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, LED4 su tutte le altre schede attiva o disattiva. La spia LED4 rimane accesa o spenta per ogni scheda finché non riceve un altro messaggio UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demo: Osserva i cambiamenti del ruolo del dispositivo

I dispositivi che hai flashizzato sono un tipo specifico di dispositivo FTD (Full Thread Device) chiamato dispositivo idoneo per router (REED). Ciò significa che possono funzionare sia come router sia come 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. Questa modifica deve essere eseguita in modo casuale entro il numero di secondi impostato sul valore otThreadSetRouterSelectionJitter nell'applicazione (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 questi 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 si autopromuove un Leader, per assicurarsi che la rete abbia ancora un leader.

Spegni la scheda Leader (quella con LED1 acceso) utilizzando l'interruttore di accensione. Attendi circa 20 secondi. In una delle due schede rimanenti, il LED2 (Router) si spegnerà e il LED1 (Leader) si accenderà. Ora questo dispositivo è il leader della rete Thread.

4c57c87adb40e0e3.png

Riattiva la lavagna Leader originale. Dovrebbe riconnettersi automaticamente alla rete Thread come dispositivo finale (il LED 3 è acceso). Entro 20 secondi (il Router Selection Jitter) si autopromuove un router (LED2 è acceso).

5f40afca2dcc4b5b.png

Ripristina le lavagne

Spegni tutte e tre le schede, quindi riaccendile e osserva i LED. La prima scheda alimentata deve avere il ruolo Leader (LED 1 acceso) - Il primo router in una rete Thread diventa automaticamente il leader.

Le altre due schede si connettono inizialmente alla rete come dispositivi finali (LED3 accesi), 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 mostrato come Leader.

Il thread si ripara automaticamente, quindi le partizioni alla fine devono essere unite nuovamente in un'unica partizione con un solo leader.

14. Demo: invia multicast UDP

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

Scegli una lavagna e premi il pulsante 1. Il LED 4 su tutte le altre schede nella rete Thread che esegue l'applicazione dovrebbe attivare/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 schede dovrebbe alternare.

Premi il pulsante 1 su una scheda diversa e osserva come il LED4 si alterna sulle altre schede. Premere il pulsante 1 su una delle schede su cui è attualmente acceso il LED 4. Il LED4 rimane acceso per quella scheda, ma si attiva sugli altri.

f5865ccb8ab7aa34.png

Partizioni di rete

Se le tue lavagne sono partizionate e c'è più di una di queste, il risultato del messaggio multicast sarà diverso tra le varie schede. 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: preferibilmente, riformeranno una singola rete Thread e i messaggi UDP dovrebbero funzionare correttamente.

15. Complimenti.

Hai creato un'applicazione che utilizza API OpenThread.

Ora sai:

  • Come programmare pulsanti e LED sulle schede di sviluppo 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 di una rete Thread
  • Come modificare i Makefile

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 collegare 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 ping a tutti i dispositivi dalla pressione di un pulsante, usa l'API Router/Leader per individuare e inviare un ping a un singolo dispositivo.
  • Connetti la tua rete mesh a Internet utilizzando un router di confine OpenThread e trasmettili dall'esterno della rete Thread per illuminare i LED.

Per approfondire

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

Riferimento: