Como desenvolver com as 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 em produtos Nest para acelerar o desenvolvimento de produtos para a casa conectada.

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

Neste codelab, você usará APIs do OpenThread para iniciar uma rede Thread, monitorar e reagir a mudanças em funções de dispositivos e enviar mensagens UDP, além de 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 desenvolvedor nórdica nRF52840
  • Como usar APIs comuns do OpenThread e a classe otInstance
  • Como monitorar e reagir às mudanças de estado do OpenThread
  • Como enviar mensagens UDP para todos os dispositivos em uma rede Thread
  • Como modificar Makefiles

Pré-requisitos

Hardware:

  • 3 placas de desenvolvedor Nordic Semiconductor nRF52840
  • 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 GNU
  • Ferramentas de linha de comando nórdica nRF5x
  • Software Selink J-Segger
  • OpenThread
  • Git

Exceto quando indicado o contrário, o conteúdo deste codelab está licenciado sob a Licença Creative Commons Attribution 3.0 e as amostras de código estão licenciadas sob a Licença Apache 2.0.

2. Como começar

Concluir o codelab de hardware

Antes de iniciar este codelab, você precisa concluir o codelab Build a Thread Network with nRF52840 Boards e OpenThread, que:

  • Detalha todos os softwares necessários para criar e atualizar
  • Ensina como criar o OpenThread e atualizá-lo 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á 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 Nordic Semiconductor nRF52840

Este codelab usa três placas PDK nRF52840 (link em inglês).

a6693da3ce213856.png

Instale o software

Para criar e atualizar o OpenThread, é necessário instalar o SEGGER J-Link, as ferramentas da 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", terá tudo o que precisa. 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 inclui 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 uma variedade de recursos e funcionalidades do OpenThread em nível de Thread e de plataforma para uso em seus aplicativos:

  • Informações e controle da instância do OpenThread
  • Serviços de aplicativos, como IPv6, UDP e CoAP
  • Gerenciamento de credenciais de rede, juntamente com as funções "Comissário" e "Combinador"
  • Gerenciamento do 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 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 (link em inglês)

#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 do OpenThread e permite que o usuário faça chamadas à 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 (link em inglês)

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, declare-os primeiro 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ídos dessa maneira, 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 vincular os botões e LEDs 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: adicionar declarações de função GPIO específicas da plataforma.

Adicione estas declarações de função após #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.

A declaração da função otSysButtonProcess usa um otInstance. Dessa forma, o aplicativo poderá acessar informações sobre a instância do OpenThread quando um botão for 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 usadas em alguns conjuntos de ferramentas. Veremos exemplos disso mais tarde.

5. Implementar a abstração GPIO da plataforma

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, você precisa implementar essas funções para a plataforma nRF52840. Neste código, você adicionará funções que:

  • Inicializar pinos e modos de GPIO
  • Controlar a tensão em um pino
  • Ativar as interrupções do 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 define.

Essas definições 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 Nordic Semiconductor Infocenter.

AÇÃO: adicionar cabeçalhos inclui.

Em seguida, adicione os cabeçalhos que serão necessários para a funcionalidade do 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 ao botão 1

Adicione este código em seguida. A função in_pin1_handler é o callback registrado quando a função 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: adicionar 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: 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: Adicione 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 pressionamentos de botão.

A primeira função inicializa a placa para pressionar um botão 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: salvar e fechar o arquivo gpio.c .

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

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

  • 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 ativar o LED correto em resposta. Usaremos a instância do OpenThread para a primeira parte e a abstração GPIO da plataforma 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 (link em inglês)

AÇÃO: adicionar cabeçalhos inclui.

Na seção de inclusões 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: adicionar declaração de 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 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ça de estado.

No main.c, adicione essa função à função main() após a chamada de 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: adicionar a implementação da mudança de estado.

No main.c, após a função main(), implemente a função handleNetifStateChanged. Essa função verifica a sinalização OT_CHANGED_THREAD_ROLE da instância do OpenThread e, se ela tiver mudado, 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 ligar um LED

Em 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, ativaremos o LED 4 nas outras placas em resposta.

Para ativar essa funcionalidade, o aplicativo precisa:

  • Inicializar uma conexão UDP durante a inicialização
  • Ser capaz de enviar uma mensagem UDP para o endereço multicast local da malha
  • Processar mensagens UDP recebidas
  • Alternar 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 (link em inglês)

AÇÃO: adicionar cabeçalhos inclui.

Na seção de inclusões na parte superior do arquivo main.c, adicione os arquivos principais 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 as condições de tempo de execução e processam os erros de maneira prática.

AÇÃO: adicionar define e constantes:

No arquivo main.c, depois da seção "includes" 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 local da malha. Todas as mensagens enviadas para esse endereço serão enviadas para todos os dispositivos de Thread completo na rede. Consulte Multicast em openthread.io (link em inglês) para ver mais informações sobre suporte a multicast no OpenThread.

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

No arquivo main.c, após a 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: adicionar 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 lidar com eventos 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 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 gerenciador (handleButtonInterrupt), que foi definido na etapa acima.

otSysButtonProcess(instance);

AÇÃO: implementar gerenciador de interrupção do 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 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 definida anteriormente (1212). A função otUdpOpen abre o soquete e registra uma função de callback (handleUdpReceive) para quando uma mensagem UDP for recebida. O otUdpBind vincula o soquete à interface de rede da linha de execução 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 mensagens 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. Isso garante que a mensagem UDP seja válida e alocada corretamente no buffer. Caso contrário, a função processará os erros pulando para o bloco exit, onde libera o buffer.

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

AÇÃO: implementar o gerenciamento 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 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, nossos dispositivos precisam iniciar o Thread imediatamente e se unir a uma rede quando são ligados. Para fazer isso, usaremos a estrutura otOperationalDataset. Essa estrutura contém todos os parâmetros necessários para transmitir as credenciais da rede Thread para um dispositivo.

O uso dessa estrutura substituirá os padrões de rede incorporados no OpenThread, para tornar nosso aplicativo mais seguro e limitar os nós de Thread em nossa rede apenas àqueles 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 (link em inglês)

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

Na seção "includes" na parte superior 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 uma declaração de função para definir a configuração de rede.

Adicione esta 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 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 a chamada 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 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: implementar a configuração de 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
  • ID do PAN = 0x2222
  • ID estendido do PAN = C0DE1AB5C0DE1AB5
  • Chave de rede = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Nome da rede = OTCodelab

Além disso, é aqui que diminuímos a instabilidade do roteador para que os dispositivos mudem de função mais rapidamente para fins de demonstração. Observe que isso só é feito se o nó for um FTD (Full Thread Device). Saiba mais sobre isso na próxima etapa.

9. API: funções restritas

Algumas 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) necessário para que um dispositivo final se promova para um roteador. O padrão para esse valor é 120, de acordo com a especificação do thread. Para facilitar o uso neste codelab, vamos mudá-lo para 20, para que você não precise esperar muito para que um nó do Thread mude de papel.

Observação: os dispositivos MTD não se tornam roteadores, e o suporte a funções 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, encontraremos uma falha de build.

Para confirmar, observe a definição da função otThreadSetRouterSelectionJitter, que está dentro de uma diretiva de pré-processador de OPENTHREAD_FTD:

./openthread/src/core/api/thread_ftd_api.cpp (link em inglês)

#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, são necessárias algumas pequenas atualizações para três arquivos do CMake. Elas são usadas pelo sistema de compilação para compilar e vincular seu aplicativo.

./third_party/NordicSemiconductor/CMakeLists.txt

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

AÇÃO: adicionar sinalizações ao arquivo CMakeLists.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: adicionar a fonte 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 do NordicSemiconductor para que ele seja incluído na compilação da biblioteca dos drivers nórdicos.

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

Depois de fazer todas as atualizações de código, você estará pronto para criar e atualizar o aplicativo para as três placas de desenvolvimento Nordic nRF52840. Cada dispositivo funcionará como um dispositivo de linha de execução completa (FTD, na sigla em inglês).

Criar 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

Navegue até o diretório com o binário CLI do OpenThread FTD e converta-o para o formato hexadecimal com a cadeia de ferramentas incorporada ARM:

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

Atualizar os tabuleiros

Atualize o arquivo ot-cli-ftd.hex para cada placa nRF52840.

Conecte o cabo USB à porta de depuração micro USB ao lado do pino externo da placa nRF52840 e conecte-o à sua máquina Linux. Defina corretamente o LED5.

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 CLI do OpenThread 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á brevemente desligado durante a atualização flash. A saída a seguir é 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 essa etapa das outras duas placas. Cada placa deve ser conectada à máquina Linux da mesma maneira, e o comando para atualizar é o mesmo, exceto para o número de série da placa. Use o número de série exclusivo de cada placa no

nrfjprog comando de atualização.

Se o procedimento for bem-sucedido, o LED1, LED2 ou LED3 será aceso em cada placa. Você pode até ver o LED aceso de três para dois (ou dois para um) logo depois de atualizar (o recurso de alteração de função do dispositivo).

12. Funcionalidade do aplicativo

Todas as três placas nRF52840 devem estar ligadas e executando o aplicativo OpenThread. Conforme detalhado anteriormente, esse aplicativo tem dois recursos principais.

Indicadores de função do dispositivo

O LED aceso em cada placa reflete o papel atual do nó da linha de execução:

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

Conforme o papel muda, o LED aceso também muda. Você já deve ter visto essas alterações em uma ou duas placas até 20 segundos após o carregamento de cada dispositivo.

Multicast UDP

Quando o Button1 é pressionado em um board, uma mensagem UDP é enviada para o endereço multicast local da malha, que inclui todos os outros nós na rede Thread. Em resposta a essa mensagem, o LED4 é ativado ou desativado em todas as outras placas. O LED4 permanece ativado ou desativado 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 um tipo específico de dispositivo de linha de execução completa (FTD, na sigla em inglês) chamado dispositivo qualificado de roteador (REED, na sigla em inglês). Isso significa que eles podem funcionar como roteadores ou dispositivos finais e se promover de dispositivos finais a roteadores.

O Thread é compatível com 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 se promoverá automaticamente como roteador. Essa mudança ocorre em um período aleatório dentro do número de segundos para o qual você definiu o valor otThreadSetRouterSelectionJitter no aplicativo (20 segundos).

Toda rede Thread também tem um líder, que é 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 ativados).

4e1e885861a66570.png

Remover o líder

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

Desative o quadro Líder (com LED 1) usando a chave liga/desliga. Aguarde cerca de 20 segundos. Em uma das duas placas restantes, o LED2 (Roteador) será desligado e o LED1 (Leader) será ativado. Este dispositivo agora é o líder da rede Thread.

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b.png

Redefinir as placas

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

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

Partições de rede

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

A linha de execução está se recuperando, por isso as partições precisam se mesclar em uma única partição com um líder.

14. Demonstração: enviar multicast UDP

Se você continuar o exercício anterior, o LED 4 não deverá ser aceso em nenhum dispositivo.

Escolha qualquer board e pressione Button1. O LED4 em todas as outras placas na rede Thread que executam o aplicativo deve alternar o estado. Se continuarem com o exercício anterior, eles continuarão ativados.

f186a2618fdbe3fd.png

Pressione Button1 para o mesmo tabuleiro novamente. O LED 4 em todas as outras placas deve ser alternado novamente.

Pressione Button1 em uma placa diferente e observe como o LED 4 alterna as outras placas. Pressione o botão 1 em uma das placas em que o LED 4 está ativado. O LED 4 permanece ativado para esse board, mas alterna com os outros.

f5865ccb8ab7aa34.png

Partições de rede

Se seus quadros estiverem particionados e houver mais de um Líder entre eles, o resultado da mensagem multicast será diferente entre os quadros. Se você pressionar o Button1 em uma placa que foi particionada (e, portanto, é o único membro da rede Thread particionada), o LED4 nas outras placas não acenderá em resposta. Se isso acontecer, redefina os painéis. O ideal é que eles reformulem uma única rede Thread e as mensagens UDP funcionem 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 desenvolvedor nórdica nRF52840
  • Como usar APIs comuns do OpenThread e a classe otInstance
  • Como monitorar e reagir às 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 seguintes exercícios:

  • Modifique o módulo GPIO para usar pinos GPIO em vez de LEDs integrados e conectar LEDs RGB externos que mudam de cor com base na função de 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 ao pressionar 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 do OpenThread e faça o multicast deles de fora da rede Thread para acender os LEDs

Leia mais

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

Referência: