Como desenvolver com APIs OpenThread

1. Introdução

26b7f4f6b3ea0700.png

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

A especificação do Thread define um protocolo de comunicação entre dispositivos sem fio confiável, seguro e de baixo consumo de energia 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 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 hardwares reais.

2a6db2e258c32237.png

O que você vai aprender

  • Como programar os botões e os LEDs nas placas de desenvolvimento nórdicas 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 nórdicas nRF52840 de semicondutores
  • Três cabos USB para micro USB para conectar as placas
  • Um computador Linux com pelo menos três portas USB

Software:

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

Salvo indicação em 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 do Apache 2.0.

2. Primeiros passos

Concluir o codelab sobre 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 criação e atualização
  • Ensina a criar o OpenThread e fazer a instalação dele em placas nórdicas 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 está detalhada neste codelab. São apenas instruções básicas para atualizar as placas. Presume-se que você já tenha concluído o codelab "Build a Thread Network".

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 de semicondutores nórdicos nRF52840

Este codelab usa três placas nRF52840 PDK.

a6693da3ce213856.png

Instale o software

Para criar e atualizar o OpenThread, você precisa instalar o SEGGER J-Link, as ferramentas de linha de comando nRF5x, o conjunto de ferramentas GNU ARM e vários pacotes do Linux. Se você concluiu o codelab "Build a Thread Network" conforme necessário, já tem tudo o que precisa instalado. Caso contrário, conclua o 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 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 para uso nos seus aplicativos, tanto no nível do Thread quanto da plataforma:

  • Informações e controle da instância do OpenThread
  • Serviços de aplicativos, como IPv6, UDP e CoAP
  • Gerenciamento de credenciais de rede, com os papéis de Comissário e Combinador
  • Gerenciamento de roteador de borda
  • Recursos aprimorados, como supervisão de crianças e detecção de Jams

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

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 da 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 você 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-as em um arquivo de origem específico da plataforma. Abstraídas dessa forma, é 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 nRF52840 precisam ser declaradas em openthread-system.h.

Abra o arquivo ./openthread/examples/platforms/openthread-system.h em seu editor de texto preferido.

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

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

Adicione estas declarações de função após a #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 compilação relacionados a variáveis não usadas em alguns conjuntos de ferramentas. Veremos 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 novo arquivo chamado gpio.c. Nesse novo arquivo, adicione o conteúdo a seguir.

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

AÇÃO: adicionar definições.

Elas servem como abstrações entre valores específicos de nRF52840 e variáveis usadas 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 nRF52840, consulte o central de informações de semicondutores nórdico (em inglês).

AÇÃO: Adicionar cabeçalho inclui.

Em seguida, adicione os itens 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: adicionar funções de callback e interrupção para o Botão 1.

Adicione esse código em seguida. A função in_pin1_handler é o callback registrado quando a funcionalidade de pressionamento do botão é inicializada (mais adiante neste arquivo).

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: adicionar uma função para configurar os LEDs

Adicione esse 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: adicionar uma função para definir o modo de um LED.

Essa função será usada quando a função 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: adicionar uma função para alternar o modo de um LED.

Esta 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: adicionar funções para inicializar e processar o pressionamento de botões.

A primeira função inicializa a placa quando 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: reaja a mudanças na função do dispositivo

Em nosso aplicativo, queremos que LEDs diferentes acendam dependendo da função do dispositivo. Vamos acompanhar os seguintes papéis: líder, roteador, dispositivo final. Podemos atribuí-los a LEDs desta forma:

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

Para ativar essa funcionalidade, o aplicativo precisa saber quando a função do dispositivo foi alterada e como acender o LED correto em resposta. Usaremos a instância do OpenThread para a primeira parte e a abstração da plataforma GPIO na segunda.

Abra o arquivo ./openthread/examples/apps/cli/main.c em seu editor de texto preferido.

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

AÇÃO: Adicionar cabeçalho inclui.

Na seção "Inclui" do arquivo main.c, adicione os arquivos principais da API necessários para o recurso de mudança de papel.

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

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

Adicione essa declaração a main.c, depois que o cabeçalho incluir 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: adicionar um registro de callback para a função do gerenciador de mudanças de estado

No 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 muda.

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

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

No main.c, depois da função main(), implemente a handleNetifStateChanged. Essa função verifica a flag OT_CHANGED_THREAD_ROLE da instância do OpenThread e, em caso de mudança, ativa/desativa 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

Em nosso aplicativo, também queremos enviar mensagens UDP para todos os outros dispositivos na rede quando o Botão1 é pressionado em uma placa. Para confirmar o recebimento da mensagem, vamos alternar o LED4 nas outras placas como 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 mesh-local
  • Processar mensagens UDP recebidas
  • Alternar o LED4 em resposta a mensagens UDP recebidas

Abra o arquivo ./openthread/examples/apps/cli/main.c em seu editor de texto preferido.

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

AÇÃO: Adicionar cabeçalho inclui.

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

#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 as condições de ambiente de execução e processam erros corretamente.

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

No arquivo main.c, depois da seção "Inclui" e antes de qualquer instrução #if, adicione constantes específicas de UDP e defina:

#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 mesh-local. Todas as mensagens enviadas para esse endereço serão enviadas para todos os dispositivos Thread completos na rede. Consulte Multicast em openthread.io para mais informações sobre a compatibilidade com multicast no OpenThread.

AÇÃO: adicionar 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 de 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: adicionar chamadas para inicializar os LEDs e o botão GPIO.

No main.c, adicione essas chamadas à função main() após otSetStateChangedCallback. Essas funções inicializam os pinos GPIO e GPIOTE e definem um manipulador de botões para lidar com eventos de push de botão.

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

AÇÃO: adicionar 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 após a inicialização do aplicativo. Sem isso, o dispositivo não pode enviar nem receber mensagens UDP.

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

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

otSysButtonProcess(instance);

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

No main.c, adicione a implementação da função handleButtonInterrupt após a função handleNetifStateChanged adicionada na etapa anterior:

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

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

No 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 definida 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 as mensagens UDP.

No 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. Eles garantem que a mensagem UDP seja válida e alocada corretamente no buffer. Caso contrário, a função processará os erros corretamente pulando para o bloco exit, onde ele vai liberar 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 gerenciamento de mensagens UDP

No main.c, adicione a implementação da função handleUdpReceive após a 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 nossos dispositivos iniciem o Thread imediatamente e se juntem a uma rede quando estiverem ligados. Para fazer isso, vamos usar a estrutura otOperationalDataset. Essa estrutura contém todos os parâmetros necessários para transmitir credenciais de rede Thread para 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 rede apenas àqueles que o executam.

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

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

AÇÃO: adicionar inclusão do cabeçalho.

Na seção "Inclui" 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: adicionar declaração de função para definir a configuração de rede.

Adicione essa declaração a main.c, depois que o cabeçalho incluir e antes de qualquer instrução #if. Esta função será definida após a função principal do aplicativo.

static void setNetworkConfiguration(otInstance *aInstance);

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

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

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

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

No main.c, adicione essas chamadas à função main() após 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 de rede Thread.

No 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
  • ID do PAN = 0x2222
  • ID do PAN estendido = C0DE1AB5C0DE1AB5
  • Chave de rede = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Nome da rede = OTCodelab

Além disso, é aqui que reduzimos a instabilidade do roteador. Assim, nossos dispositivos mudam de função mais rapidamente para fins de demonstração. Isso só será feito se o nó for um dispositivo FTD (Full Thread Device). 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 podem ser usadas em uma implantação de produção de um aplicativo que utilize 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 da linha de execução. Para facilitar o uso neste codelab, vamos alterá-lo para 20. Assim, você não precisa esperar muito até que um nó da linha de execução mude de papel.

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, haverá uma falha de build.

É possível confirmar isso analisando a definição da função otThreadSetRouterSelectionJitter, 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, algumas pequenas atualizações são necessárias para três arquivos CMake. Elas são usadas pelo sistema de build para compilar e vincular seu aplicativo.

./third_party/NordicSemiconductor/CMakeLists.txt

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

AÇÃO: adicione sinalizações ao arquivo CMakeLists.txt .

Abra ./third_party/NordicSemiconductor/CMakeLists.txt em seu editor de texto preferido 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 em seu editor de texto preferido 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, que vai ser incluído no build da biblioteca dos drivers nórdicos.

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

Abra ./third_party/NordicSemiconductor/CMakeLists.txt em seu editor de texto preferido e adicione o arquivo à seção COMMON_SOURCES.

...

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

11. Configurar os dispositivos

Depois de fazer todas as atualizações de código, você já pode criar e atualizar o aplicativo nas três placas de desenvolvimento nórdicas nRF52840. Cada dispositivo funcionará como um dispositivo Thread completo (FTD).

Criar OpenThread

Criar 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

Navegue até o diretório com o binário OpenThread FTD CLI e converta-o para o formato hexadecimal com o conjunto de ferramentas incorporado da ARM:

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

Piscar as placas

Atualizar o 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. Depois, conecte-o à máquina Linux. Defina corretamente o LED5, que está ligado.

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 atualize o arquivo hexadecimal FTD da OpenThread CLI na placa nRF52840, usando o número de série da placa:

$ 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 o flash. A saída a seguir será gerada após a conclusão:

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 o comando "Pule as placas" etapa para os outros dois quadros. Cada placa deve estar 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 funcionar, o LED1, o LED2 ou o LED3 vão acender em cada placa. É possível que o LED aceso mude de 3 para 2 (ou 2 para 1) logo depois de piscar (o recurso de mudança de função do dispositivo).

12. Funcionalidade do aplicativo

Todas as três placas nRF52840 agora precisam ser alimentadas e executar nosso 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ó da linha de execução:

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

O LED aceso muda conforme a função muda. Você já deve ter visto essas mudanças em uma ou duas placas dentro de 20 segundos após cada dispositivo ser ligado.

Multicast UDP

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

203dd094acca1f97.png.

9bbd96d9b1c63504.png

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

Os dispositivos atualizados são de um tipo específico de dispositivo de linha de execução completa (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 oferece suporte a até 32 roteadores, mas tenta manter o número entre 16 e 23. Se um REED for anexado como um Dispositivo Final e o número de Roteadores for menor do que 16, ele se promoverá automaticamente a um Roteador. Essa mudança ocorre em um momento aleatório dentro do número de segundos para o qual você define o valor otThreadSetRouterSelectionJitter no aplicativo (20 segundos).

Toda 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 deve ser um líder (LED1 ativado) e os outros dois devem ser roteadores (LED2 ativado).

4e1e885861a66570.png

Remover o líder

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

Desligue a placa líder (aquela com o LED1 aceso) usando o interruptor Liga/desliga. Aguarde cerca de 20 segundos. Em uma das duas placas restantes, o LED2 (Roteador) será desligado e o LED1 (Leader) acenderá. Este dispositivo agora é o líder da rede Thread.

4c57c87adb40e0e3.png

Reative o quadro de líderes original. Ele voltará automaticamente à rede Thread como um dispositivo final (LED3 está aceso). Em 20 segundos (a instabilidade da seleção do roteador), ele se promove a roteador (o LED2 fica aceso).

5f40afca2dcc4b5b.png

Redefinir as placas

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

Inicialmente, as outras duas placas se conectam à rede como dispositivos finais (LED3 está 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 a conexão de rádio entre elas for fraca, a rede Thread poderá ser dividida em partições e talvez mais de um dispositivo apareça como líder.

A linha de execução tem autocorreção, então as partições precisam ser reunidas em uma única partição com um líder.

14. Demonstração: Enviar multicast UDP

Ao continuar o exercício anterior, o LED4 não deve estar aceso em nenhum dispositivo.

Escolha qualquer tábua e pressione Botão1. O LED4 em todas as outras placas na rede Thread que executa o aplicativo precisa alternar esse estado. Ao continuar do exercício anterior, eles devem estar ativados.

f186a2618fdbe3fd.png

Pressione Botão1 para a mesma placa novamente. O LED4 em todas as outras placas deve alternar novamente.

Pressione o Botão1 em uma placa diferente e observe como o LED4 se alterna nas outras placas. Pressione o Botão1 em uma das placas em que o LED4 está ligado. O LED4 permanece aceso nessa placa, mas alterna nas outras.

f5865ccb8ab7aa34.png

Partições de rede

Se seus boards estiverem particionados e houver mais de um líder entre eles, o resultado da mensagem multicast será diferente para cada um deles. Se você pressionar o Botão1 em uma placa particionada (e, portanto, o único membro da rede Thread particionada), o LED4 nas outras placas não acenderá em resposta. Se isso acontecer, redefina as placas. O ideal é que elas reformam uma única rede Thread e as mensagens UDP devem funcionar corretamente.

15. Parabéns!

Você criou um aplicativo que usa APIs OpenThread.

Agora você sabe:

  • Como programar os botões e os LEDs nas placas de desenvolvimento nórdicas 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 internos 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 o multicast para dar um ping em todos os dispositivos pressionando um botão, use a API Router/Leader para localizar e dar um ping em um dispositivo individual
  • Conecte sua rede mesh à Internet usando um roteador de borda OpenThread e faça o multicast de fora da rede Thread para acender os LEDs

Leitura adicional

Acesse openthread.io e GitHub (links em inglês) para ver uma variedade de recursos do OpenThread, incluindo:

Referência: