O Google tem o compromisso de promover a igualdade racial para as comunidades negras. Saiba como.

Como desenvolver com as APIs OpenThread

1. Introdução

26b7f4f6b3ea0700.png

A OpenThread lançada 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 desses produtos para a casa conectada.

A especificação 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 Thread, incluindo IPv6, 6LoWPAN, IEEE 802.15.4 com segurança MAC, estabelecimento de link de malha e roteamento de malha.

Neste codelab, você usará as APIs 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 LEDs nas placas de desenvolvimento da Nordic nRF52840
  • Como usar as 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é-requisitos

Hardware:

  • 3 placas de desenvolvimento semicondutores nRF52840
  • 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 nRF5x nórdica
  • Software Segger J-Link
  • OpenThread
  • Git

A menos que indicado de outra forma, o conteúdo deste codelab é licenciado de acordo com a Licença de Atribuição 3.0 do Creative Commons. Os exemplos de código estão licenciadas de acordo com a Licença do Apache 2.0.

2. Primeiros passos

Preencher o codelab de hardware

Antes de iniciar este codelab, você precisa concluir o codelab Criar uma rede de linhas de execução com placas nRF52840 e OpenThread que:

  • Detalha todos os softwares necessários para a criação e atualização
  • Ensina como criar o OpenThread e atualizá-lo em placas Nordic nRF52840
  • Demonstra os conceitos básicos de uma rede Thread

Nenhum codelab é necessário para criar o OpenThread e atualizar os boards neste codelab, apenas instruções básicas para a atualização deles. Presume-se que você já concluiu 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 np52840 PDK (link em inglês).

a6693da3ce213856.png

Instale o software

Para criar e atualizar o OpenThread, é necessário instalar o SEGGER J-Link, as ferramentas de linha de comando nRF5x, o ARM GNU Toolchain e vários pacotes do Linux. Se você concluiu o codelab "Build a Thread Network" conforme necessário, já tem todos os componentes necessários. Caso contrário, conclua esse codelab antes de continuar para garantir que você possa criar e atualizar o OpenThread para placas de desenvolvedor nRF52840.

3. Clonar o repositório

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

Clone o repositório de exemplos Openrd 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 diversos recursos e funcionalidades do OpenThread no nível da linha de execução e da plataforma para uso nos aplicativos:

  • Controle e informações da instância do OpenThread
  • Serviços de aplicativo, como IPv6, UDP e CoAP
  • Gerenciamento de credenciais de rede, além de papéis de comissário e combinador
  • Gerenciamento do roteador de borda
  • Recursos aprimorados, como supervisão infantil e detecção de jam

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 de cabeçalho 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ê usará com frequência ao trabalhar com as APIs do OpenThread. Depois de inicializada, essa estrutura representa uma instância estática da biblioteca OpenThread e permite que o usuário faça chamadas 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 (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 quiser adicionar funções específicas da plataforma a um dos aplicativos de exemplo incluídos no OpenThread, primeiro declare-os 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. Dessa maneira, é possível usar os mesmos cabeçalhos de função em outras plataformas de exemplo.

Por exemplo, as funções GPIO que usaremos para conectar 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 (link em inglês)

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 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 em torno de variáveis não utilizadas para alguns conjuntos de ferramentas. Veremos exemplos disso mais tarde.

5. Implementar abstração GPIO da plataforma

Na etapa anterior, abordamos as declarações de função específicas da plataforma em ./openthread/examples/platforms/openthread-system.h que podem ser usadas para GPIO. Para acessar os botões e LEDs nas placas de desenvolvimento nRF52840, é preciso implementar essas funções na plataforma nRF52840. Nesse código, você 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, 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 os botões e LEDs nRF52840, consulte o Infocenter nórdico nórdico.

AÇÃO: adicionar cabeçalhos inclui.

Em seguida, adicione o cabeçalho que inclui as funcionalidades 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 para o botão 1.

Adicione o código a seguir. A função in_pin1_handler é o callback registrado quando a funcionalidade de pressionar o 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 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.

Esta função será usada quando a função do dispositivo for alterada.

/**
 * @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.

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: adicionar funções para inicializar e processar pressionamentos do botão.

A primeira função inicializa a placa para o pressionamento do 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 a mudanças de 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 assim:

  • 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 ativar o LED correto em resposta. Usaremos a instância da OpenThread na primeira parte e a abstração da plataforma GPIO para a segunda.

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

./openthread/examples/apps/cli/main.c (link em inglês)

AÇÃO: adicionar cabeçalhos inclui.

Na seção de inclusão do arquivo main.c, adicione os arquivos principais do API que você precisará para o recurso de mudança de papel.

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

AÇÃO: adicionar uma declaração de função de 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 alterações de estado.

No main.c, adicione essa função à função main() após a chamada do otAppCliInit. Esse registro de callback diz 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: adicionar a implementação de alteração de estado.

No main.c, depois da 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 ou 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 o 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, vamos alternar o LED4 nos outros quadros 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 de 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 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ão na parte superior do arquivo main.c, adicione os arquivos principais de 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 adequadamente erros.

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

No arquivo main.c, após a seção de inclusão e antes de qualquer instrução #if, adicione constantes 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 de multicast mesh local. As mensagens enviadas para esse endereço serão enviadas para todos os dispositivos com linha de execução completa na rede. Consulte Multicast em openthread.io para ver 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 do otTaskletsSignalPending e antes da função main(), adicione funções específicas UDP, assim como 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 para lidar com os eventos de push.

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

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

No 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 poderá enviar nem receber mensagens UDP.

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

No 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 o 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. 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. 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 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 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 imediatamente a linha de execução e se conectem a uma rede quando estiverem ligados. Para isso, usaremos a estrutura otOperationalDataset. Essa estrutura contém todos os parâmetros necessários para transmitir as credenciais de rede Thread para um dispositivo.

O uso dessa estrutura substituirá os padrões de rede criados no OpenThread, para tornar nosso aplicativo mais seguro e limitar os nós de Thread na nossa rede apenas aos que executam o aplicativo.

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

./openthread/examples/apps/cli/main.c (link em inglês)

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

Na seção de inclusões na parte superior do arquivo main.c, adicione o arquivo principal de API que você precisará 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. Esta função será definida depois da função principal do aplicativo.

static void setNetworkConfiguration(otInstance *aInstance);

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

No 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 de rede e a pilha Thread.

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

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

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

AÇÃO: implementar a configuração 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 da rede da linha de execução 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, é neste local que diminuímos a instabilidade da seleção de roteadores para que nossos dispositivos alterem as funções mais rapidamente para fins de demonstração. Isso só é feito se o nó for um dispositivo de linha de execução completa (FTD). Falaremos mais sobre isso na próxima etapa.

9. API: funções restritas

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

Por exemplo, a função otThreadSetRouterSelectionJitter ajusta o tempo (em segundos) necessário para que um dispositivo final se promova. 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, para que você não precise esperar muito tempo até que um nó de linha de execução mude de função.

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

Para confirmar, analise a definição da função otThreadSetRouterSelectionJitter, que faz parte 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, há algumas pequenas atualizações necessárias para três arquivos do CMake. Eles são usados 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 GPIO sejam definidas no aplicativo.

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

Abra ./third_party/NordicSemiconductor/CMakeLists.txt no 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 (link em inglês)

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 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 do driver nrfx_gpiote.c ao arquivo CMakeLists.txt do NordicSemiconductor para que ele seja incluído com o build da biblioteca dos drivers nórdicos.

AÇÃO: adicionar o driver gpio ao arquivo CMakeLists.txt nordicSemicondutor.

Abra ./third_party/NordicSemiconductor/CMakeLists.txt no 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

Com todas as atualizações de código concluídas, você está pronto para criar e atualizar o aplicativo em todas as três placas de desenvolvedor nórdica nRF52840. Cada dispositivo funcionará como um dispositivo com linha de execução completa (FTD).

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 da CLI OpenThread FTD 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

Atualizar as lousas

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 à máquina Linux. Defina corretamente o LED5.

20a3b4b480356447

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 do 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 a atualização. 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 esta etapa do Flash para as outras duas placas. 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

nrfjprog o comando de atualização.

Se for concluído, o LED1, LED2 ou LED3 será aceso em cada placa. É possível que você veja até mesmo o interruptor do LED iluminado de 3 para 2 (ou 2 para 1) logo após a atualização (o recurso de mudança de função do dispositivo).

12. Funcionalidade do app

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

Indicadores de função do dispositivo

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

  • LED1 = líder
  • LED2 = roteador
  • LED3 = Dispositivo final

Quando a função mudar, o LED aceso também mudará. Você já deve ter visto essas mudanças em uma placa ou em até 20 segundos de cada dispositivo que foi ligado.

Multicast UDP

Quando o Button1 é pressionado em um board, uma mensagem UDP é enviada ao endereço multicast mesh-local, que inclui todos os outros nós na rede Thread. Em resposta a essa mensagem, o LED4 em todas as outras placas é ativado ou desativado. O LED4 permanece ativado ou desativado em cada placa até receber outra mensagem UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

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

Os dispositivos que você atualizou é um tipo específico de dispositivo de linha de execução completa (FTD) chamado dispositivo qualificado final (REED, na sigla em inglês) do roteador. Isso significa que podem funcionar como um roteador ou dispositivo final e se promover de um dispositivo final para um roteador.

A Thread é compatível com até 32 roteadores, mas tenta manter o número de roteadores entre 16 e 23. Se um REED for anexado como um dispositivo final e a quantidade de roteadores for inferior a 16, ele será promovido automaticamente a um roteador. Essa alteração precisa ocorrer aleatoriamente no número de segundos para o qual você definiu o valor otThreadSetRouterSelectionJitter no aplicativo (20 segundos).

Cada rede Thread também tem um líder, que é um roteador responsável por gerenciar o conjunto de roteadores em uma rede Thread. Com todos os dispositivos ativados, 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á para o líder, para garantir que a rede ainda tenha um líder.

Desligue a placa líder (aquela com LED1 iluminado) usando a chave liga/desliga. Aguarde cerca de 20 segundos. Em uma das duas placas restantes, o LED2 (roteador) será desativado, e o LED1 (líder) será ativado. Este dispositivo agora é líder da rede Thread.

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b.png

Redefinir os boards

Desligue as três molduras, ligue-as novamente e observe os LEDs. A primeira placa que foi alimentada precisa começar com a função de líder (LED1 está 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 (o LED3 está aceso), mas precisam se promover para roteadores (o LED2 está aceso) em até 20 segundos.

Partições de rede

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

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

14. Demonstração: enviar multicast UDP

Ao continuar com o exercício anterior, o LED4 não deve 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 precisa alternar o estado delas. Se você continuar com o exercício anterior, isso significa que ele já está ativado.

f186a2618fdbe3fd.png

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

Pressione o Button1 em uma placa diferente e observe como o LED4 alterna nas outras placas. Pressione o Botão 1 em uma das placas em que o LED 4 está. O LED 4 permanecerá ativado para essa placa, mas ativará os outros.

f5865ccb8ab7aa34.png

Partições de rede

Caso seus boards tenham sido particionados e haja mais de um Líder entre eles, o resultado da mensagem multicast será diferente entre os boards. Se você pressionar Button1 em uma placa que é 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 boards, de preferência, que vão transformar uma única rede Thread, e a mensagem UDP funcionará corretamente.

15. Parabéns!

Você criou um aplicativo que usa APIs OpenThread.

Agora você sabe:

  • Como programar os botões e LEDs nas placas de desenvolvimento da Nordic nRF52840
  • Como usar as 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, tente os seguintes exercícios:

  • 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 no papel do roteador
  • Adicionar suporte GPIO a outra plataforma de exemplo
  • 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.
  • Conecte a rede mesh à Internet usando um roteador de borda OpenThread e faça o multicast delas de fora da rede Thread para acender os LEDs

Leitura adicional

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

Referência: