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 al fine di accelerare lo sviluppo di prodotti per la casa connessa.

La specifica Thread definisce un protocollo di comunicazione tra dispositivi wireless basato su IPv6 affidabile, sicuro e a basso consumo per le applicazioni domestiche. OpenThread implementa tutti i livelli di rete Thread inclusi IPv6, 6LoWPAN, IEEE 802.15.4 con sicurezza MAC, rete 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, inviare messaggi UDP e collegare queste azioni a pulsanti e LED su hardware reale.

2a6db2e258c32237.png

Obiettivi didattici

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

Che cosa ti serve

Hardware:

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

Software:

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

Salvo quanto diversamente indicato, i contenuti di questo Codelab sono disponibili 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 iniziare

Completa il codelab hardware

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

  • Descrive tutti i software necessari per la creazione e il flashing
  • Ti insegna a sviluppare OpenThread e a eseguire il flashing sulle schede nordiche nRF52840
  • Illustra le nozioni di base di una rete Thread

In questo codelab non è descritta la configurazione dell'ambiente necessaria per creare OpenThread e eseguire il flashing delle schede, ma solo le istruzioni di base per il flashing delle schede. Si presume che tu abbia già completato il codelab sulla creazione di una rete Thread.

Macchina Linux

Questo codelab è stato progettato per utilizzare un computer Linux basato 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 nRF52840 di Nordic Semiconductor

Questo codelab utilizza tre schede nRF52840 PDK.

a6693da3ce213856.png

Installa software

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

3. clona il repository

OpenThread include un codice dell'applicazione di esempio che puoi usare 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 all'indirizzo ./openthread/include/openthread nel repository OpenThread. Queste API forniscono accesso a una varietà di caratteristiche e funzionalità OpenThread sia a livello di Thread che di piattaforma da utilizzare nelle tue applicazioni:

  • Informazioni e controllo dell'istanza OpenThread
  • Servizi per le applicazioni come IPv6, UDP e CoAP
  • Gestione delle credenziali di rete, insieme ai ruoli Commissioner e Joiner
  • Gestione del router di confine
  • Funzionalità avanzate come la supervisione dei bambini e il rilevamento dei 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 desiderata.

Ad esempio, l'app di esempio 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 è che utilizzerai di frequente quando lavori con le API OpenThread. Una volta inizializzata, questa struttura rappresenta un'istanza statica della libreria OpenThread e consente all'utente di effettuare chiamate 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 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. Poi implementale in un file di origine specifico della piattaforma. In questo modo puoi utilizzare le stesse intestazioni di funzione per altre piattaforme di esempio.

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

Apri il file ./openthread/examples/platforms/openthread-system.h nel tuo editor di testo preferito.

./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 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 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 toolchain. Vedremo alcuni esempi più avanti.

5. Implementazione dell'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 ai pulsanti e ai LED sulle schede dev nRF52840, è necessario implementare queste funzioni per la piattaforma nRF52840. In questo codice, aggiungerai funzioni che:

  • Inizializzare pin e modalità GPIO
  • Controlla la tensione di un pin
  • Attiva gli interrupt 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.

Queste definizioni fungono da astrazioni tra i valori specifici di nRF52840 e le 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 sui semiconduttori nordici.

AZIONE: Aggiungi intestazione include.

Poi, aggiungi l'intestazione per includere il necessario 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 successivamente. La funzione in_pin1_handler è il callback che viene registrato quando viene inizializzata la funzionalità di pressione dei pulsanti (più avanti in questo file).

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

/* Declaring callback function for button 1. */
static otSysButtonCallback sButtonHandler;
static bool                sButtonPressed;

/**
 * @brief Function to receive interrupt and call back function
 * set by the application for button 1.
 *
 */
static void in_pin1_handler(uint32_t pin, nrf_gpiote_polarity_t action)
{
    OT_UNUSED_VARIABLE(pin);
    OT_UNUSED_VARIABLE(action);
    sButtonPressed = true;
}

AZIONE: aggiungi una funzione per configurare i LED.

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

/**
 * @brief Function for configuring: PIN_IN pin for input, PIN_OUT pin for output,
 * and configures GPIOTE to give an interrupt on pin change.
 */

void otSysLedInit(void)
{
    /* Configure GPIO mode: output */
    nrf_gpio_cfg_output(LED_1_PIN);
    nrf_gpio_cfg_output(LED_2_PIN);
    nrf_gpio_cfg_output(LED_3_PIN);
    nrf_gpio_cfg_output(LED_4_PIN);

    /* Clear all output first */
    nrf_gpio_pin_write(LED_1_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_2_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_3_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_4_PIN, GPIO_LOGIC_LOW);

    /* Initialize gpiote for button(s) input.
     Button event handlers are set in the application (main.c) */
    ret_code_t err_code;
    err_code = nrfx_gpiote_init();
    APP_ERROR_CHECK(err_code);
}

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

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

/**
 * @brief Function to set the mode of an LED.
 */

void otSysLedSet(uint8_t aLed, bool aOn)
{
    switch (aLed)
    {
    case 1:
        nrf_gpio_pin_write(LED_1_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 2:
        nrf_gpio_pin_write(LED_2_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 3:
        nrf_gpio_pin_write(LED_3_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 4:
        nrf_gpio_pin_write(LED_4_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    }
}

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

Questa funzione verrà utilizzata per attivare/disattivare il LED4 quando il dispositivo riceve un messaggio UDP multicast.

/**
 * @brief Function to toggle the mode of an LED.
 */
void otSysLedToggle(uint8_t aLed)
{
    switch (aLed)
    {
    case 1:
        nrf_gpio_pin_toggle(LED_1_PIN);
        break;
    case 2:
        nrf_gpio_pin_toggle(LED_2_PIN);
        break;
    case 3:
        nrf_gpio_pin_toggle(LED_3_PIN);
        break;
    case 4:
        nrf_gpio_pin_toggle(LED_4_PIN);
        break;
    }
}

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

La prima funzione inizializza la scheda quando viene premuto 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 file gpio.c .

6. API: reagire alle modifiche dei ruoli del dispositivo

Nella nostra applicazione, vogliamo che si accendano LED diversi a seconda del ruolo del dispositivo. Teniamo traccia dei seguenti ruoli: Leader, Router, Dispositivo finale. Possiamo assegnarli ai LED in questo modo:

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

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

Apri il file ./openthread/examples/apps/cli/main.c nel tuo editor di testo preferito.

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

AZIONE: Aggiungi intestazione include.

Nella sezione include 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 una dichiarazione della funzione gestore per la modifica dello stato dell'istanza OpenThread.

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

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

AZIONE: aggiungi una registrazione di callback per la funzione di gestore delle modifiche 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 è cambiato, attiva/disattiva i LED in base alle esigenze.

void handleNetifStateChanged(uint32_t aFlags, void *aContext)
{
   if ((aFlags & OT_CHANGED_THREAD_ROLE) != 0)
   {
       otDeviceRole changedRole = otThreadGetDeviceRole(aContext);

       switch (changedRole)
       {
       case OT_DEVICE_ROLE_LEADER:
           otSysLedSet(1, true);
           otSysLedSet(2, false);
           otSysLedSet(3, false);
           break;

       case OT_DEVICE_ROLE_ROUTER:
           otSysLedSet(1, false);
           otSysLedSet(2, true);
           otSysLedSet(3, false);
           break;

       case OT_DEVICE_ROLE_CHILD:
           otSysLedSet(1, false);
           otSysLedSet(2, false);
           otSysLedSet(3, true);
           break;

       case OT_DEVICE_ROLE_DETACHED:
       case OT_DEVICE_ROLE_DISABLED:
           /* Clear LED4 if Thread is not enabled. */
           otSysLedSet(4, false);
           break;
        }
    }
}

7. API: usare il multicast per accendere il LED

Nella nostra applicazione, vogliamo anche inviare messaggi UDP a tutti gli altri dispositivi nella rete quando viene premuto il pulsante1 su una scheda. Per confermare la ricezione del messaggio, di conseguenza attiveremo il LED4 sulle altre schede.

Per abilitare questa funzionalità, l'applicazione deve:

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

Apri il file ./openthread/examples/apps/cli/main.c nel tuo editor di testo preferito.

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

AZIONE: Aggiungi intestazione include.

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 correttamente gli errori.

AZIONE: aggiungi definizioni e costanti:

Nel file main.c, dopo la sezione include e prima di qualsiasi istruzione #if, aggiungi costanti specifiche UDP e definisce:

#define UDP_PORT 1212

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

ff03::1 è l'indirizzo multicast locale- mesh. Tutti i messaggi inviati a questo indirizzo verranno inviati a tutti i dispositivi Thread completi nella rete. Vedi Multicast su openthread.io per ulteriori informazioni sul supporto del multicast in OpenThread.

AZIONE: aggiungi dichiarazioni di funzione.

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

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

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

AZIONE: aggiungi la chiamata di inizializzazione UDP.

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

initUdp(instance);

Questa chiamata garantisce che un socket UDP venga inizializzato all'avvio dell'applicazione. Senza questo segnale, 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 di funzione alla funzione main() dopo la chiamata otSysProcessDrivers, nel loop while. Questa funzione, dichiarata in gpio.c, controlla se il pulsante è stato premuto e, in questo caso, chiama il gestore (handleButtonInterrupt) impostato nel passaggio precedente.

otSysButtonProcess(instance);

AZIONE: implementa il gestore di interruzione dei pulsanti.

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

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

AZIONE: implementa l'inizializzazione UDP.

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

/**
 * Initialize UDP socket
 */
void initUdp(otInstance *aInstance)
{
    otSockAddr  listenSockAddr;

    memset(&sUdpSocket, 0, sizeof(sUdpSocket));
    memset(&listenSockAddr, 0, sizeof(listenSockAddr));

    listenSockAddr.mPort    = UDP_PORT;

    otUdpOpen(aInstance, &sUdpSocket, handleUdpReceive, aInstance);
    otUdpBind(aInstance, &sUdpSocket, &listenSockAddr, OT_NETIF_THREAD);
}

UDP_PORT è la porta 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 dell'interfaccia di rete, consulta l'enumerazione di otNetifIdentifier nel Riferimento API UDP.

AZIONE: Implementa la messaggistica UDP.

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

/**
 * Send a UDP datagram
 */
void sendUdp(otInstance *aInstance)
{
    otError       error = OT_ERROR_NONE;
    otMessage *   message;
    otMessageInfo messageInfo;
    otIp6Address  destinationAddr;

    memset(&messageInfo, 0, sizeof(messageInfo));

    otIp6AddressFromString(UDP_DEST_ADDR, &destinationAddr);
    messageInfo.mPeerAddr    = destinationAddr;
    messageInfo.mPeerPort    = UDP_PORT;

    message = otUdpNewMessage(aInstance, NULL);
    otEXPECT_ACTION(message != NULL, error = OT_ERROR_NO_BUFS);

    error = otMessageAppend(message, UDP_PAYLOAD, sizeof(UDP_PAYLOAD));
    otEXPECT(error == OT_ERROR_NONE);

    error = otUdpSend(aInstance, &sUdpSocket, message, &messageInfo);

 exit:
    if (error != OT_ERROR_NONE && message != NULL)
    {
        otMessageFree(message);
    }
}

Osserva le macro otEXPECT e otEXPECT_ACTION. Questo assicura che il messaggio UDP sia valido e allocato correttamente nel buffer. In caso contrario, la funzione gestisce correttamente gli errori passando al blocco exit, dove libera il buffer.

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

AZIONE: implementa la gestione dei messaggi UDP.

In main.c, aggiungi l'implementazione della funzione handleUdpReceive dopo la funzione sendUdp appena aggiunta. 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 facilitare la dimostrazione, vogliamo che i nostri dispositivi avviino immediatamente Thread e si uniscano in una rete quando sono accesi. Per farlo, 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 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.

Anche in questo caso, apri il file ./openthread/examples/apps/cli/main.c nel tuo editor di testo preferito.

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

AZIONE: Aggiungi intestazione di inclusione.

All'interno della sezione Include all'inizio del file main.c, aggiungi il file di intestazione API che ti servirà per configurare la rete Thread:

#include <openthread/dataset_ftd.h>

AZIONE: è stata aggiunta una dichiarazione di funzione per impostare la configurazione di rete.

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

static void setNetworkConfiguration(otInstance *aInstance);

AZIONE: aggiungi la chiamata di configurazione di rete.

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

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

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

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

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

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

AZIONE: implementa la configurazione 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 in dettaglio nella funzione, i parametri di 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, in questo caso riduciamo il tremolio di selezione del router, in modo che i nostri dispositivi cambino ruolo più rapidamente per scopi dimostrativi. Tieni presente che questa operazione viene eseguita solo se il nodo è un dispositivo 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 nel deployment di produzione di un'applicazione che utilizza OpenThread.

Ad esempio, la funzione otThreadSetRouterSelectionJitter regola il tempo (in secondi) necessario a un dispositivo finale per promuoversi a router. Il valore predefinito per questo valore è 120, secondo la specifica del Thread. Per facilitare l'uso in questo codelab, modificheremo il valore impostandolo su 20, in modo da non dover aspettare molto perché un nodo Thread cambi ruolo.

Nota: i dispositivi MTD non diventano router e il supporto per una funzione come otThreadSetRouterSelectionJitter non è incluso nella build MTD. In seguito dobbiamo specificare l'opzione CMake -DOT_MTD=OFF, altrimenti riscontreremo un errore nella build.

Puoi verificarlo osservando la definizione della funzione otThreadSetRouterSelectionJitter, che è 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 CMake

Prima di creare l'applicazione, sono necessari alcuni aggiornamenti di minore entità per tre file CMake. Queste vengono usate dal sistema di compilazione per compilare e collegare la tua 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 flag al file CMakeLists.txt .

Apri ./third_party/NordicSemiconductor/CMakeLists.txt nel tuo editor di testo preferito e aggiungi le seguenti righe nella sezione COMMON_FLAG.

...
set(COMMON_FLAG
    -DSPIS_ENABLED=1
    -DSPIS0_ENABLED=1
    -DNRFX_SPIS_ENABLED=1
    -DNRFX_SPIS0_ENABLED=1
    ...

    # Defined in ./third_party/NordicSemiconductor/nrfx/templates/nRF52840/nrfx_config.h
    -DGPIOTE_ENABLED=1
    -DGPIOTE_CONFIG_IRQ_PRIORITY=7
    -DGPIOTE_CONFIG_NUM_OF_LOW_POWER_EVENTS=1
)

...

./src/CMakeLists.txt

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

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

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

...

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

...

./third_party/NordicSemiconductor/CMakeLists.txt

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

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

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

...

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

11. Configura i dispositivi

Con tutti gli aggiornamenti del codice fatti, sei pronto per sviluppare e eseguire il flashing dell'applicazione su tutte e tre le schede dev nRF52840 nordiche. Ogni dispositivo funzionerà come un dispositivo FTD (Full Thread Device).

Creazione di OpenThread

Crea i file 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

Accedi alla directory con il file binario dell'interfaccia a riga di comando FTD OpenThread 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

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 piedino di alimentazione esterna sulla scheda nRF52840, quindi collegalo al computer Linux. Impostato correttamente, il LED5 è acceso.

20a3b4b480356447.png

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

c00d519ebec7e5f0.jpeg

Accedi alla posizione degli strumenti a riga di comando nRFx e esegui il flashing del file esadecimale FTD 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 spegne brevemente durante il lampeggiamento. Se l'operazione riesce, viene generato il seguente output:

Parsing hex file.
Erasing user available code and UICR flash areas.
Applying system reset.
Checking that the area to write is not protected.
Programing device.
Applying system reset.
Run.

Ripeti questo "Flash the board" per le altre due bacheche. Ogni scheda deve essere connessa alla macchina Linux allo stesso modo e il comando per Flash è lo stesso, ad eccezione del numero di serie della scheda. Assicurati di utilizzare il numero di serie univoco di ogni scheda

Comando lampeggiante nrfjprog.

Se l'operazione ha esito positivo, su ogni scheda si accenderanno i LED1, LED2 o LED3. Potresti anche vedere il commutatore del LED acceso da 3 a 2 (o da 2 a 1) subito dopo il lampeggiamento (la funzione di modifica del ruolo del dispositivo).

12. Funzionalità dell'applicazione

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

Indicatori dei ruoli del dispositivo

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

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

Quando il ruolo cambia, cambia anche il LED acceso. Dovresti aver già visto questi cambiamenti su una o due schede entro 20 secondi dall'accensione di ogni dispositivo.

Multicast UDP

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

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demo: osservare le modifiche ai ruoli del dispositivo

I dispositivi su cui hai eseguito il flashing sono un tipo specifico di dispositivo FTD (Full Thread Device), dispositivo finale idoneo per router (REED). Ciò significa che possono fungere da router o dispositivo finale e possono promuoversi da dispositivo finale a router.

Thread può supportare fino a 32 router, ma cerca di mantenere il numero di router compreso tra 16 e 23. Se un REED si collega come Dispositivo finale e il numero di Router è inferiore a 16, si promuoverà automaticamente a Router. Questa modifica dovrebbe avvenire in modo casuale entro il numero di secondi in cui hai impostato il valore otThreadSetRouterSelectionJitter nell'applicazione (20 secondi).

Ogni rete Thread dispone anche di un Leader, cioè un router responsabile della gestione del gruppo 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 devono essere router (LED2 acceso).

4e1e885861a66570.png

Rimuovi leader

Se il Leader viene rimosso dalla rete Thread, un router diverso si promuoverà a Leader, per garantire che la rete abbia ancora un Leader.

Spegni la classifica (quella con il LED1 acceso) usando l'interruttore di accensione. Attendi circa 20 secondi. Su una delle due schede rimanenti, il LED2 (Router) si spegnerà e il LED1 (Leader) si accenderà. Questo dispositivo è ora leader della rete Thread.

4c57c87adb40e0e3.png

Riattiva la classifica leader originale. Il dispositivo dovrebbe rientrare automaticamente nella rete Thread come dispositivo finale (il LED3 è acceso). Entro 20 secondi (jitter di selezione del router) si promuove e diventa un router (il LED2 è acceso).

5f40afca2dcc4b5b.png

Reimposta le bacheche

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

Le altre due schede inizialmente si connettono alla rete come Dispositivi finali (il LED3 è acceso), ma dovrebbero promuoversi a Router (il LED2 è acceso) entro 20 secondi.

Partizioni di rete

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

Il thread si ripara da solo, quindi le partizioni dovrebbero unirsi nuovamente in una singola partizione con un leader.

14. Demo: invio multicast UDP

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

Scegli una griglia e premi Pulsante1. Il LED4 su tutte le altre schede nella rete Thread che esegue l'applicazione dovrebbe attivare/disattivare il proprio stato. Se continui dall'esercizio precedente, ora dovrebbero essere attive.

f186a2618fdbe3fd.png

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

Premi il tasto 1 su un'altra scheda e osserva come si attiva il LED4 sulle altre schede. Premi il tasto 1 su una delle schede in cui il LED4 è attualmente acceso. Il LED4 rimane acceso per quella scheda, ma si attiva sulle altre.

f5865ccb8ab7aa34.png

Partizioni di rete

Se i tuoi tabelloni sono partizionati e c'è più di un leader tra di loro, il risultato del messaggio multicast sarà diverso da un tavolo all'altro. Se si preme Pulsante1 su una scheda partizionata (e quindi è l'unico membro della rete Thread partizionata), il LED4 sulle altre schede non si accende. In questo caso, reimposta le bacheche: idealmente, riformeranno una singola rete Thread e la messaggistica UDP dovrebbe funzionare correttamente.

15. Complimenti!

Hai creato un'applicazione che utilizza le API OpenThread.

Ora sai:

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

Passaggi successivi

Partendo da questo codelab, prova questi esercizi:

  • Modifica il modulo GPIO in modo che utilizzi i pin GPIO anziché i LED integrati e collega LED RGB esterni che cambiano colore in base al ruolo del router
  • Aggiungi il supporto GPIO per una diversa piattaforma di esempio
  • Anziché utilizzare il multicast per inviare un ping a tutti i dispositivi premendo il pulsante, utilizza 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 in multicast dall'esterno della rete Thread per accendere i LED

Per approfondire

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

Riferimento: