Como desenvolver com APIs OpenThread

1. Introdução

26b7f4f6b3ea0700.png

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

A especificação do Thread define um protocolo de comunicação sem fio confiável, seguro e de baixo consumo de energia entre dispositivos com base em IPv6 para aplicações domésticas. 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 OpenThread para iniciar uma rede Thread, monitorar e reagir a mudanças nas funções do dispositivo e enviar mensagens UDP, além de 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 OpenThread comuns 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 Nordic Semiconductor nRF52840
  • 3 cabos USB para micro USB para conectar os quadros
  • Uma máquina Linux com pelo menos três portas USB

Software:

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

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

2. Primeiros passos

Conclua o codelab de hardware

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

  • Detalha todo o software necessário para criação e atualização
  • Ensina como criar o OpenThread e gravar 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. Supõe-se que você já tenha concluído o codelab "Criar uma rede Thread".

Máquina Linux

Este codelab foi criado para usar uma máquina Linux baseada em 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 Nordic Semiconductor nRF52840

Este codelab usa três placas nRF52840 PDK.

a6693da3ce213856.png

Instale o software

Para criar e gravar o OpenThread, é necessário instalar o SEGGER J-Link, as ferramentas de linha de comando nRF5x, o 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 nas 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. Princípios básicos da API OpenThread

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

  • Informações e controle da instância OpenThread.
  • Serviços de aplicativo, como IPv6, UDP e CoAP
  • Gerenciamento de credenciais de rede, além das funções de comissário e associado
  • Gerenciamento de roteadores de borda
  • Recursos avançados, como supervisão de crianças e detecção de congestionamento

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

Como usar uma API

Para usar uma API, inclua o arquivo principal 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 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 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 quiser adicionar funções específicas da plataforma a um dos aplicativos de exemplo 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-os em um arquivo de origem específico da plataforma. Abstraído dessa forma, você pode 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 para o 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.

Observe que a declaração da função otSysButtonProcess usa um otInstance. Assim, 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 dela na implementação da função, use a macro OT_UNUSED_VARIABLE da API OpenThread para suprimir erros de build em torno de variáveis não usadas em algumas toolchains. Vamos conferir exemplos disso mais tarde.

5. Implementar a abstração da plataforma GPIO

Na etapa anterior, analisamos 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 de 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: adicione definições.

Essas definições servem como abstrações entre valores e variáveis específicos do nRF52840 usados no nível do aplicativo 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 (link em inglês).

AÇÃO: adicione inclusões de cabeçalho.

Em seguida, adicione os includes de cabeçalho 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.

Adicione este código em seguida. A função in_pin1_handler é o callback registrado quando a funcionalidade de pressionamento de 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 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 alternar 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 o quadro para um toque de botão, e a segunda envia a mensagem UDP de 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 LEDs diferentes acendam dependendo do papel do dispositivo. Vamos acompanhar as seguintes funções: líder, roteador e dispositivo final. Podemos atribuí-los a LEDs da seguinte forma:

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

Para ativar essa funcionalidade, o aplicativo precisa saber quando a função do dispositivo mudou e como acender o LED correto em resposta. Vamos usar a instância do OpenThread na primeira parte e a abstração da plataforma GPIO na 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 inclusões de cabeçalho.

Na seção de inclusões 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 de gerenciador para a mudança de estado da instância do OpenThread.

Adicione essa declaração a main.c, depois dos includes de cabeçalho 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 de gerenciador de mudança de estado.

Em main.c, adicione essa função à função main() após a chamada otAppCliInit. Esse registro de callback informa ao OpenThread para 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, depois da função main(), implemente a função handleNetifStateChanged. Essa função verifica a flag OT_CHANGED_THREAD_ROLE da instância 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: usar multicast para acender um LED

No nosso aplicativo, também queremos enviar mensagens UDP para todos os outros dispositivos na rede quando o Button1 é 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
  • Ser capaz de enviar uma mensagem UDP para o endereço multicast local da malha
  • Processar mensagens UDP recebidas
  • Alternar 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 inclusões de cabeçalho.

Na seção de inclusões na parte de cima do arquivo main.c, adicione os arquivos de cabeçalho da API necessários para o recurso UDP 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 tratam erros de maneira adequada.

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

No arquivo main.c, depois da seção de inclusões 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 de multicast local da malha. Todas as mensagens enviadas para esse endereço serão enviadas para todos os dispositivos de encadeamento completo na rede. Consulte Multicast em 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 de otTaskletsSignalPending e antes da função main(), adicione funções específicas do UDP, além de 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 do UDP.

Em main.c, adicione esta função à função main() depois da 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, em caso afirmativo, chama o manipulador (handleButtonInterrupt) definido na etapa acima.

otSysButtonProcess(instance);

AÇÃO: implemente um 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: implemente a inicialização do UDP.

Em main.c, adicione a implementação da função initUdp depois da 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 soquete e registra uma função de callback (handleUdpReceive) para quando uma mensagem UDP é recebida. otUdpBind vincula o soquete à 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: implemente o envio de mensagens UDP.

Em main.c, adicione a implementação da função sendUdp depois da 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 maneira adequada, pulando para o bloco exit, onde libera o buffer.

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

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

Em main.c, adicione a implementação da função handleUdpReceive depois da função sendUdp que você acabou de adicionar. Essa função apenas 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 e se conectem a uma rede imediatamente 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 nosso aplicativo mais seguro e limitar os nós do Thread na nossa rede apenas àqueles que executam o aplicativo.

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 a inclusão de cabeçalho.

Na seção de inclusões na parte de cima do arquivo main.c, adicione o arquivo principal da API necessário para configurar a rede Thread:

#include <openthread/dataset_ftd.h>

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

Adicione essa declaração a main.c, depois dos includes de cabeçalho 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 e a pilha de rede 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: implemente 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 de rede Thread que estamos usando para este aplicativo são:

  • Canal = 15
  • PAN ID = 0x2222
  • Extended PAN ID = C0DE1AB5C0DE1AB5
  • Network Key = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Network Name = 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 thread completo). Saiba mais sobre isso na próxima etapa.

9. API: funções restritas

Algumas APIs do OpenThread modificam configurações que só devem ser alteradas 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) necessário para um dispositivo final 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. Assim, você não precisa esperar muito tempo para que um nó de encadeamento mude de função.

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

Para confirmar isso, consulte 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 o aplicativo, é necessário fazer algumas pequenas atualizações 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 CMakeLists.txt da NordicSemiconductor para garantir que as funções GPIO sejam definidas no aplicativo.

AÇÃO: adicione flags ao arquivoCMakeLists.txt .

Abra ./third_party/NordicSemiconductor/CMakeLists.txt no editor de texto de sua preferência e adicione as seguintes linhas na 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 origem gpio ao arquivo ./src/CMakeLists.txt .

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 da NordicSemiconductor para que ele seja incluído no build da biblioteca dos drivers da Nordic.

AÇÃO: adicione o driver gpio ao arquivo CMakeLists.txt 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 gravar o aplicativo em todas as três placas de desenvolvimento Nordic nRF52840. Cada dispositivo vai funcionar como um dispositivo Thread completo (FTD, na sigla em inglês).

Criar o OpenThread

Crie os binários 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

Acesse o diretório com o binário da CLI FTD do OpenThread e converta para o formato hexadecimal com a cadeia de ferramentas incorporada do ARM:

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

Atualizar 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 conector de alimentação externa na placa nRF52840 e conecte-o à máquina Linux. Defina corretamente, o 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 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 vai se apagar brevemente durante o processo. A seguinte saída é gerada em caso de 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 esta etapa de "atualizar as placas" para as outras duas placas. Cada placa precisa ser conectada à máquina Linux da mesma forma, e o comando para atualizar é o mesmo, exceto pelo número de série da placa. Use o número de série exclusivo de cada placa no

Comando de atualização nrfjprog.

Se a operação for bem-sucedida, o LED1, o LED2 ou o LED3 vai acender em cada placa. Você pode até ver o LED aceso mudar de 3 para 2 (ou de 2 para 1) logo após o flash (o recurso de mudança de função do dispositivo).

12. Funcionalidade do aplicativo

As três placas nRF52840 agora devem estar ligadas e executando o aplicativo OpenThread. Como detalhado anteriormente, esse aplicativo tem dois recursos principais.

Indicadores de função do dispositivo

O LED aceso em cada placa reflete a função atual do nó do 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 visto essas mudanças em uma ou duas placas em até 20 segundos após a inicialização de cada dispositivo.

UDP multicast

Quando o Button1 é pressionado em uma placa, uma mensagem UDP é enviada para o endereço de multicast local da malha, que inclui todos os outros nós na rede Thread. Em resposta ao recebimento dessa mensagem, o LED4 em todas as outras placas é ativado ou desativado. O LED4 permanece ativado ou desativado para cada placa até receber outra mensagem UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demonstração: observar mudanças na função do dispositivo

Os dispositivos que você atualizou são um tipo específico de dispositivo Thread completo (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 um dispositivo final e podem se promover de um dispositivo final para um roteador.

O Thread pode oferecer suporte a até 32 roteadores, mas tenta manter o número entre 16 e 23. Se um REED se conectar como um dispositivo final e o número de roteadores for inferior a 16, ele será promovido automaticamente a um roteador. Essa mudança vai ocorrer em um momento aleatório dentro do número de segundos que você definiu para 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á um 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, outro roteador vai se promover a líder para garantir que a rede ainda tenha um líder.

Desligue a placa principal (a que tem o LED1 aceso) usando o interruptor Liga/desliga. Aguarde cerca de 20 segundos. Em uma das duas placas restantes, o LED2 (roteador) vai desligar e o LED1 (líder) vai acender. Este dispositivo agora é o líder da rede Thread.

4c57c87adb40e0e3.png

Ative o quadro de líderes original novamente. Ele vai se reconectar automaticamente à rede Thread como um dispositivo final (o LED3 acende). Em até 20 segundos (o jitter de seleção de roteador), ele se promove a um roteador (o LED2 acende).

5f40afca2dcc4b5b.png

Redefinir os quadros

Desligue as três placas e ligue-as novamente. Observe os LEDs. A primeira placa ligada deve começar na função de líder (o LED1 fica aceso). O primeiro roteador em uma rede Thread se torna automaticamente o líder.

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

Partições de rede

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

A linha de execução é autorrecuperável, então as partições acabam sendo mescladas em uma única partição com um líder.

14. Demonstração: enviar multicast UDP

Se você estiver continuando do exercício anterior, o LED4 não deve estar aceso em nenhum dispositivo.

Escolha qualquer tabuleiro e pressione o botão 1. O LED4 em todas as outras placas na rede Thread que executam o aplicativo deve alternar o estado. Se você estiver continuando do exercício anterior, eles já estarão ativados.

f186a2618fdbe3fd.png

Pressione o botão 1 para a mesma ação de novo. O LED4 em todas as outras placas deve piscar novamente.

Pressione Button1 em outra placa e observe como o LED4 alterna nas outras placas. Pressione o botão 1 em uma das placas em que o LED4 está aceso. O LED4 permanece aceso nessa placa, mas pisca nas outras.

f5865ccb8ab7aa34.png

Partições de rede

Se os quadros forem particionados e houver mais de um líder entre eles, o resultado da mensagem multicast será diferente entre os quadros. Se você pressionar o botão 1 em uma placa 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 as placas. O ideal é que elas formem uma única rede Thread e que as mensagens UDP funcionem corretamente.

15. Parabéns!

Você criou um aplicativo que usa as APIs OpenThread.

Agora você sabe:

  • Como programar os botões e LEDs nas placas de desenvolvimento Nordic nRF52840
  • Como usar APIs OpenThread comuns 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:

  • Modifique o módulo GPIO para usar pinos GPIO em vez dos LEDs integrados e conecte LEDs RGB externos que mudam de cor com base na função 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 o pressionamento de um botão, 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 transmita por multicast de fora da rede Thread para acender os LEDs.

Leitura adicional

Confira openthread.io e GitHub para acessar vários recursos do OpenThread, incluindo:

Referência: