Como desenvolver com APIs OpenThread

Sobre este codelab
schedule60 minutos
subjectÚltimo 5 de maio de 2025 atualizado
account_circleEscrito por Jeff Bumgardner

1. Introdução

26b7f4f6b3ea0700.png

A OpenThread lançada pelo Nest é uma implementação de código aberto do protocolo de rede Thread®. O Nest lançou o OpenThread para disponibilizar a tecnologia usada nos produtos Nest de forma ampla aos desenvolvedores e acelerar o desenvolvimento de produtos para a casa conectada.

A especificação Thread define um protocolo de comunicação entre dispositivos sem fio, confiável, seguro e de baixo consumo energético baseado em IPv6 para aplicativos domésticos. O OpenThread implementa todas as camadas de rede Thread, incluindo IPv6, 6LoWPAN, IEEE 802.15.4 com segurança MAC, estabelecimento de link de malha e roteamento de malha.

Neste codelab, você vai usar as APIs do OpenThread para iniciar uma rede Thread, monitorar e reagir a mudanças nas funções do dispositivo, enviar mensagens UDP e vincular essas ações a botões e LEDs em hardware real.

2a6db2e258c32237.png

O que você vai aprender

  • Como programar os botões e LEDs nas placas de desenvolvimento Nordic nRF52840
  • Como usar APIs comuns do OpenThread e a classe otInstance
  • Como monitorar e reagir a mudanças de estado do OpenThread
  • Como enviar mensagens UDP para todos os dispositivos em uma rede Thread
  • Como modificar Makefiles

O que é necessário

Hardware:

  • 3 placas de desenvolvimento nRF52840 da Nordic Semiconductor
  • Três cabos USB para micro USB para conectar as placas
  • Uma máquina Linux com pelo menos três portas USB

Software:

  • Conjunto de ferramentas do GNU
  • Ferramentas de linha de comando nRF5x da Nordic
  • Software Segger J-Link
  • OpenThread
  • Git

Exceto quando indicado o contrário, o conteúdo deste Codelab está licenciado de acordo com a Licença de Atribuição 3.0 do Creative Commons, e os exemplos de código estão licenciados de acordo com a Licença Apache 2.0.

2. Primeiros passos

Concluir o codelab de hardware

Antes de iniciar este codelab, conclua o codelab Criar uma rede Thread com placas nRF52840 e OpenThread, que:

  • Detalha todos os softwares necessários para criar e fazer o flash
  • Ensina como criar o OpenThread e fazer o flash dele em placas Nordic nRF52840
  • Demonstra os conceitos básicos de uma rede Thread

Nenhuma das configurações de ambiente necessárias para criar o OpenThread e atualizar as placas é detalhada neste codelab, apenas instruções básicas para atualizar as placas. Presume-se que você já concluiu o codelab "Criar uma rede Thread".

Máquina Linux

Este codelab foi criado para máquinas Linux com i386 ou x86 para atualizar todas as placas de desenvolvimento do Thread. Todas as etapas foram testadas no Ubuntu 14.04.5 LTS (Trusty Tahr).

Placas nRF52840 da Nordic Semiconductor

Este codelab usa três placas PDK nRF52840.

a6693da3ce213856.png

Instale o software

Para criar e atualizar o OpenThread, é necessário instalar o SEGGER J-Link, as ferramentas de linha de comando nRF5x, a ARM GNU Toolchain e vários pacotes do Linux. Se você concluiu o codelab Criar uma rede Thread conforme necessário, já terá tudo o que precisa instalado. Caso contrário, conclua esse codelab antes de continuar para garantir que você possa criar e atualizar o OpenThread para placas de desenvolvimento nRF52840.

3. Clonar o repositório

O OpenThread vem com um exemplo de código de aplicativo que pode ser usado como ponto de partida para este codelab.

Clone o repositório de exemplos do OpenThread Nordic nRF528xx e crie o OpenThread:

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

4. Noções básicas sobre a API OpenThread

As APIs públicas do OpenThread estão localizadas em ./openthread/include/openthread no repositório do OpenThread. Essas APIs oferecem acesso a vários recursos e funcionalidades do OpenThread no nível da plataforma e da linha de execução para uso nos seus aplicativos:

  • Informações e controle de instâncias do OpenThread
  • Serviços de aplicativo, como IPv6, UDP e CoAP
  • Gerenciamento de credenciais de rede e papéis de comissário e participante
  • Gerenciamento do roteador de borda
  • Recursos aprimorados, como a supervisão infantil e a detecção de interferência

As informações de referência sobre todas as APIs do OpenThread estão disponíveis em openthread.io/reference.

Como usar uma API

Para usar uma API, inclua o arquivo de cabeçalho dela em um dos arquivos do aplicativo. Em seguida, chame a função desejada.

Por exemplo, o app de exemplo da CLI incluído no OpenThread usa os seguintes cabeçalhos 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>

A instância do OpenThread

A estrutura otInstance é algo que você vai usar com frequência ao trabalhar com as APIs do OpenThread. Depois de inicializada, essa estrutura representa uma instância estática da biblioteca OpenThread e permite que o usuário faça chamadas de API do OpenThread.

Por exemplo, a instância do OpenThread é inicializada na função main() do app de exemplo da 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;
}

Funções específicas da plataforma

Se você quiser adicionar funções específicas da plataforma a um dos exemplos de aplicativos incluídos no OpenThread, primeiro declare-as no cabeçalho ./openthread/examples/platforms/openthread-system.h, usando o namespace otSys para todas as funções. Em seguida, implemente-as em um arquivo de origem específico da plataforma. Com essa abstração, é possível usar os mesmos cabeçalhos de função para outras plataformas de exemplo.

Por exemplo, as funções GPIO que vamos usar para conectar os botões e LEDs do nRF52840 precisam ser declaradas em openthread-system.h.

Abra o arquivo ./openthread/examples/platforms/openthread-system.h no editor de texto de sua preferência.

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

AÇÃO: adicione declarações de função GPIO específicas da plataforma.

Adicione estas declarações de função após o #include do cabeçalho 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);

Vamos implementar isso na próxima etapa.

A declaração da função otSysButtonProcess usa um otInstance. Dessa forma, o aplicativo pode acessar informações sobre a instância do OpenThread quando um botão é pressionado, se necessário. Tudo depende das necessidades do seu aplicativo. Se você não precisar dele na implementação da função, use a macro OT_UNUSED_VARIABLE da API OpenThread para suprimir erros de build em variáveis não utilizadas para alguns toolchains. Vamos conferir exemplos disso mais tarde.

5. Implementar a abstração da plataforma GPIO

Na etapa anterior, abordamos as declarações de função específicas da plataforma em ./openthread/examples/platforms/openthread-system.h que podem ser usadas para GPIO. Para acessar botões e LEDs nas placas de desenvolvimento nRF52840, é necessário implementar essas funções para a plataforma nRF52840. Neste código, você vai adicionar funções que:

  • Inicializar pinos e modos GPIO
  • Controlar a tensão em um pino
  • Ativar interrupções GPIO e registrar um callback

No diretório ./src/src, crie um arquivo chamado gpio.c. Nesse novo arquivo, adicione o seguinte conteúdo.

./src/src/gpio.c (novo arquivo)

AÇÃO: adicionar definições.

Essas definições servem como abstrações entre valores específicos do nRF52840 e variáveis usadas no nível do aplicativo do 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 mais informações sobre botões e LEDs do nRF52840, consulte o Infocenter da Nordic Semiconductor.

AÇÃO: adicione cabeçalhos de inclusão.

Em seguida, adicione os cabeçalhos necessários para a funcionalidade 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"

AÇÃO: adicione funções de callback e interrupção para o botão 1.

Em seguida, adicione este código. A função in_pin1_handler é o callback registrado quando a funcionalidade de pressionar o botão é inicializada (mais adiante neste arquivo).

Observe como esse callback usa a macro OT_UNUSED_VARIABLE, já que as variáveis transmitidas para in_pin1_handler não são realmente usadas na função.

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

AÇÃO: adicione uma função para configurar os LEDs.

Adicione este código para configurar o modo e o estado de todos os LEDs durante a inicialização.

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

AÇÃO: adicione uma função para definir o modo de um LED.

Essa função será usada quando o papel do dispositivo mudar.

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

AÇÃO: adicione uma função para alternar o modo de um LED.

Essa função será usada para ativar o LED4 quando o dispositivo receber uma mensagem 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;
    }
}

AÇÃO: adicione funções para inicializar e processar pressionamentos de botão.

A primeira função inicializa a placa para um botão pressionado, e a segunda envia a mensagem UDP multicast quando o botão 1 é pressionado.

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

AÇÃO: salve e feche o arquivo gpio.c .

6. API: reagir a mudanças de função do dispositivo

No nosso aplicativo, queremos que diferentes LEDs se iluminem dependendo da função do dispositivo. Vamos acompanhar os seguintes papéis: líder, roteador e dispositivo final. Podemos atribuir esses valores aos LEDs da seguinte maneira:

  • LED1 = líder
  • LED2 = Roteador
  • LED3 = dispositivo final

Para ativar essa funcionalidade, o aplicativo precisa saber quando o papel do dispositivo mudou e como acender o LED correto em resposta. Vamos usar a instância do OpenThread para a primeira parte e a abstração da plataforma GPIO para a segunda.

Abra o arquivo ./openthread/examples/apps/cli/main.c no editor de texto de sua preferência.

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

AÇÃO: adicione cabeçalhos de inclusão.

Na seção "includes" do arquivo main.c, adicione os arquivos de cabeçalho da API necessários para o recurso de mudança de função.

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

AÇÃO: adicione a declaração da função do gerenciador para a mudança de estado da instância do OpenThread.

Adicione esta declaração a main.c, depois que o cabeçalho for incluído e antes de qualquer instrução #if. Essa função será definida após o aplicativo principal.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

AÇÃO: adicione um registro de callback para a função gerenciadora de mudança de estado.

Em main.c, adicione essa função à função main() após a chamada otAppCliInit. Esse registro de callback instrui o OpenThread a chamar a função handleNetifStateChange sempre que o estado da instância do OpenThread mudar.

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

AÇÃO: adicione a implementação da mudança de estado.

Em main.c, após a função main(), implemente a função handleNetifStateChanged. Essa função verifica a flag OT_CHANGED_THREAD_ROLE da instância do OpenThread e, se ela tiver mudado, liga/desliga os LEDs conforme necessário.

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: use multicast para ativar um LED

No nosso aplicativo, também queremos enviar mensagens UDP para todos os outros dispositivos na rede quando o botão 1 for pressionado em uma placa. Para confirmar o recebimento da mensagem, vamos ativar o LED4 nas outras placas em resposta.

Para ativar essa funcionalidade, o aplicativo precisa:

  • Inicializar uma conexão UDP na inicialização
  • Enviar uma mensagem UDP para o endereço multicast local de malha
  • Processar mensagens UDP de entrada
  • Ativar o LED4 em resposta a mensagens UDP recebidas

Abra o arquivo ./openthread/examples/apps/cli/main.c no editor de texto de sua preferência.

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

AÇÃO: adicione cabeçalhos de inclusão.

Na seção de inclusão na parte de cima do arquivo main.c, adicione os arquivos de cabeçalho da API necessários para o recurso UDP de multicast.

#include <string.h>

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

#include "utils/code_utils.h"

O cabeçalho code_utils.h é usado para as macros otEXPECT e otEXPECT_ACTION, que validam condições de execução e processam erros de maneira adequada.

AÇÃO: adicionar definições e constantes

No arquivo main.c, após a seção de inclusão e antes de qualquer instrução #if, adicione constantes e definições específicas do UDP:

#define UDP_PORT 1212

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

ff03::1 é o endereço multicast local de malha. Todas as mensagens enviadas para esse endereço serão enviadas para todos os dispositivos Full Thread na rede. Consulte Multicast no openthread.io para mais informações sobre o suporte a multicast no OpenThread.

AÇÃO: adicione declarações de função.

No arquivo main.c, depois da definição otTaskletsSignalPending e antes da função main(), adicione funções específicas do UDP e uma variável estática para representar um soquete 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;

AÇÃO: adicione chamadas para inicializar os LEDs e o botão GPIO.

Em main.c, adicione essas chamadas de função à função main() após a chamada otSetStateChangedCallback. Essas funções inicializam os pinos GPIO e GPIOTE e definem um gerenciador de botões para processar eventos de pressionamento de botão.

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

AÇÃO: adicione a chamada de inicialização UDP.

Em main.c, adicione esta função à função main() após a chamada otSysButtonInit que você acabou de adicionar:

initUdp(instance);

Essa chamada garante que um soquete UDP seja inicializado na inicialização do aplicativo. Sem isso, o dispositivo não pode enviar nem receber mensagens UDP.

AÇÃO: adicione uma chamada para processar o evento do botão GPIO.

Em main.c, adicione essa chamada de função à função main() após a chamada otSysProcessDrivers, no loop while. Essa função, declarada em gpio.c, verifica se o botão foi pressionado e, se sim, chama o gerenciador (handleButtonInterrupt) que foi definido na etapa acima.

otSysButtonProcess(instance);

AÇÃO: implementar o gerenciador de interrupção de botão.

Em main.c, adicione a implementação da função handleButtonInterrupt após a função handleNetifStateChanged que você adicionou na etapa anterior:

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

AÇÃO: implementar a inicialização do UDP.

Em main.c, adicione a implementação da função initUdp após a função handleButtonInterrupt que você acabou de adicionar:

/**
 * 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 é a porta que você definiu anteriormente (1212). A função otUdpOpen abre o socket e registra uma função de callback (handleUdpReceive) para quando uma mensagem UDP é recebida. otUdpBind vincula o socket à interface de rede Thread transmitindo OT_NETIF_THREAD. Para outras opções de interface de rede, consulte a enumeração otNetifIdentifier na Referência da API UDP.

AÇÃO: implementar a mensagem UDP.

Em main.c, adicione a implementação da função sendUdp após a função initUdp que você acabou de adicionar:

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

Observe as macros otEXPECT e otEXPECT_ACTION. Elas garantem que a mensagem UDP seja válida e alocada corretamente no buffer. Caso contrário, a função processa os erros de forma adequada, pulando para o bloco exit, onde libera o buffer.

Consulte as referências de IPv6 e UDP no openthread.io para mais informações sobre as funções usadas para inicializar o UDP.

AÇÃO: implementar o processamento de mensagens UDP.

Em main.c, adicione a implementação da função handleUdpReceive após a função sendUdp que você acabou de adicionar. Essa função simplesmente alterna o 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: configurar a rede Thread

Para facilitar a demonstração, queremos que os dispositivos iniciem o Thread imediatamente e se conectem em uma rede quando forem ligados. Para isso, vamos usar a estrutura otOperationalDataset. Essa estrutura contém todos os parâmetros necessários para transmitir as credenciais da rede Thread a um dispositivo.

O uso dessa estrutura vai substituir os padrões de rede integrados ao OpenThread para tornar o aplicativo mais seguro e limitar os nós Thread na rede apenas aos que executam o aplicativo.

Novamente, abra o arquivo ./openthread/examples/apps/cli/main.c no editor de texto de sua preferência.

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

AÇÃO: adicione o cabeçalho include.

Na seção de inclusão na parte de cima do arquivo main.c, adicione o arquivo de cabeçalho da API que você vai precisar para configurar a rede Thread:

#include <openthread/dataset_ftd.h>

AÇÃO: adicione a declaração de função para definir a configuração de rede.

Adicione esta declaração a main.c, depois que o cabeçalho for incluído e antes de qualquer instrução #if. Essa função será definida após a função principal do aplicativo.

static void setNetworkConfiguration(otInstance *aInstance);

AÇÃO: adicione a chamada de configuração de rede.

Em main.c, adicione essa chamada de função à função main() após a chamada otSetStateChangedCallback. Essa função configura o conjunto de dados da rede Thread.

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

AÇÃO: adicione chamadas para ativar a interface de rede e a pilha Thread.

Em main.c, adicione essas chamadas de função à função main() após a chamada otSysButtonInit.

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

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

AÇÃO: implementar a configuração da rede Thread.

Em main.c, adicione a implementação da função setNetworkConfiguration após a função 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);
}

Conforme detalhado na função, os parâmetros da rede Thread que estamos usando para este aplicativo são:

  • Canal = 15
  • ID PAN = 0x2222
  • ID do PAN estendido = C0DE1AB5C0DE1AB5
  • Chave de rede = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Nome da rede = OTCodelab

Além disso, é aqui que diminuímos o jitter de seleção de roteador para que nossos dispositivos mudem de função mais rapidamente para fins de demonstração. Isso só é feito se o nó for um FTD (dispositivo de linha completa). Mais informações sobre isso na próxima etapa.

9. API: funções restritas

Algumas das APIs do OpenThread modificam configurações que só devem ser modificadas para fins de demonstração ou teste. Essas APIs não devem ser usadas em uma implantação de produção de um aplicativo que usa o OpenThread.

Por exemplo, a função otThreadSetRouterSelectionJitter ajusta o tempo (em segundos) que um dispositivo final leva para se promover a um roteador. O padrão para esse valor é 120, de acordo com a especificação do Thread. Para facilitar o uso neste codelab, vamos mudar para 20, para que você não precise esperar muito para que um nó de linha de execução mude de função.

Observação: os dispositivos MTD não se tornam roteadores, e o suporte a uma função como otThreadSetRouterSelectionJitter não está incluído em um build MTD. Mais tarde, precisamos especificar a opção -DOT_MTD=OFF do CMake. Caso contrário, o build vai falhar.

Para confirmar isso, analise a definição da função otThreadSetRouterSelectionJitter, que está contida em uma diretiva de pré-processador 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. Atualizações do CMake

Antes de criar seu aplicativo, é necessário fazer algumas atualizações menores em três arquivos CMake. Eles são usados pelo sistema de build para compilar e vincular seu aplicativo.

./third_party/NordicSemiconductor/CMakeLists.txt

Agora, adicione algumas flags ao NordicSemiconductor CMakeLists.txt para garantir que as funções GPIO sejam definidas no aplicativo.

AÇÃO: adicione flags ao arquivo CMakeLists.txt .

Abra ./third_party/NordicSemiconductor/CMakeLists.txt no editor de texto de sua preferência e adicione as linhas a seguir à seção 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

Edite o arquivo ./src/CMakeLists.txt para adicionar o novo arquivo de origem gpio.c:

AÇÃO: adicione a fonte GPIO ao ./src/CMakeLists.txt arquivo.

Abra ./src/CMakeLists.txt no editor de texto de sua preferência e adicione o arquivo à seção NRF_COMM_SOURCES.

...

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

...

./third_party/NordicSemiconductor/CMakeLists.txt

Por fim, adicione o arquivo de driver nrfx_gpiote.c ao arquivo CMakeLists.txt do NordicSemiconductor para que ele seja incluído no build da biblioteca dos drivers Nordic.

AÇÃO: adicione o driver gpio ao arquivo CMakeLists.txt do NordicSemiconductor.

Abra ./third_party/NordicSemiconductor/CMakeLists.txt no editor de texto de sua preferência e adicione o arquivo à seção COMMON_SOURCES.

...

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

11. Configurar os dispositivos

Com todas as atualizações de código concluídas, você está pronto para criar e atualizar o aplicativo nas três placas de desenvolvimento Nordic nRF52840. Cada dispositivo vai funcionar como um dispositivo de linha completa (FTD, na sigla em inglês).

Criar OpenThread

Crie os binários do FTD do OpenThread para a plataforma nRF52840.

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

Navegue até o diretório com o binário da CLI do FTD do OpenThread e converta-o para o formato hexadecimal com o Toolchain ARM Embedded:

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

Mostrar os quadros

Faça o flash do arquivo ot-cli-ftd.hex em cada placa nRF52840.

Conecte o cabo USB à porta de depuração micro USB ao lado do pino de alimentação externo na placa nRF52840 e conecte-o à máquina Linux. Definir corretamente, LED5 está aceso.

20a3b4b480356447.png

Como antes, anote o número de série da placa nRF52840:

c00d519ebec7e5f0.jpeg

Navegue até o local das ferramentas de linha de comando nRFx e faça o flash do arquivo hexadecimal FTD da CLI do OpenThread na placa nRF52840 usando o número de série dela:

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

O LED5 será desligado brevemente durante a atualização. A saída a seguir é gerada após o sucesso:

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.

Repita a etapa "Mostrar as placas" para as outras duas placas. Cada placa precisa ser conectada à máquina Linux da mesma maneira, e o comando para flash é o mesmo, exceto o número de série da placa. Use o número de série exclusivo de cada placa no

nrfjprog comando de ativação.

Se for bem-sucedido, o LED1, LED2 ou LED3 vai acender em cada placa. Talvez você até veja o LED aceso mudar de 3 para 2 (ou de 2 para 1) logo após a inicialização (o recurso de mudança de função do dispositivo).

12. Funcionalidade do app

Agora, as três placas nRF52840 estão conectadas e executando nosso aplicativo OpenThread. Como detalhado anteriormente, este aplicativo tem dois recursos principais.

Indicadores de função do dispositivo

O LED aceso em cada placa reflete o papel atual do nó Thread:

  • LED1 = líder
  • LED2 = Roteador
  • LED3 = dispositivo final

À medida que a função muda, o LED aceso também muda. Você já deve ter notado essas mudanças em um ou dois painéis dentro de 20 segundos após a inicialização de cada dispositivo.

UDP multicast

Quando o botão 1 é pressionado em um dispositivo, uma mensagem UDP é enviada para o endereço multicast local de malha, que inclui todos os outros nós na rede Thread. Em resposta ao recebimento dessa mensagem, o LED4 em todos os outros cartões é ativado ou desativado. O LED4 permanece ativado ou desativado em cada placa até receber outra mensagem UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demonstração: observe as mudanças de função do dispositivo

Os dispositivos que você atualizou são um tipo específico de dispositivo completo do Thread (FTD, na sigla em inglês), chamado de dispositivo final qualificado para roteador (REED, na sigla em inglês). Isso significa que eles podem funcionar como um roteador ou dispositivo final e podem ser promovidos de um dispositivo final para um roteador.

O Thread pode oferecer suporte a até 32 roteadores, mas tenta manter o número de roteadores entre 16 e 23. Se um REED for conectado como um dispositivo final e o número de roteadores for inferior a 16, ele será promovido automaticamente a um roteador. Essa mudança precisa ocorrer em um momento aleatório dentro do número de segundos em que você definiu o valor otThreadSetRouterSelectionJitter no aplicativo (20 segundos).

Cada rede Thread também tem um líder, que é um roteador responsável por gerenciar o conjunto de roteadores em uma rede Thread. Com todos os dispositivos ligados, após 20 segundos, um deles será o líder (LED1 aceso) e os outros dois serão roteadores (LED2 aceso).

4e1e885861a66570.png

Remover o líder

Se o líder for removido da rede Thread, um roteador diferente vai se promover para líder, para garantir que a rede ainda tenha um líder.

Desligue o placar de líderes (aquele com o LED1 aceso) usando o interruptor Power. Aguarde cerca de 20 segundos. Em uma das duas placas restantes, o LED2 (roteador) será desligado e o LED1 (líder) será ligado. Esse dispositivo agora é o líder da rede Thread.

4c57c87adb40e0e3.png

Ative o placar original novamente. Ele vai se juntar automaticamente à rede Thread como um dispositivo final (LED3 aceso). Em 20 segundos (o jitter de seleção do roteador), ele se promove para um roteador (LED2 acende).

5f40afca2dcc4b5b.png

Redefinir os quadros

Desligue as três placas, ligue-as novamente e observe os LEDs. O primeiro dispositivo que foi ligado precisa começar na função de líder (LED1 aceso). O primeiro roteador em uma rede Thread se torna o líder automaticamente.

As outras duas placas se conectam inicialmente à rede como dispositivos finais (LED3 aceso), mas precisam ser promovidas a roteadores (LED2 aceso) em até 20 segundos.

Partições de rede

Se as placas não estiverem recebendo energia suficiente ou a conexão de rádio entre elas estiver fraca, a rede Thread poderá se dividir em partições, e talvez mais de um dispositivo apareça como líder.

A linha de execução é autocorretiva, então as partições devem ser mescladas novamente em uma única partição com um líder.

14. Demonstração: enviar multicast UDP

Se você continuar a partir do exercício anterior, o LED4 não vai estar aceso em nenhum dispositivo.

Escolha qualquer placa e pressione o botão 1. O LED4 em todas as outras placas na rede Thread que executam o aplicativo precisa alternar o estado. Se você estiver continuando o exercício anterior, elas devem estar ativas.

f186a2618fdbe3fd.png

Pressione o botão 1 para o mesmo tabuleiro novamente. O LED4 em todas as outras placas vai mudar de novo.

Pressione o botão 1 em uma placa diferente e observe como o LED4 alterna nas outras placas. Pressione o botão 1 em uma das placas em que o LED4 está ativado. O LED4 permanece aceso para essa placa, mas alterna para as outras.

f5865ccb8ab7aa34.png

Partições de rede

Se as suas mesas tiverem sido divididas e houver mais de um líder entre elas, o resultado da mensagem de multicast vai ser diferente entre as mesas. Se você pressionar o botão 1 em uma placa que foi particionada (e, portanto, é o único membro da rede Thread particionada), o LED4 nas outras placas não vai acender em resposta. Se isso acontecer, redefina os cartões. O ideal é que eles reformem uma única rede Thread e que as mensagens UDP funcionem corretamente.

15. Parabéns!

Você criou um aplicativo que usa APIs do OpenThread.

Agora você sabe:

  • Como programar os botões e LEDs nas placas de desenvolvimento Nordic nRF52840
  • Como usar APIs comuns do OpenThread e a classe otInstance
  • Como monitorar e reagir a mudanças de estado do OpenThread
  • Como enviar mensagens UDP para todos os dispositivos em uma rede Thread
  • Como modificar Makefiles

Próximas etapas

Com base neste codelab, faça os exercícios a seguir:

  • Modificar o módulo GPIO para usar pinos GPIO em vez dos LEDs integrados e conectar LEDs RGB externos que mudam de cor com base no papel do roteador
  • Adicionar suporte a GPIO para uma plataforma de exemplo diferente
  • Em vez de usar multicast para fazer ping em todos os dispositivos com um botão pressionado, use a API Router/Leader para localizar e fazer ping em um dispositivo individual.
  • Conecte sua rede mesh à Internet usando um roteador de borda OpenThread e faça multicast dela fora da rede Thread para acender os LEDs.

Leitura adicional

Confira openthread.io e GitHub para conferir uma variedade de recursos do OpenThread, incluindo:

Referência: