1. Introducción
OpenThread, lanzado por Nest, es una implementación de código abierto del protocolo de red Thread®. Nest lanzó OpenThread a fin de poner a disposición de los desarrolladores la tecnología que se usa en los productos Nest para acelerar el desarrollo de productos en el hogar conectado.
La especificación de Thread define un protocolo de comunicación entre dispositivos inalámbrico, confiable y seguro basado en IPv6 para las aplicaciones de la casa. OpenThread implementa todas las capas de red Thread, incluidas IPv6, 6LoWPAN, IEEE 802.15.4, con seguridad MAC, el establecimiento de vínculos de malla y el enrutamiento de mallas.
En este Codelab, usarás las API de OpenThread para iniciar una red Thread, supervisar y reaccionar a los cambios en los roles de los dispositivos y enviar mensajes UDP. También vincularás estas acciones a botones y luces LED en hardware real.
Qué aprenderás
- Cómo programar los botones y los LED en placas de desarrollo nórdicas nRF52840
- Cómo usar las API de OpenThread comunes y la clase
otInstance
- Cómo supervisar y reaccionar a los cambios de estado de OpenThread
- Cómo enviar mensajes UDP a todos los dispositivos en una red Thread
- Cómo modificar Makefiles
Requisitos
Hardware:
- 3 placas de desarrollo nórdicas nRF52840 nórdicas
- 3 cables USB a micro-USB para conectar las placas
- Una máquina Linux con al menos 3 puertos USB
Software:
- Conjunto de herramientas de GNU
- Herramientas de línea de comandos nórdicas de nRF5x
- Software Segger J-Link
- OpenThread
- Git
Excepto que se indique lo contrario, el contenido de este Codelab cuenta con la licencia de atribución Creative Commons 3.0 y las muestras de código cuentan con la licencia Apache 2.0.
2. Comenzar
Cómo completar el codelab de hardware
Antes de comenzar este Codelab, debes completar el codelab Build a Thread Network with nRF52840 Boards and OpenThread, que tiene las siguientes características:
- Detalles de todo el software que necesitas para compilar y escribir en la memoria flash
- Te enseña a compilar OpenThread y a escribirlo en placas nórdicas nRF52840
- Muestra los conceptos básicos de una red Thread.
En este Codelab, no se detalla ninguno de los entornos requeridos para compilar OpenThread e instalar las placas, solo instrucciones básicas para escribirlas. Se supone que ya completaste el codelab de Build a Thread Network.
Máquina Linux
Este Codelab fue diseñado para usar una máquina Linux basada en i386 o x86 a fin de 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 de semiconductor nórdico nRF52840
Este codelab usa tres placas PDK nRF52840.
Instale el software
Para compilar e instalar OpenThread, debes instalar SEGGER J-Link, las herramientas de línea de comandos nRF5x, la cadena de herramientas ARM GNU y varios paquetes de Linux. Si completaste el Codelab para compilar una red Thread según sea necesario, ya tendrás todo lo que necesitas instalado. De lo contrario, completa ese Codelab antes de continuar para asegurarte de compilar e instalar OpenThread en placas de desarrollo nRF52840.
3. Clone el repositorio
OpenThread incluye código de aplicación de ejemplo que puedes usar como punto de partida para este Codelab.
Clona el repositorio de ejemplos nRF5/nr528xx de OpenThread 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 API públicas de OpenThread se encuentran en ./openthread/include/openthread
en el repositorio de OpenThread. Estas API proporcionan acceso a una variedad de características y funcionalidades de OpenThread a nivel de la conversación y de la plataforma para usarlas 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 funciones de comisionado y comisionado
- Administración de router fronterizo
- Funciones mejoradas, como la supervisión infantil y la detección de Jams
La información de referencia sobre todas las API de OpenThread está disponible en openthread.io/reference.
Usar una API
Para usar una API, incluye el 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 con OpenThread utiliza 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 API de OpenThread. Una vez inicializada, esta estructura representa una instancia estática de la biblioteca de OpenThread y permite que el usuario realice 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 quieres agregar funciones específicas de la plataforma a una de las aplicaciones de ejemplo incluidas con OpenThread, primero debes declararlas en el encabezado ./openthread/examples/platforms/openthread-system.h
, con el espacio de nombres otSys
para todas las funciones. Luego, impleméntalos en un archivo de origen específico de la plataforma. Esta es una abstracción, de modo que puedes usar los mismos encabezados de función para otras plataformas de ejemplo.
Por ejemplo, las funciones GPIO que usaremos para conectar a los botones nRF52840 y las luces LED se deben declarar en openthread-system.h
.
Abre el archivo ./openthread/examples/platforms/openthread-system.h
en el editor de texto que prefieras.
./openthread/examples/platforms/openthread-system.h
ACCIÓN: Agrega declaraciones de funciones GPIO específicas de la plataforma.
Agrega estas declaraciones de función después del #include
para el encabezado 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 esto en el paso siguiente.
Ten en cuenta que la declaración de la función otSysButtonProcess
usa un otInstance
. De esta 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 su aplicación. Si no la necesitas en tu implementación de la función, puedes usar la macro OT_UNUSED_VARIABLE
de la API de OpenThread para suprimir errores de compilación alrededor de variables sin usar de 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. A fin de acceder a los botones y las luces LED de las placas de desarrollo nRF52840, debe implementar esas funciones para la plataforma nRF52840. En este código, agregarás funciones que hacen lo siguiente:
- Inicializa los modos y los pines 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 el archivo nuevo, agrega el siguiente contenido.
./src/src/gpio.c (archivo nuevo)
ACTION: Add define.
Estas definen como abstracciones entre valores y variables específicos de nRF52840 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 las luces LED nRF52840, consulta el Centro de información de los semiconductores nórdicos.
ACTION: Add include include.
A continuación, agrega el encabezado que necesitarás para la funcionalidad GPIO.
/* Header for the functions defined here */ #include "openthread-system.h" #include <string.h> /* Header to access an OpenThread instance */ #include <openthread/instance.h> /* Headers for lower-level nRF52840 functions */ #include "platform-nrf5.h" #include "hal/nrf_gpio.h" #include "hal/nrf_gpiote.h" #include "nrfx/drivers/include/nrfx_gpiote.h"
ACTION: Agrega funciones de interrupción y devolución de llamada 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 presionar el botón (más adelante en este archivo).
Observa que esta devolución de llamada usa la macro OT_UNUSED_VARIABLE
, ya que las variables pasadas 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 las luces LED.
Agrega este código para configurar el modo y el estado de todos los 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 una luz LED.
Esta función se usará cuando cambie la función 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; } }
ACTION: Agrega una función para activar o desactivar el modo de una luz LED.
Esta función se utilizará para activar o desactivar LED4 cuando el dispositivo reciba un mensaje UDP multidifusión.
/** * @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 los botones.
La primera función inicializa la placa para la presión de un botón y la segunda envía el mensaje UDP multidifusión 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: Guarde y cierre el archivo gpio.c
.
6. API: Cómo reaccionar a los cambios de funciones del dispositivo
En nuestra aplicación, queremos que se enciendan diferentes LED según la función del dispositivo. Realicemos un seguimiento de las siguientes funciones: líder, router, 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 activar el LED correcto en respuesta. Utilizaremos 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 el editor de texto que prefieras.
./openthread/examples/apps/cli/main.c
ACTION: Add include include.
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 función.
#include <openthread/instance.h> #include <openthread/thread.h> #include <openthread/thread_ftd.h>
ACTION: Agrega la declaración de función del controlador para el cambio de estado de la instancia de OpenThread.
Agrega esta declaración a main.c
, después del encabezado y antes de cualquier declaració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 del controlador de cambios de estado.
En main.c
, agrega esta función a la función main()
después de la llamada 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: Agregue 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: Cómo usar la multidifusión para activar un LED
En nuestra aplicación, también queremos enviar mensajes UDP a todos los demás dispositivos en la red cuando se presiona Button1 en una placa. Para confirmar la recepción del mensaje, activaremos las luces LED4 en las otras placas en respuesta a ellas.
Para habilitar esta funcionalidad, la aplicación debe hacer lo siguiente:
- Inicializa una conexión UDP al inicio
- Puedes enviar un mensaje UDP a la dirección de multidifusión local de la malla
- Maneja mensajes UDP entrantes
- Activa o desactiva LED4 en respuesta a mensajes UDP entrantes
Abre el archivo ./openthread/examples/apps/cli/main.c
en el editor de texto que prefieras.
./openthread/examples/apps/cli/main.c
ACTION: Add include include.
En la sección de inclusiónes, en la parte superior del archivo main.c
, agrega los archivos de encabezado de la API que necesitarás para la función UDP multidifusión.
#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: Agregue definiciones y constantes:
En el archivo main.c
, después de la sección de inclusiones y antes de cualquier declaración #if
, agrega constantes específicas de UDP y define lo siguiente:
#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 de la conversación completa de la red. Consulta Multicast en openthread.io para obtener más información sobre la compatibilidad con multidifusión en OpenThread.
ACCIÓN: Agrega declaraciones de funciones.
En el archivo main.c
, después de la definición 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 botones y las luces LED de GPIO.
En main.c
, agrega estas llamadas a la función main()
después de la llamada otSetStateChangedCallback
. Estas funciones inicializan los pines GPIO y GPIOTE, y configuran un controlador de botones para controlar los eventos de envío de botones.
/* init GPIO LEDs and button */ otSysLedInit(); otSysButtonInit(handleButtonInterrupt);
ACTION: 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 otSysButtonInit
que acabas de agregar:
initUdp(instance);
Esta llamada garantiza que se inicialice un socket UDP cuando se inicie la aplicación. Sin esto, el dispositivo no podrá enviar ni recibir mensajes UDP.
ACCIÓN: Agrega una llamada para procesar el evento del botón de GPIO.
En main.c
, agrega esta llamada a la función a la función main()
después de la llamada otSysProcessDrivers
, en el bucle while
. Esta función, declarada en gpio.c
, comprueba si se presionó el botón y, de ser así, llama al controlador (handleButtonInterrupt
) que se configuró en el paso anterior.
otSysButtonProcess(instance);
ACTION: Implementa el controlador de interrupciones de botones.
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 de Thread pasando OT_NETIF_THREAD
. Para conocer otras opciones de interfaz de red, consulta la enumeración otNetifIdentifier
en 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); } }
Observa las macros otEXPECT
y otEXPECT_ACTION
. Estos se aseguran de que el mensaje UDP sea válido y se asigne correctamente en el búfer y, de no ser así, la función maneja correctamente los errores 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 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: Cómo configurar la red Thread
Para facilitar la demostración, queremos que nuestros dispositivos inicien Thread de inmediato y se unan a una red cuando estén encendidos. Para ello, usaremos la estructura otOperationalDataset
. Esta estructura contiene todos los parámetros necesarios para transmitir las credenciales de red de Thread a un dispositivo.
El uso de esta estructura anulará los valores predeterminados de red integrados en OpenThread, para que nuestra aplicación sea más segura y limitar los nodos de Thread de nuestra red a solo los que ejecutan la app.
Vuelve a abrir el archivo ./openthread/examples/apps/cli/main.c
en el editor de texto que prefieras.
./openthread/examples/apps/cli/main.c
ACCIÓN: Agregue la inclusión del encabezado.
En la sección de inclusión que se encuentra en la parte superior del archivo main.c
, agrega el archivo de encabezado de la API que necesitarás para configurar la red Thread:
#include <openthread/dataset_ftd.h>
ACTION: Agrega una declaración de función para establecer la configuración de red.
Agrega esta declaración a main.c
, después del encabezado y antes de cualquier declaración #if
. Esta función se definirá después de la función principal de la aplicación.
static void setNetworkConfiguration(otInstance *aInstance);
ACTION: Agregue la llamada de configuración de red.
En main.c
, agrega esta llamada a la función a la función main()
después de la llamada otSetStateChangedCallback
. Esta función configura el conjunto de datos de red de Thread.
/* Override default network credentials */ setNetworkConfiguration(instance);
ACTION: Agrega llamadas para habilitar la interfaz y la pila de la red Thread.
En main.c
, agrega estas llamadas a la función main()
después de la llamada 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 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 utilizamos para esta aplicación son los siguientes:
- Canal = 15
- ID PAN = 0x2222
- ID del PAN extendido = C0DE1AB5C0DE1AB5
- Clave de red = 1234C0DE1AB51234C0DE1AB51234C0DE
- Nombre de la red = OTCodelab
Además, es aquí en la que se reduce el Jitter de selección de selección, por lo que nuestros dispositivos cambian las funciones más rápido para fines de demostración. Ten en cuenta que esto solo se hace si el nodo es un FTD (dispositivo de subproceso completo). Obtendrás más información al respecto en el siguiente paso.
9. API: Funciones restringidas
Algunas de las API de OpenThread modifican la configuración que solo se debe modificar con fines de demostración o prueba. Estas API no deben usarse en la implementación de producción de una aplicación mediante OpenThread.
Por ejemplo, la función otThreadSetRouterSelectionJitter
ajusta el tiempo (en segundos) que tarda un dispositivo final en promocionarse como router. El valor predeterminado para este valor es 120, según la especificación de subproceso. Para facilitar el uso en este Codelab, lo cambiaremos a 20, así que no tienes que esperar mucho para que un nodo de Thread cambie de función.
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 MTD. Más adelante, necesitaremos especificar la opción -DOT_MTD=OFF
de CMake; de lo contrario, se producirá un error de compilación.
Para confirmarlo, consulta la definición de la función otThreadSetRouterSelectionJitter
, que se encuentra dentro de una directiva de 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 al NordicSemiconductor CMakeLists.txt
para asegurarte de que las funciones GPIO estén definidas en la aplicación.
ACTION: Agrega marcas al archivo CMakeLists.txt
.
Abre ./third_party/NordicSemiconductor/CMakeLists.txt
en el editor de texto que desees 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 de gpio al archivo ./src/CMakeLists.txt
.
Abre ./src/CMakeLists.txt
en el editor de texto que prefieras 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 nrfx_gpiote.c
del controlador al archivo CMakeLists.txt
de NordicSemiconductor, de modo que se incluya en la compilación de la biblioteca de los controladores nórdicos.
ACTION: Agrega el controlador gpio al archivo NordicSemiconductor CMakeLists.txt
.
Abre ./third_party/NordicSemiconductor/CMakeLists.txt
en el editor de texto que prefieras 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 hechas, estás listo para compilar e instalar la aplicación en las tres placas de desarrollo nórdicas nRF52840. Cada dispositivo funcionará como un dispositivo de conversación completa (FTD).
Cómo compilar OpenThread
Compila los objetos binarios de OpenThread FTD 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 objeto binario de la CLI de OpenThread FTD y conviértelo a formato hexadecimal con el conjunto de herramientas incorporadas de ARM:
$ cd build/bin $ arm-none-eabi-objcopy -O ihex ot-cli-ftd ot-cli-ftd.hex
Cómo escribir en las placas
Escribe el archivo ot-cli-ftd.hex
en cada placa nRF52840.
Conecta el cable USB al puerto de depuración micro-USB junto al pin de alimentación externo en la placa nRF52840 y, luego, conéctalo a la máquina Linux. Configura LED5 correctamente.
Al igual que antes, ten en cuenta el número de serie de la placa nRF52840:
Navega a la ubicación de las herramientas de línea de comandos nRFx y escribe el archivo hexadecimal CLI FTD 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
El LED5 se apagará brevemente durante el parpadeo. La siguiente salida se genera con éxito:
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 para escribir en las otras dos placas. Cada placa debe estar conectada 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 la
El comando nrfjprog
Flashing
Si se realiza correctamente, se encenderán las luces LED1, LED2 o LED3 en cada placa. Es posible que veas el interruptor de luz LED de 3 a 2 (o 2 a 1) poco después de parpadear (función de cambio de función en el dispositivo).
12. Funcionalidad de la aplicación
Las tres placas nRF52840 ahora deberían funcionar y ejecutar nuestra aplicación de OpenThread. Como se detalló anteriormente, esta aplicación tiene dos funciones principales.
Indicadores de función de los dispositivos
La luz LED en cada placa refleja la función actual del nodo Thread:
- LED1 = líder
- LED2 = Router
- LED3 = Dispositivo final
A medida que cambia el rol, también se ilumina la luz LED. Deberías haber visto estos cambios en una o dos placas en 20 segundos de encendido de cada dispositivo.
Multidifusión UDP
Cuando se presiona Button1 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 este mensaje, LED4 en todas las demás placas se activa o desactiva. LED4 permanecerá encendido o apagado en cada placa hasta que reciba otro mensaje UDP.
13. Demostración: Cómo observar los cambios en las funciones del dispositivo
Los dispositivos que escrituraste son un tipo específico de dispositivo de subprocesos completos (FTD) llamado dispositivo final apto apto para routers (REED). Esto significa que pueden funcionar como un router o como dispositivo final, y pueden cambiar de un dispositivo final a uno.
Thread puede admitir hasta 32 routers, pero intenta mantener la cantidad de routers entre 16 y 23. Si se adjunta un REED como dispositivo final y la cantidad de routers es inferior a 16, se promueve automáticamente a un router. Este cambio debería ocurrir de forma aleatoria dentro de la cantidad de segundos en que estableces el valor otThreadSetRouterSelectionJitter
en la aplicación (20 segundos).
Cada red Thread tiene un líder, que es el responsable de administrar el conjunto de routers en una red Thread. Con todos los dispositivos encendidos, uno de ellos debe ser un líder (LED1 encendido) y los otros dos deben ser routers (LED2 encendido).
Quitar el líder
Si se quita al líder de la red Thread, un router diferente se promocionará como líder para garantizar que la red aún tenga un líder.
Desactiva la placa principal (la que tiene luz LED1) con el interruptor de encendido. Espera unos 20 segundos. En una de las dos placas restantes, se apagarán las luces LED2 (Router) y LED1 (Líder). Ahora, este dispositivo es el líder de la red Thread.
Vuelve a activar el panel de líderes original. Debería volver a unirse automáticamente a la red Thread como un dispositivo final (LED3 está encendido). En 20 segundos (el jitter de selección de router) se promueve a un router (LED2 está encendido).
Restablece las placas
Apague las tres placas y vuelva a encenderlas para observar los LED. La primera placa que se debe encender debe comenzar en la función de líder (LED1 está encendida), es decir, el primer router de una red Thread se convierte automáticamente en el líder.
Las otras dos placas se conectan primero a la red como dispositivos finales (LED3 está encendido), pero deberían promocionarse a routers (LED2 está encendido) en un plazo de 20 segundos.
Particiones de red
Si tus placas no reciben suficiente energía o la conexión de radio entre ellas es débil, la red Thread podría dividirse en particiones y podría tener más de un dispositivo que se muestre como líder.
Thread se repara automáticamente, por lo que las particiones deberían combinarse en una sola partición con un líder.
14. Demostración: Cómo enviar multidifusión UDP
Si continúas desde el ejercicio anterior, el LED4 no debería estar encendido en ningún dispositivo.
Elige cualquier pizarra y presiona Button1. El LED4 de todas las demás placas de la red Thread que ejecutan la aplicación debería activar o desactivar su estado. Si continúa desde el ejercicio anterior, ahora debería estar activado.
Vuelve a presionar Button1 para el mismo panel. El LED4 de todas las demás placas debería volver a activarse.
Presiona Button1 en otra placa y observa cómo LED4 se activa en las otras placas. Presiona Button1 en una de las placas en las que actualmente se encuentre encendida la luz LED4. La luz LED4 permanece encendida en esa placa, pero se activa en las demás.
Particiones de red
Si tus tablas se particionaron y hay más de un líder entre ellas, el resultado del mensaje multidifusión será distinto entre ellas. Si presionas Button1 en una placa que tiene particiones (y que es, por lo tanto, el único miembro de la red de subprocesos particionada), no se iluminará LED4 en las otras placas en respuesta. Si esto sucede, restablece las placas. Lo ideal sería que reforman una sola red Thread y los mensajes UDP deberían funcionar correctamente.
15. ¡Felicitaciones!
Creaste una aplicación que usa las API de OpenThread.
Ahora sabe lo siguiente:
- Cómo programar los botones y los LED en placas de desarrollo nórdicas nRF52840
- Cómo usar las API de OpenThread comunes y la clase
otInstance
- Cómo supervisar y reaccionar a los cambios de estado de OpenThread
- Cómo enviar mensajes UDP a todos los dispositivos en una red Thread
- Cómo modificar Makefiles
Próximos pasos
A partir de este codelab, prueba los siguientes ejercicios:
- Modifica el módulo de GPIO para usar pines GPIO en lugar de los LED integrados, y conecta LED RGB externos que cambien de color según el rol del router.
- Cómo agregar compatibilidad con GPIO para una plataforma de ejemplo diferente
- En lugar de usar la multidifusión para hacer ping a todos los dispositivos cuando se presiona el botón, usa la API de Router o líder para ubicar y hacer ping en un dispositivo individual.
- Conecta tu red en malla a Internet con un router de borde de OpenThread y realiza una multidifusión desde fuera de la red de Thread para iluminar las luces LED.
Lecturas adicionales
Consulta openthread.io y GitHub para obtener una variedad de recursos de OpenThread, incluidos los siguientes:
- Plataformas compatibles: Descubre todas las plataformas compatibles con OpenThread
- Build OpenThread: Obtén más detalles para compilar y configurar OpenThread
- Thread Primer: Una excelente referencia sobre los conceptos de Thread
Referencia: