Cómo desarrollar con las APIs de OpenThread

1. Introducción

26b7f4f6b3ea0700.png

OpenThread, lanzado por Nest, es una implementación de código abierto del protocolo de red Thread®. Nest lanzó OpenThread para que los desarrolladores puedan acceder a la tecnología que se usa en los productos Nest y, así, acelerar el desarrollo de productos para la casa conectada.

La especificación de Thread define un protocolo de comunicación inalámbrico confiable, seguro y de bajo consumo basado en IPv6 para aplicaciones domésticas. OpenThread implementa todas las capas de red de Thread, incluidas IPv6, 6LoWPAN, IEEE 802.15.4 con seguridad MAC, establecimiento de vínculos de malla y enrutamiento de malla.

En este codelab, usarás las APIs de OpenThread para iniciar una red de Thread, supervisar y reaccionar a los cambios en los roles de los dispositivos, y enviar mensajes UDP, además de vincular estas acciones a botones y luces LED en hardware real.

2a6db2e258c32237.png

Qué aprenderás

  • Cómo programar los botones y los LEDs en las placas de desarrollo Nordic nRF52840
  • Cómo usar las APIs comunes de OpenThread y la clase otInstance
  • Cómo supervisar los cambios de estado de OpenThread y reaccionar ante ellos
  • Cómo enviar mensajes UDP a todos los dispositivos de una red Thread
  • Cómo modificar archivos makefile

Requisitos

Hardware:

  • 3 placas de desarrollo Nordic Semiconductor nRF52840
  • 3 cables USB a micro USB para conectar las placas
  • Una máquina Linux con al menos 3 puertos USB

Software:

  • Cadena de herramientas de GNU
  • Herramientas de línea de comandos de Nordic nRF5x
  • Software Segger J-Link
  • OpenThread
  • Git

A menos que se indique lo contrario, el contenido de este Codelab se rige por la licencia Creative Commons Atribución 3.0, y las muestras de código se rigen por la licencia Apache 2.0.

2. Cómo comenzar

Completa el Codelab de hardware

Antes de comenzar este codelab, debes completar el codelab Crea una red de Thread con placas nRF52840 y OpenThread, que incluye lo siguiente:

  • Detalla todo el software que necesitas para compilar y escribir en la memoria flash
  • Te enseña a compilar OpenThread y grabarlo en placas Nordic nRF52840
  • Demuestra los conceptos básicos de una red Thread

En este codelab, no se detalla la configuración del entorno necesaria para compilar OpenThread y grabar las placas, sino que solo se proporcionan instrucciones básicas para grabar las placas. Se supone que ya completaste el codelab Build a Thread Network.

Máquina Linux

Este codelab se diseñó para usar una máquina Linux basada en i386 o x86 para escribir en la memoria flash todas las placas de desarrollo de Thread. Todos los pasos se probaron en Ubuntu 14.04.5 LTS (Trusty Tahr).

Placas Nordic Semiconductor nRF52840

En este codelab, se usan tres placas PDK nRF52840.

a6693da3ce213856.png

Instale el software

Para compilar y escribir OpenThread en la memoria flash, debes instalar SEGGER J-Link, las herramientas de línea de comandos de nRF5x, la cadena de herramientas de ARM GNU y varios paquetes de Linux. Si completaste el codelab Build a Thread Network según lo requerido, ya tendrás todo lo que necesitas instalado. De lo contrario, completa ese codelab antes de continuar para asegurarte de que puedes compilar y grabar OpenThread en las placas de desarrollo nRF52840.

3. Clona el repositorio

OpenThread incluye código de aplicación de ejemplo que puedes usar como punto de partida para este codelab.

Clona el repo de ejemplos de OpenThread Nordic nRF528xx y compila OpenThread:

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

4. Conceptos básicos de la API de OpenThread

Las APIs públicas de OpenThread se encuentran en ./openthread/include/openthread en el repositorio de OpenThread. Estas APIs proporcionan acceso a una variedad de funciones y características de OpenThread a nivel de Thread y de plataforma para su uso en tus aplicaciones:

  • Información y control de la instancia de OpenThread
  • Servicios de aplicación, como IPv6, UDP y CoAP
  • Administración de credenciales de red, junto con los roles de comisionado y de participante
  • Administración del router de borde
  • Funciones mejoradas, como Supervisión infantil y Detección de atascos

La información de referencia sobre todas las APIs de OpenThread está disponible en openthread.io/reference.

Uso de una API

Para usar una API, incluye su archivo de encabezado en uno de los archivos de tu aplicación. Luego, llama a la función deseada.

Por ejemplo, la app de ejemplo de la CLI incluida en OpenThread usa los siguientes encabezados de 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>

La instancia de OpenThread

La estructura otInstance es algo que usarás con frecuencia cuando trabajes con las APIs de OpenThread. Una vez inicializada, esta estructura representa una instancia estática de la biblioteca de OpenThread y permite al usuario realizar llamadas a la API de OpenThread.

Por ejemplo, la instancia de OpenThread se inicializa en la función main() de la app de ejemplo de la CLI:

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

int main(int argc, char *argv[])
{
    otInstance *instance

...

#if OPENTHREAD_ENABLE_MULTIPLE_INSTANCES
    // Call to query the buffer size
    (void)otInstanceInit(NULL, &otInstanceBufferLength);

    // Call to allocate the buffer
    otInstanceBuffer = (uint8_t *)malloc(otInstanceBufferLength);
    assert(otInstanceBuffer);

    // Initialize OpenThread with the buffer
    instance = otInstanceInit(otInstanceBuffer, &otInstanceBufferLength);
#else
    instance = otInstanceInitSingle();
#endif

...

    return 0;
}

Funciones específicas de la plataforma

Si deseas agregar funciones específicas de la plataforma a una de las aplicaciones de ejemplo incluidas en OpenThread, primero decláralas en el encabezado ./openthread/examples/platforms/openthread-system.h, usando el espacio de nombres otSys para todas las funciones. Luego, impleméntalos en un archivo fuente específico de la plataforma. De esta manera, puedes usar los mismos encabezados de función para otras plataformas de ejemplo.

Por ejemplo, las funciones de GPIO que usaremos para conectar los botones y los LEDs del nRF52840 se deben declarar en openthread-system.h.

Abre el archivo ./openthread/examples/platforms/openthread-system.h en tu editor de texto preferido.

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

ACCIÓN: Agrega declaraciones de funciones de GPIO específicas de la plataforma.

Agrega estas declaraciones de funciones después del encabezado #include para 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);

Implementaremos estos métodos en el siguiente paso.

Ten en cuenta que la declaración de la función otSysButtonProcess usa un otInstance. De esa manera, la aplicación puede acceder a la información sobre la instancia de OpenThread cuando se presiona un botón, si es necesario. Todo depende de las necesidades de tu aplicación. Si no lo necesitas en la implementación de la función, puedes usar la macro OT_UNUSED_VARIABLE de la API de OpenThread para suprimir los errores de compilación relacionados con variables no utilizadas para algunas cadenas de herramientas. Veremos ejemplos de esto más adelante.

5. Implementa la abstracción de la plataforma GPIO

En el paso anterior, revisamos las declaraciones de funciones específicas de la plataforma en ./openthread/examples/platforms/openthread-system.h que se pueden usar para GPIO. Para acceder a los botones y los LEDs de las placas de desarrollo nRF52840, debes implementar esas funciones para la plataforma nRF52840. En este código, agregarás funciones que harán lo siguiente:

  • Inicializa los pines y modos de GPIO
  • Controla el voltaje en un pin
  • Habilita las interrupciones de GPIO y registra una devolución de llamada

En el directorio ./src/src, crea un archivo nuevo llamado gpio.c. En este archivo nuevo, agrega el siguiente contenido.

./src/src/gpio.c (archivo nuevo)

ACCIÓN: Agrega definiciones.

Estas definiciones sirven como abstracciones entre los valores específicos de nRF52840 y las variables que se usan a nivel de la aplicación 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

Para obtener más información sobre los botones y los LEDs del nRF52840, consulta el Infocenter de Nordic Semiconductor.

ACCIÓN: Agrega encabezados de inclusión.

A continuación, agrega los encabezados que necesitarás para la funcionalidad de 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"

ACCIÓN: Agrega funciones de devolución de llamada y de interrupción para el botón 1.

A continuación, agrega este código. La función in_pin1_handler es la devolución de llamada que se registra cuando se inicializa la funcionalidad de presión del botón (más adelante en este archivo).

Observa cómo esta devolución de llamada usa la macro OT_UNUSED_VARIABLE, ya que las variables que se pasan a in_pin1_handler no se usan en la función.

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

ACCIÓN: Agrega una función para configurar los LEDs.

Agrega este código para configurar el modo y el estado de todas las luces LED durante la inicialización.

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

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

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

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

ACTION: Agrega una función para configurar el modo de un LED.

Esta función se usará cuando cambie el rol del dispositivo.

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

ACCIÓN: Agrega una función para alternar el modo de un LED.

Esta función se usará para activar o desactivar el LED4 cuando el dispositivo reciba un mensaje UDP de transmisión 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;
    }
}

ACCIÓN: Agrega funciones para inicializar y procesar las pulsaciones de botones.

La primera función inicializa la placa para la presión de un botón, y la segunda envía el mensaje UDP de transmisión múltiple cuando se presiona el botón 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);
    }
}

ACCIÓN: Guarda y cierra el archivogpio.c.

6. API: Reacciona a los cambios de rol del dispositivo

En nuestra aplicación, queremos que se enciendan diferentes luces LED según el rol del dispositivo. Hagamos un seguimiento de los siguientes roles: líder, router y dispositivo final. Podemos asignarlos a las luces LED de la siguiente manera:

  • LED1 = Líder
  • LED2 = Router
  • LED3 = Dispositivo final

Para habilitar esta funcionalidad, la aplicación debe saber cuándo cambió el rol del dispositivo y cómo encender la luz LED correcta en respuesta. Usaremos la instancia de OpenThread para la primera parte y la abstracción de la plataforma GPIO para la segunda.

Abre el archivo ./openthread/examples/apps/cli/main.c en tu editor de texto preferido.

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

ACCIÓN: Agrega encabezados de inclusión.

En la sección de inclusiones del archivo main.c, agrega los archivos de encabezado de la API que necesitarás para la función de cambio de rol.

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

ACTION: Add handler function declaration for the OpenThread instance state change.

Agrega esta declaración a main.c, después de las inclusiones de encabezado y antes de cualquier instrucción #if. Esta función se definirá después de la aplicación principal.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

ACTION: Agrega un registro de devolución de llamada para la función de controlador de cambio de estado.

En main.c, agrega esta función a la función main() después de la llamada a otAppCliInit. Este registro de devolución de llamada le indica a OpenThread que llame a la función handleNetifStateChange cada vez que cambie el estado de la instancia de OpenThread.

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

ACCIÓN: Agrega la implementación del cambio de estado.

En main.c, después de la función main(), implementa la función handleNetifStateChanged. Esta función verifica la marca OT_CHANGED_THREAD_ROLE de la instancia de OpenThread y, si cambió, enciende o apaga las luces LED según sea necesario.

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: Usa la transmisión multidifusión para encender un LED

En nuestra aplicación, también queremos enviar mensajes UDP a todos los demás dispositivos de la red cuando se presiona el botón 1 en una placa. Para confirmar la recepción del mensaje, activaremos el LED4 en las otras placas como respuesta.

Para habilitar esta funcionalidad, la aplicación debe hacer lo siguiente:

  • Inicializa una conexión UDP al inicio
  • Poder enviar un mensaje UDP a la dirección de multidifusión local de la malla
  • Cómo controlar los mensajes UDP entrantes
  • Alterna LED4 en respuesta a los mensajes UDP entrantes

Abre el archivo ./openthread/examples/apps/cli/main.c en tu editor de texto preferido.

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

ACCIÓN: Agrega encabezados de inclusión.

En la sección de inclusión que se encuentra en la parte superior del archivo main.c, agrega los archivos de encabezado de la API que necesitarás para la función de UDP de transmisión simultánea.

#include <string.h>

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

#include "utils/code_utils.h"

El encabezado code_utils.h se usa para las macros otEXPECT y otEXPECT_ACTION que validan las condiciones de tiempo de ejecución y controlan los errores de forma correcta.

ACCIÓN: Agrega definiciones y constantes:

En el archivo main.c, después de la sección de inclusión y antes de cualquier instrucción #if, agrega constantes y definiciones específicas de UDP:

#define UDP_PORT 1212

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

ff03::1 es la dirección de multidifusión local de la malla. Los mensajes que se envíen a esta dirección se enviarán a todos los dispositivos Full Thread de la red. Consulta Multicast en openthread.io para obtener más información sobre la compatibilidad con la transmisión multidifusión en OpenThread.

ACTION: Add function declarations.

En el archivo main.c, después de la definición de otTaskletsSignalPending y antes de la función main(), agrega funciones específicas de UDP, así como una variable estática para representar 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;

ACCIÓN: Agrega llamadas para inicializar los LEDs y el botón de GPIO.

En main.c, agrega estas llamadas a función a la función main() después de la llamada a otSetStateChangedCallback. Estas funciones inicializan los pines GPIO y GPIOTE, y establecen un controlador de botones para controlar los eventos de presión de los botones.

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

ACCIÓN: Agrega la llamada de inicialización de UDP.

En main.c, agrega esta función a la función main() después de la llamada a otSysButtonInit que acabas de agregar:

initUdp(instance);

Esta llamada garantiza que se inicialice un socket UDP al iniciar la aplicación. Sin esto, el dispositivo no puede enviar ni recibir mensajes UDP.

ACCIÓN: Agrega una llamada para procesar el evento del botón GPIO.

En main.c, agrega esta llamada a la función main() después de la llamada a otSysProcessDrivers, en el bucle while. Esta función, declarada en gpio.c, verifica si se presionó el botón y, si es así, llama al controlador (handleButtonInterrupt) que se configuró en el paso anterior.

otSysButtonProcess(instance);

ACCIÓN: Implementa el controlador de interrupciones del botón.

En main.c, agrega la implementación de la función handleButtonInterrupt después de la función handleNetifStateChanged que agregaste en el paso anterior:

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

ACCIÓN: Implementa la inicialización de UDP.

En main.c, agrega la implementación de la función initUdp después de la función handleButtonInterrupt que acabas de agregar:

/**
 * 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 es el puerto que definiste antes (1212). La función otUdpOpen abre el socket y registra una función de devolución de llamada (handleUdpReceive) para cuando se recibe un mensaje UDP. otUdpBind vincula el socket a la interfaz de red Thread pasando OT_NETIF_THREAD. Para ver otras opciones de interfaz de red, consulta la enumeración otNetifIdentifier en la referencia de la API de UDP.

ACCIÓN: Implementa la mensajería UDP.

En main.c, agrega la implementación de la función sendUdp después de la función initUdp que acabas de agregar:

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

Ten en cuenta las macros otEXPECT y otEXPECT_ACTION. Estas instrucciones garantizan que el mensaje UDP sea válido y se asigne correctamente en el búfer. De lo contrario, la función controla los errores correctamente saltando al bloque exit, donde libera el búfer.

Consulta las referencias de IPv6 y UDP en openthread.io para obtener más información sobre las funciones que se usan para inicializar UDP.

ACCIÓN: Implementa el control de mensajes UDP.

En main.c, agrega la implementación de la función handleUdpReceive después de la función sendUdp que acabas de agregar. Esta función simplemente activa o desactiva el 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 red de Thread

Para facilitar la demostración, queremos que nuestros dispositivos inicien Thread de inmediato y se unan a una red cuando se enciendan. Para ello, usaremos la estructura otOperationalDataset. Esta estructura contiene todos los parámetros necesarios para transmitir las credenciales de la red de Thread a un dispositivo.

El uso de esta estructura anulará los valores predeterminados de la red integrados en OpenThread para que nuestra aplicación sea más segura y limitar los nodos de Thread en nuestra red solo a aquellos que ejecutan la aplicación.

Nuevamente, abre el archivo ./openthread/examples/apps/cli/main.c en tu editor de texto preferido.

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

ACCIÓN: Agrega la inclusión del encabezado.

Dentro de la sección de inclusión en la parte superior del archivo main.c, agrega el archivo de encabezado de la API que necesitarás para configurar la red de Thread:

#include <openthread/dataset_ftd.h>

ACTION: Agrega la declaración de la función para establecer la configuración de red.

Agrega esta declaración a main.c, después de las inclusiones de encabezado y antes de cualquier instrucción #if. Esta función se definirá después de la función principal de la aplicación.

static void setNetworkConfiguration(otInstance *aInstance);

ACCIÓN: Agrega la llamada de configuración de red.

En main.c, agrega esta llamada a función a la función main() después de la llamada a otSetStateChangedCallback. Esta función configura el conjunto de datos de la red de Thread.

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

ACCIÓN: Agrega llamadas para habilitar la interfaz y la pila de la red de Thread.

En main.c, agrega estas llamadas a función a la función main() después de la llamada a otSysButtonInit.

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

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

ACCIÓN: Implementa la configuración de la red de Thread.

En main.c, agrega la implementación de la función setNetworkConfiguration después de la función 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);
}

Como se detalla en la función, los parámetros de red de Thread que usamos para esta aplicación son los siguientes:

  • Canal = 15
  • ID de PAN = 0x2222
  • ID de PAN extendido = C0DE1AB5C0DE1AB5
  • Clave de red = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Nombre de la red = OTCodelab

Además, aquí es donde disminuimos la fluctuación de la selección del router, de modo que nuestros dispositivos cambien de rol más rápido para fines de demostración. Ten en cuenta que esto solo se hace si el nodo es un FTD (dispositivo Thread completo). Hablaremos más sobre eso en el siguiente paso.

9. API: Funciones restringidas

Algunas de las APIs de OpenThread modifican parámetros de configuración que solo se deben modificar con fines de demostración o prueba. Estas APIs no se deben usar en una implementación de producción de una aplicación que use OpenThread.

Por ejemplo, la función otThreadSetRouterSelectionJitter ajusta el tiempo (en segundos) que tarda un dispositivo final en promocionarse a sí mismo como router. El valor predeterminado para este valor es 120, según la especificación de Thread. Para facilitar el uso en este codelab, lo cambiaremos a 20, de modo que no tengas que esperar mucho para que un nodo de Thread cambie de rol.

Nota: Los dispositivos MTD no se convierten en routers, y la compatibilidad con una función como otThreadSetRouterSelectionJitter no se incluye en una compilación de MTD. Más adelante, debemos especificar la opción de CMake -DOT_MTD=OFF; de lo contrario, se producirá un error de compilación.

Puedes confirmar esto si observas la definición de la función otThreadSetRouterSelectionJitter, que se encuentra dentro de una directiva del preprocesador de 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. Actualizaciones de CMake

Antes de compilar tu aplicación, se necesitan algunas actualizaciones menores para tres archivos CMake. El sistema de compilación las usa para compilar y vincular tu aplicación.

./third_party/NordicSemiconductor/CMakeLists.txt

Ahora, agrega algunas marcas a NordicSemiconductor CMakeLists.txt para asegurarte de que las funciones de GPIO estén definidas en la aplicación.

ACCIÓN: Agrega marcas al archivoCMakeLists.txt .

Abre ./third_party/NordicSemiconductor/CMakeLists.txt en tu editor de texto preferido y agrega las siguientes líneas en la sección 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

Edita el archivo ./src/CMakeLists.txt para agregar el nuevo archivo fuente gpio.c:

ACCIÓN: Agrega la fuente gpio al archivo ./src/CMakeLists.txt.

Abre ./src/CMakeLists.txt en tu editor de texto preferido y agrega el archivo a la sección NRF_COMM_SOURCES.

...

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

...

./third_party/NordicSemiconductor/CMakeLists.txt

Por último, agrega el archivo del controlador nrfx_gpiote.c al archivo CMakeLists.txt de NordicSemiconductor para que se incluya en la compilación de la biblioteca de los controladores de Nordic.

ACCIÓN: Agrega el controlador gpio al archivo NordicSemiconductorCMakeLists.txt.

Abre ./third_party/NordicSemiconductor/CMakeLists.txt en tu editor de texto preferido y agrega el archivo a la sección COMMON_SOURCES.

...

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

11. Configura los dispositivos

Con todas las actualizaciones de código completadas, ya puedes compilar y grabar la aplicación en las tres placas de desarrollo Nordic nRF52840. Cada dispositivo funcionará como un dispositivo Thread completo (FTD).

Cómo compilar OpenThread

Compila los objetos binarios de FTD de OpenThread para la plataforma nRF52840.

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

Navega al directorio con el binario de la CLI de FTD de OpenThread y conviértelo al formato hexadecimal con la cadena de herramientas integradas de ARM:

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

Cómo actualizar las placas

Graba el archivo ot-cli-ftd.hex en cada placa nRF52840.

Conecta el cable USB al puerto de depuración micro-USB que se encuentra junto al pin de alimentación externa en la placa nRF52840 y, luego, conéctalo a tu máquina Linux. Si lo configuras correctamente, la luz LED5 se encenderá.

20a3b4b480356447.png

Como antes, anota el número de serie de la placa nRF52840:

c00d519ebec7e5f0.jpeg

Navega a la ubicación de las herramientas de línea de comandos de nRFx y graba el archivo .hex de FTD de la CLI de OpenThread en la placa nRF52840 con el número de serie de la placa:

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

La luz LED5 se apagará brevemente durante el parpadeo. Si la operación se realiza correctamente, se genera el siguiente resultado:

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.

Repite este paso de "Grabar las placas" para las otras dos placas. Cada placa debe conectarse a la máquina Linux de la misma manera, y el comando para escribir en la memoria flash es el mismo, excepto por el número de serie de la placa. Asegúrate de usar el número de serie único de cada placa en el

Comando de escritura de nrfjprog

Si la operación se realiza correctamente, se encenderá el LED1, el LED2 o el LED3 en cada placa. Es posible que incluso veas que la luz LED encendida cambia de 3 a 2 (o de 2 a 1) poco después de parpadear (la función de cambio de rol del dispositivo).

12. Funcionalidad de la aplicación

Las tres placas nRF52840 ahora deberían estar encendidas y ejecutando nuestra aplicación OpenThread. Como se detalló anteriormente, esta aplicación tiene dos funciones principales.

Indicadores de rol del dispositivo

La luz LED encendida en cada placa refleja el rol actual del nodo de Thread:

  • LED1 = Líder
  • LED2 = Router
  • LED3 = Dispositivo final

A medida que cambia el rol, también lo hace la luz LED encendida. Ya deberías haber visto estos cambios en uno o dos tableros en un plazo de 20 segundos después de que se encendiera cada dispositivo.

Multidifusión UDP

Cuando se presiona el botón 1 en una placa, se envía un mensaje UDP a la dirección de multidifusión local de la malla, que incluye todos los demás nodos de la red de Thread. En respuesta a la recepción de este mensaje, el LED4 de todas las demás placas se activa o desactiva. El LED4 permanece encendido o apagado en cada placa hasta que recibe otro mensaje UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demostración: Observa los cambios en el rol del dispositivo

Los dispositivos que grabaste son un tipo específico de dispositivo Thread completo (FTD) llamado dispositivo final apto para router (REED). Esto significa que pueden funcionar como router o dispositivo final, y pueden promocionarse de dispositivo final a router.

Thread puede admitir hasta 32 routers, pero intenta mantener la cantidad de routers entre 16 y 23. Si un REED se conecta como dispositivo final y la cantidad de routers es inferior a 16, se promueve automáticamente a router. Este cambio debería ocurrir en un momento aleatorio dentro de la cantidad de segundos que estableciste para el valor otThreadSetRouterSelectionJitter en la aplicación (20 segundos).

Cada red de Thread también tiene un líder, que es un router responsable de administrar el conjunto de routers en una red de Thread. Con todos los dispositivos encendidos, después de 20 segundos, uno de ellos debería ser un líder (LED1 encendido) y los otros dos deberían ser routers (LED2 encendido).

4e1e885861a66570.png

Cómo quitar al líder

Si se quita el líder de la red Thread, otro router se promoverá como líder para garantizar que la red siga teniendo uno.

Apaga la placa de Leader (la que tiene la luz LED1 encendida) con el interruptor de encendido. Espera unos 20 segundos. En una de las dos placas restantes, se apagará el LED2 (router) y se encenderá el LED1 (líder). Este dispositivo ahora es el líder de la red Thread.

4c57c87adb40e0e3.png

Vuelve a activar el ranking original. Debería volver a unirse automáticamente a la red de Thread como dispositivo final (la luz LED3 está encendida). En un plazo de 20 segundos (la fluctuación de la selección del router), se promueve a sí mismo como router (la luz LED2 está encendida).

5f40afca2dcc4b5b.png

Cómo restablecer los tableros

Apaga las tres placas y, luego, vuelve a encenderlas para observar las luces LED. La primera placa que se encendió debería comenzar con el rol de líder (la luz LED1 está encendida): el primer router de una red Thread se convierte automáticamente en el líder.

Las otras dos placas se conectan inicialmente a la red como dispositivos finales (la luz LED3 está encendida), pero deberían convertirse en routers (la luz LED2 está encendida) en un plazo de 20 segundos.

Particiones de red

Si las placas no reciben suficiente energía o la conexión de radio entre ellas es débil, es posible que la red Thread se divida en particiones y que más de un dispositivo aparezca como líder.

El subproceso es autorreparable, por lo que las particiones deberían volver a fusionarse en una sola partición con un solo líder.

14. Demostración: Envía multidifusión UDP

Si continúas desde el ejercicio anterior, la luz LED4 no debería estar encendida en ningún dispositivo.

Elige cualquier tablero y presiona el botón 1. El LED4 de todas las demás placas de la red de Thread que ejecutan la aplicación debería alternar su estado. Si continuaste desde el ejercicio anterior, deberían estar encendidas.

f186a2618fdbe3fd.png

Vuelve a presionar el botón 1 en la misma placa. El LED4 de todas las demás placas debería volver a cambiar.

Presiona el botón 1 en otra placa y observa cómo el LED4 se activa en las otras placas. Presiona el botón 1 en una de las placas en las que la luz LED4 está encendida. La luz LED4 permanece encendida en esa placa, pero se activa y desactiva en las demás.

f5865ccb8ab7aa34.png

Particiones de red

Si tus placas están particionadas y hay más de un líder entre ellas, el resultado del mensaje de transmisión múltiple será diferente entre las placas. Si presionas el botón 1 en una placa que se particionó (y, por lo tanto, es el único miembro de la red de subprocesos particionada), el LED4 de las otras placas no se encenderá en respuesta. Si esto sucede, restablece las placas. Lo ideal es que se forme una sola red de Thread y que la mensajería UDP funcione correctamente.

15. ¡Felicitaciones!

Creaste una aplicación que usa las APIs de OpenThread.

Ahora ya sabes lo siguiente:

  • Cómo programar los botones y los LEDs en las placas de desarrollo Nordic nRF52840
  • Cómo usar las APIs comunes de OpenThread y la clase otInstance
  • Cómo supervisar los cambios de estado de OpenThread y reaccionar ante ellos
  • Cómo enviar mensajes UDP a todos los dispositivos de una red Thread
  • Cómo modificar archivos makefile

Próximos pasos

A partir de este Codelab, prueba los siguientes ejercicios:

  • Modifica el módulo GPIO para que use pines GPIO en lugar de los LEDs integrados y conecta LEDs RGB externos que cambien de color según el rol del router.
  • Agrega compatibilidad con GPIO para otra plataforma de ejemplo
  • En lugar de usar la transmisión multidifusión para hacer ping a todos los dispositivos cuando se presiona un botón, usa la API de Router/Leader para ubicar y hacer ping a un dispositivo individual.
  • Conecta tu red de malla a Internet con un router de borde OpenThread y transmítela mediante multidifusión desde fuera de la red Thread para encender las luces LED.

Lecturas adicionales

Consulta openthread.io y GitHub para obtener una variedad de recursos de OpenThread, incluidos los siguientes:

Referencia: