Разработка с использованием API OpenThread

1. Введение

26b7f4f6b3ea0700.png

OpenThread , выпущенный Nest, представляет собой реализацию сетевого протокола Thread® с открытым исходным кодом. Nest выпустила OpenThread, чтобы сделать технологию, используемую в продуктах Nest, широко доступной для разработчиков и ускорить разработку продуктов для подключенного дома.

Спецификация Thread определяет надежный, безопасный и маломощный протокол беспроводной связи между устройствами на основе IPv6 для домашних приложений. OpenThread реализует все сетевые уровни Thread, включая IPv6, 6LoWPAN, IEEE 802.15.4 с безопасностью MAC, созданием Mesh Link и Mesh Routing.

В этой лаборатории кода вы будете использовать API-интерфейсы OpenThread для запуска сети потоков, мониторинга и реагирования на изменения ролей устройств, а также отправки сообщений UDP, а также привязки этих действий к кнопкам и индикаторам на реальном оборудовании.

2a6db2e258c32237.png

Что вы узнаете

  • Как запрограммировать кнопки и светодиоды на платах разработчика Nordic nRF52840
  • Как использовать общие API OpenThread и класс otInstance
  • Как отслеживать и реагировать на изменения состояния OpenThread
  • Как отправлять UDP-сообщения на все устройства в сети Thread
  • Как изменить Makefile

Что вам понадобится

Аппаратное обеспечение:

  • 3 платы разработки Nordic Semiconductor nRF52840
  • 3 кабеля USB-Micro-USB для подключения плат.
  • Компьютер с Linux как минимум с 3 портами USB.

Программное обеспечение:

  • Инструментальная цепочка GNU
  • Инструменты командной строки Nordic nRF5x
  • Программное обеспечение Segger J-Link
  • OpenThread
  • Гит

Если не указано иное, содержимое этой Codelab лицензируется по лицензии Creative Commons Attribution 3.0 , а примеры кода — по лицензии Apache 2.0 .

2. Начало работы

Завершите лабораторную работу по аппаратному обеспечению

Прежде чем приступить к этой лабораторной работе, вам следует выполнить лабораторную работу «Построение сети потоков с помощью плат nRF52840 и OpenThread Codelab», которая:

  • Подробно описано все программное обеспечение, необходимое для сборки и прошивки.
  • Научит вас, как собрать OpenThread и прошить его на платах Nordic nRF52840.
  • Демонстрирует основы сети потоков.

Ни одна из настроек среды, необходимой для сборки OpenThread и прошивки плат, не описана подробно в этой лаборатории кода — только базовые инструкции по прошивке плат. Предполагается, что вы уже завершили работу над кодовой работой «Построение сети потоков».

Linux-машина

Эта Codelab была разработана для использования машины Linux на базе i386 или x86 для прошивки всех плат разработки Thread. Все шаги были протестированы на Ubuntu 14.04.5 LTS (Trusty Tahr).

Платы Nordic Semiconductor nRF52840

В этой Codelab используются три платы nRF52840 PDK .

a6693da3ce213856.png

Установить программное обеспечение

Чтобы собрать и прошить OpenThread, вам необходимо установить SEGGER J-Link, инструменты командной строки nRF5x, набор инструментов ARM GNU Toolchain и различные пакеты Linux. Если вы прошли необходимую лабораторную работу по построению сети потоков, у вас уже будет установлено все необходимое. Если нет, завершите эту Codelab, прежде чем продолжить, чтобы убедиться, что вы можете собрать и прошить OpenThread на платы разработки nRF52840.

3. Клонируйте репозиторий

OpenThread поставляется с примером кода приложения, который вы можете использовать в качестве отправной точки для этой лаборатории кода.

Клонируйте репозиторий примеров OpenThread Nordic nRF528xx и соберите OpenThread:

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

4. Основы API OpenThread

Публичные API OpenThread расположены по адресу ./openthread/include/openthread в репозитории OpenThread. Эти API обеспечивают доступ к различным функциям и функциям OpenThread как на уровне потоков, так и на уровне платформы для использования в ваших приложениях:

  • Информация об экземпляре OpenThread и управление им
  • Службы приложений, такие как IPv6, UDP и CoAP.
  • Управление сетевыми учетными данными, а также роли комиссара и присоединения.
  • Управление пограничным маршрутизатором
  • Расширенные функции, такие как надзор за детьми и обнаружение замятия

Справочная информация по всем API OpenThread доступна по адресу openthread.io/reference .

Использование API

Чтобы использовать API, включите его заголовочный файл в один из файлов вашего приложения. Затем вызовите нужную функцию.

Например, пример приложения CLI, включенный в OpenThread, использует следующие заголовки 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>

Экземпляр OpenThread

Структура otInstance — это то, что вы будете часто использовать при работе с API OpenThread. После инициализации эта структура представляет собой статический экземпляр библиотеки OpenThread и позволяет пользователю выполнять вызовы API OpenThread.

Например, экземпляр OpenThread инициализируется в функции main() примера приложения 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;
}

Функции, специфичные для платформы

Если вы хотите добавить функции, специфичные для платформы, в одно из примеров приложений, включенных в OpenThread, сначала объявите их в заголовке ./openthread/examples/platforms/openthread-system.h , используя пространство имен otSys для всех функций. Затем реализуйте их в исходном файле для конкретной платформы. Абстрагируя таким образом, вы можете использовать те же заголовки функций для других примеров платформ.

Например, функции GPIO, которые мы собираемся использовать для подключения к кнопкам и светодиодам nRF52840, должны быть объявлены в openthread-system.h .

Откройте файл ./openthread/examples/platforms/openthread-system.h в предпочитаемом вами текстовом редакторе.

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

ДЕЙСТВИЕ: Добавьте объявления функций GPIO для конкретной платформы.

Добавьте эти объявления функций после #include для заголовка 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);

Мы реализуем их на следующем этапе.

Обратите внимание, что в объявлении функции otSysButtonProcess используется otInstance . Таким образом, приложение может получить доступ к информации об экземпляре OpenThread при нажатии кнопки, если это необходимо. Все зависит от потребностей вашего приложения. Если вам это не нужно в вашей реализации функции, вы можете использовать макрос OT_UNUSED_VARIABLE из API OpenThread для подавления ошибок сборки вокруг неиспользуемых переменных для некоторых цепочек инструментов. Мы увидим примеры этого позже.

5. Реализуйте абстракцию платформы GPIO.

На предыдущем шаге мы рассмотрели объявления функций для конкретной платформы в ./openthread/examples/platforms/openthread-system.h , которые можно использовать для GPIO. Чтобы получить доступ к кнопкам и светодиодам на платах разработки nRF52840, вам необходимо реализовать эти функции для платформы nRF52840. В этом коде вы добавите функции, которые:

  • Инициализация контактов и режимов GPIO
  • Контролируйте напряжение на выводе
  • Включить прерывания GPIO и зарегистрировать обратный вызов

В каталоге ./src/src создайте новый файл с именем gpio.c В этот новый файл добавьте следующее содержимое.

./src/src/gpio.c (новый файл)

ДЕЙСТВИЕ: Добавьте определения.

Эти определения служат абстракцией между значениями, специфичными для nRF52840, и переменными, используемыми на уровне приложения 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

Дополнительную информацию о кнопках и светодиодах nRF52840 см. в информационном центре Nordic Semiconductor .

ДЕЙСТВИЕ: Добавить заголовок включает в себя.

Затем добавьте заголовок, который вам понадобится для функциональности 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"

ДЕЙСТВИЕ: Добавьте функции обратного вызова и прерывания для кнопки 1.

Добавьте этот код дальше. Функция in_pin1_handler — это обратный вызов, который регистрируется при инициализации функции нажатия кнопки (далее в этом файле).

Обратите внимание, что этот обратный вызов использует макрос OT_UNUSED_VARIABLE , поскольку переменные, переданные в in_pin1_handler фактически не используются в функции.

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

ДЕЙСТВИЕ: Добавьте функцию для настройки светодиодов.

Добавьте этот код, чтобы настроить режим и состояние всех светодиодов во время инициализации.

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

ДЕЙСТВИЕ: Добавьте функцию для установки режима светодиода.

Эта функция будет использоваться при изменении роли устройства.

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

ДЕЙСТВИЕ: Добавьте функцию переключения режима светодиода.

Эта функция будет использоваться для переключения светодиода LED4, когда устройство получает многоадресное сообщение UDP.

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

ДЕЙСТВИЕ: Добавьте функции для инициализации и обработки нажатий кнопок.

Первая функция инициализирует плату для нажатия кнопки, а вторая отправляет многоадресное UDP-сообщение при нажатии кнопки 1.

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

ДЕЙСТВИЕ: Сохраните и закройте файл gpio.c

6. API: реагирование на изменения ролей устройства.

В нашем приложении мы хотим, чтобы в зависимости от роли устройства загорались разные светодиоды. Давайте отследим следующие роли: Лидер, Маршрутизатор, Конечное устройство. Мы можем назначить их светодиодам следующим образом:

  • Светодиод 1 = Лидер
  • Светодиод 2 = Маршрутизатор
  • LED3 = Конечное устройство

Чтобы включить эту функцию, приложению необходимо знать, когда изменилась роль устройства и как в ответ включить правильный светодиод. Мы будем использовать экземпляр OpenThread для первой части и абстракцию платформы GPIO для второй.

Откройте файл ./openthread/examples/apps/cli/main.c в предпочитаемом вами текстовом редакторе.

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

ДЕЙСТВИЕ: Добавить заголовок включает в себя.

В разделе «Includes» файла main.c добавьте файлы заголовков API, которые вам понадобятся для функции смены роли.

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

ДЕЙСТВИЕ: Добавьте объявление функции-обработчика для изменения состояния экземпляра OpenThread.

Добавьте это объявление в main.c после включения заголовка и перед любыми операторами #if . Эта функция будет определена после основного приложения.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

ДЕЙСТВИЕ: Добавьте регистрацию обратного вызова для функции обработчика изменения состояния.

В main.c добавьте эту функцию в функцию main() после вызова otAppCliInit . Эта регистрация обратного вызова сообщает OpenThread вызывать функцию handleNetifStateChange при каждом изменении состояния экземпляра OpenThread.

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

ДЕЙСТВИЕ: Добавьте реализацию изменения состояния.

В main.c после функции main() реализуйте функцию handleNetifStateChanged . Эта функция проверяет флаг OT_CHANGED_THREAD_ROLE экземпляра OpenThread и, если он изменился, включает/выключает светодиоды по мере необходимости.

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: используйте многоадресную рассылку для включения светодиода.

В нашем приложении мы также хотим отправлять UDP-сообщения всем остальным устройствам в сети при нажатии кнопки Button1 на одной плате. Чтобы подтвердить получение сообщения, мы в ответ переключим светодиод 4 на других платах.

Чтобы включить эту функцию, приложению необходимо:

  • Инициализировать UDP-соединение при запуске
  • Уметь отправлять UDP-сообщение на локальный адрес многоадресной рассылки.
  • Обработка входящих UDP-сообщений
  • Переключить светодиод 4 в ответ на входящие сообщения UDP

Откройте файл ./openthread/examples/apps/cli/main.c в предпочитаемом вами текстовом редакторе.

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

ДЕЙСТВИЕ: Добавить заголовок включает в себя.

В разделе «Включения» в верхней части файла main.c добавьте файлы заголовков API, которые вам понадобятся для функции многоадресной рассылки UDP.

#include <string.h>

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

#include "utils/code_utils.h"

Заголовок code_utils.h используется для макросов otEXPECT и otEXPECT_ACTION , которые проверяют условия времени выполнения и корректно обрабатывают ошибки.

ДЕЙСТВИЕ: Добавьте определения и константы:

В файле main.c после раздела include и перед любыми операторами #if добавьте константы, специфичные для UDP, и определите:

#define UDP_PORT 1212

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

ff03::1 — это локальный адрес многоадресной рассылки. Любые сообщения, отправленные на этот адрес, будут отправлены на все полнопоточные устройства в сети. См. раздел «Многоадресная рассылка» на openthread.io для получения дополнительной информации о поддержке многоадресной рассылки в OpenThread.

ДЕЙСТВИЕ: Добавьте объявления функций.

В файле main.c после определения otTaskletsSignalPending и перед функцией main() добавьте функции, специфичные для UDP, а также статическую переменную для представления сокета 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;

ДЕЙСТВИЕ: Добавьте вызовы для инициализации светодиодов и кнопки GPIO.

В main.c добавьте эти вызовы функций в функцию main() после вызова otSetStateChangedCallback . Эти функции инициализируют контакты GPIO и GPIOTE и устанавливают обработчик кнопок для обработки событий нажатия кнопок.

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

ДЕЙСТВИЕ: Добавьте вызов инициализации UDP.

В main.c добавьте эту функцию в функцию main() после только что добавленного вызова otSysButtonInit :

initUdp(instance);

Этот вызов гарантирует инициализацию сокета UDP при запуске приложения. Без этого устройство не сможет отправлять или получать сообщения UDP.

ДЕЙСТВИЕ: Добавьте вызов для обработки события кнопки GPIO.

В main.c добавьте этот вызов функции в функцию main() после вызова otSysProcessDrivers в цикле while . Эта функция, объявленная в gpio.c , проверяет, была ли нажата кнопка, и если да, вызывает обработчик ( handleButtonInterrupt ), который был установлен на предыдущем шаге.

otSysButtonProcess(instance);

ДЕЙСТВИЕ: Внедрить обработчик прерывания кнопки.

В main.c добавьте реализацию функции handleButtonInterrupt после функции handleNetifStateChanged , которую вы добавили на предыдущем шаге:

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

ДЕЙСТВИЕ: Реализуйте инициализацию UDP.

В main.c добавьте реализацию функции initUdp после только что добавленной функции handleButtonInterrupt :

/**
 * 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 — это порт, который вы определили ранее (1212). Функция otUdpOpen открывает сокет и регистрирует функцию обратного вызова ( handleUdpReceive ) на случай получения UDP-сообщения. otUdpBind привязывает сокет к сетевому интерфейсу Thread, передавая OT_NETIF_THREAD . Другие параметры сетевого интерфейса см. в перечислении otNetifIdentifier в справочнике по UDP API .

ДЕЙСТВИЕ: Внедрите обмен сообщениями UDP.

В main.c добавьте реализацию функции sendUdp после только что добавленной функции initUdp :

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

Обратите внимание на макросы otEXPECT и otEXPECT_ACTION . Они гарантируют, что UDP-сообщение допустимо и правильно размещено в буфере, а если нет, функция корректно обрабатывает ошибки, переходя к exit блоку, где освобождает буфер.

Дополнительную информацию о функциях, используемых для инициализации UDP, см. в справочниках по IPv6 и UDP на сайте openthread.io.

ДЕЙСТВИЕ: Реализовать обработку сообщений UDP.

В main.c добавьте реализацию функции handleUdpReceive после только что добавленной функции sendUdp . Эта функция просто переключает 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: настройка сети потоков

Для простоты демонстрации мы хотим, чтобы наши устройства немедленно запускали Thread и объединялись в сеть при включении питания. Для этого мы воспользуемся структурой otOperationalDataset . Эта структура содержит все параметры, необходимые для передачи учетных данных сети Thread на устройство.

Использование этой структуры переопределит сетевые настройки по умолчанию, встроенные в OpenThread, чтобы сделать наше приложение более безопасным и ограничить количество узлов Thread в нашей сети только теми, на которых выполняется приложение.

Снова откройте файл ./openthread/examples/apps/cli/main.c в предпочитаемом вами текстовом редакторе.

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

ДЕЙСТВИЕ: Добавьте заголовок.

В разделе «Includes» в верхней части файла main.c добавьте файл заголовка API, который вам понадобится для настройки сети потоков:

#include <openthread/dataset_ftd.h>

ДЕЙСТВИЕ: Добавьте объявление функции для настройки конфигурации сети.

Добавьте это объявление в main.c после включения заголовка и перед любыми операторами #if . Эта функция будет определена после основной функции приложения.

static void setNetworkConfiguration(otInstance *aInstance);

ДЕЙСТВИЕ: Добавьте вызов конфигурации сети.

В main.c добавьте этот вызов функции в функцию main() после вызова otSetStateChangedCallback . Эта функция настраивает набор сетевых данных Thread.

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

ДЕЙСТВИЕ: Добавьте вызовы для включения сетевого интерфейса и стека Thread.

В main.c добавьте эти вызовы функций в функцию main() после вызова otSysButtonInit .

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

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

ДЕЙСТВИЕ: Реализуйте настройку сети потоков.

В main.c добавьте реализацию функции setNetworkConfiguration после функции 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);
}

Как подробно описано в функции, параметры сети Thread, которые мы используем для этого приложения:

  • Канал = 15
  • ИДЕНТИФИКАТОР ПАН = 0x2222
  • Расширенный PAN ID = C0DE1AB5C0DE1AB5
  • Сетевой ключ = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Имя сети = OTCodelab

Кроме того, здесь мы уменьшаем джиттер выбора маршрутизатора, чтобы наши устройства быстрее меняли роли в демонстрационных целях. Обратите внимание, что это делается только в том случае, если узел является FTD (Full Thread Device). Подробнее об этом в следующем шаге.

9. API: ограниченные функции

Некоторые из API OpenThread изменяют настройки, которые следует изменять только в демонстрационных целях или целях тестирования. Эти API не следует использовать при рабочем развертывании приложения, использующего OpenThread.

Например, функция otThreadSetRouterSelectionJitter регулирует время (в секундах), которое требуется конечному устройству для повышения статуса до маршрутизатора. По умолчанию для этого значения установлено значение 120 в соответствии со спецификацией резьбы. Для простоты использования в этой Codelab мы изменим его на 20, чтобы вам не пришлось долго ждать, пока узел Thread поменяет роли.

Примечание. Устройства MTD не становятся маршрутизаторами, а поддержка такой функции, как otThreadSetRouterSelectionJitter , не включена в сборку MTD. Позже нам нужно указать опцию CMake -DOT_MTD=OFF , иначе мы столкнемся с ошибкой сборки.

Вы можете убедиться в этом, посмотрев определение функции otThreadSetRouterSelectionJitter , которое содержится в директиве препроцессора 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. Обновления CMake

Прежде чем создавать приложение, необходимо внести несколько незначительных обновлений в три файла CMake. Они используются системой сборки для компиляции и компоновки вашего приложения.

./ Third_party/NordicSemiconductor/CMakeLists.txt

Теперь добавьте несколько флагов в файл NordicSemiconductor CMakeLists.txt , чтобы гарантировать, что функции GPIO определены в приложении.

ДЕЙСТВИЕ: Добавьте флаги в файл CMakeLists.txt .

Откройте ./third_party/NordicSemiconductor/CMakeLists.txt NordicSemiconductor/CMakeLists.txt в предпочитаемом вами текстовом редакторе и добавьте следующие строки в раздел 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

Отредактируйте файл ./src/CMakeLists.txt , добавив новый исходный файл gpio.c :

ДЕЙСТВИЕ: Добавьте источник gpio в файл ./src/CMakeLists.txt .

Откройте ./src/CMakeLists.txt в предпочитаемом вами текстовом редакторе и добавьте файл в раздел NRF_COMM_SOURCES .

...

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

...

./ Third_party/NordicSemiconductor/CMakeLists.txt

Наконец, добавьте файл драйвера nrfx_gpiote.c в файл NordicSemiconductor CMakeLists.txt , чтобы он был включен в сборку библиотеки драйверов Nordic.

ДЕЙСТВИЕ: Добавьте драйвер gpio в файл NordicSemiconductor CMakeLists.txt .

Откройте ./third_party/NordicSemiconductor/CMakeLists.txt NordicSemiconductor/CMakeLists.txt в предпочитаемом вами текстовом редакторе и добавьте файл в раздел COMMON_SOURCES .

...

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

11. Настройте устройства

После завершения всех обновлений кода вы готовы собрать и установить приложение на все три платы разработчика Nordic nRF52840. Каждое устройство будет функционировать как полнопоточное устройство (FTD).

Сборка OpenThread

Создайте двоичные файлы OpenThread FTD для платформы nRF52840.

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

Перейдите в каталог с двоичным файлом CLI OpenThread FTD и преобразуйте его в шестнадцатеричный формат с помощью встроенной цепочки инструментов ARM:

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

Прошить доски

Запишите файл ot-cli-ftd.hex на каждую плату nRF52840.

Подключите USB-кабель к порту отладки Micro-USB рядом с контактом внешнего питания на плате nRF52840, а затем подключите его к компьютеру с Linux. Установите правильно, светодиод 5 горит.

20a3b4b480356447.png

Как и раньше, обратите внимание на серийный номер платы nRF52840:

c00d519ebec7e5f0.jpeg

Перейдите к расположению инструментов командной строки nRFx и запишите шестнадцатеричный файл OpenThread CLI FTD на плату nRF52840, используя серийный номер платы:

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

LED5 ненадолго погаснет во время мигания. В случае успеха генерируется следующий вывод:

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.

Повторите этот шаг «Прошивка плат» для двух других плат. Каждая плата должна быть подключена к машине Linux одинаково, и команда для прошивки та же, за исключением серийного номера платы. Обязательно используйте уникальный серийный номер каждой платы в

Команда прошивки nrfjprog .

В случае успеха на каждой плате загорится светодиод LED1, LED2 или LED3. Вы можете даже увидеть горящий светодиодный переключатель с 3 на 2 (или с 2 на 1) вскоре после мигания (функция смены роли устройства).

12. Функционал приложения

Теперь все три платы nRF52840 должны быть включены и работать с нашим приложением OpenThread. Как подробно говорилось ранее, это приложение имеет две основные функции.

Индикаторы роли устройства

Горящий светодиод на каждой плате отражает текущую роль узла Thread:

  • Светодиод 1 = Лидер
  • Светодиод 2 = Маршрутизатор
  • LED3 = Конечное устройство

По мере изменения роли меняется и горящий светодиод. Вы уже должны были увидеть эти изменения на одной или двух платах в течение 20 секунд после включения каждого устройства.

UDP-многоадресная рассылка

Когда Button1 нажата на плате, UDP-сообщение отправляется на локальный адрес многоадресной рассылки, который включает в себя все остальные узлы в сети Thread. В ответ на получение этого сообщения светодиод 4 на всех остальных платах включается или выключается . LED4 остается включенным или выключенным для каждой платы, пока она не получит другое сообщение UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Демонстрация: наблюдайте за изменением ролей устройства.

Устройства, которые вы прошили, представляют собой особый тип полнопоточного устройства (FTD), называемого конечным устройством, подходящим для маршрутизатора (REED). Это означает, что они могут функционировать как маршрутизатор или конечное устройство и могут превращаться из конечного устройства в маршрутизатор.

Поток может поддерживать до 32 маршрутизаторов, но старается поддерживать количество маршрутизаторов в диапазоне от 16 до 23. Если REED подключается как конечное устройство и количество маршрутизаторов меньше 16, он автоматически повышает себя до уровня маршрутизатора. Это изменение должно происходить в случайное время в течение того количества секунд, которое вы установили для значения otThreadSetRouterSelectionJitter в приложении (20 секунд).

В каждой сети потоков также есть лидер, который является маршрутизатором, отвечающим за управление набором маршрутизаторов в сети потоков. Когда все устройства будут включены, через 20 секунд одно из них должно стать ведущим (светодиод 1 горит), а два других должны стать маршрутизаторами (горит светодиод 2).

4e1e885861a66570.png

Удалить лидера

Если лидер удаляется из сети потока, другой маршрутизатор становится лидером, чтобы гарантировать, что в сети все еще есть лидер.

Выключите доску лидеров (та, у которой горит светодиод 1) с помощью выключателя питания . Подождите около 20 секунд. На одной из оставшихся двух плат светодиод LED2 (маршрутизатор) погаснет, а светодиод LED1 (лидер) включится. Это устройство теперь является Лидером сети Thread.

4c57c87adb40e0e3.png

Снова включите исходную таблицу лидеров. Он должен автоматически присоединиться к сети Thread в качестве конечного устройства (светодиод 3 горит). В течение 20 секунд (дрожание выбора маршрутизатора) он переходит в режим маршрутизатора (светодиод 2 горит).

5f40afca2dcc4b5b.png

Сброс досок

Выключите все три платы, затем снова включите их и наблюдайте за светодиодами. Первая включенная плата должна начать играть роль лидера (светодиод 1 горит) — первый маршрутизатор в сети Thread автоматически становится лидером.

Две другие платы первоначально подключаются к сети как конечные устройства (светодиод 3 горит), но должны стать маршрутизаторами (светодиод 2 горит) в течение 20 секунд.

Сетевые разделы

Если ваши платы не получают достаточного питания или радиосвязь между ними слабая, сеть Thread может разделиться на разделы, и у вас может быть более одного устройства, отображаемого как лидер.

Поток является самовосстанавливающимся, поэтому разделы в конечном итоге должны снова объединиться в один раздел с одним лидером.

14. Демо: отправка многоадресной рассылки UDP

Если продолжить предыдущее упражнение, светодиод 4 не должен гореть ни на одном устройстве.

Выберите любую доску и нажмите кнопку Button1. LED4 на всех других платах в сети Thread, на которых работает приложение, должен переключать свое состояние. Если продолжить предыдущее упражнение, теперь они должны быть включены.

f186a2618fdbe3fd.png

Нажмите кнопку Button1 для той же платы еще раз. LED4 на всех остальных платах должен снова переключиться.

Нажмите кнопку Button1 на другой плате и посмотрите, как переключается светодиод LED4 на других платах. Нажмите кнопку Button1 на одной из плат, где в данный момент горит светодиод 4. Светодиод 4 остается включенным для этой платы, но включается для остальных.

f5865ccb8ab7aa34.png

Сетевые разделы

Если ваши доски разделены и среди них более одного Лидера, результат многоадресного сообщения будет отличаться на разных досках. Если вы нажмете кнопку 1 на разделенной на разделы плате (и, таким образом, являющейся единственным членом разделенной сети потоков), светодиод 4 на других платах не загорится в ответ. Если это произойдет, перезагрузите платы — в идеале они преобразуют однопоточную сеть, и обмен сообщениями UDP должен работать правильно.

15. Поздравляем!

Вы создали приложение, использующее API OpenThread!

Теперь вы знаете:

  • Как запрограммировать кнопки и светодиоды на платах разработчика Nordic nRF52840
  • Как использовать общие API OpenThread и класс otInstance
  • Как отслеживать и реагировать на изменения состояния OpenThread
  • Как отправлять UDP-сообщения на все устройства в сети Thread
  • Как изменить Makefile

Следующие шаги

Опираясь на эту Codelab, попробуйте выполнить следующие упражнения:

  • Измените модуль GPIO, чтобы использовать контакты GPIO вместо встроенных светодиодов, и подключите внешние светодиоды RGB, которые меняют цвет в зависимости от роли маршрутизатора.
  • Добавьте поддержку GPIO для другого примера платформы.
  • Вместо использования многоадресной рассылки для проверки связи всех устройств одним нажатием кнопки используйте API Router/Leader для поиска и проверки связи отдельного устройства.
  • Подключите ячеистую сеть к Интернету с помощью пограничного маршрутизатора OpenThread и выполните многоадресную рассылку из-за пределов сети Thread, чтобы загорелись светодиоды.

Дальнейшее чтение

Посетите openthread.io и GitHub , чтобы найти различные ресурсы OpenThread, в том числе:

Ссылка: