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

1. Введение

26b7f4f6b3ea0700.png

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

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

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

2a6db2e258c32237.png

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

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

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

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

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

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

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

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

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

Выполните лабораторную работу по аппаратному обеспечению.

Перед началом этого практического занятия вам следует выполнить практическое занятие по созданию сети потоков с использованием плат nRF52840 и OpenThread , которое включает в себя:

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

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

Linux-машина

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

Платы Nordic Semiconductor nRF52840

В этом практическом занятии используются три платы nRF52840 PDK .

a6693da3ce213856.png

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

Для сборки и прошивки OpenThread вам необходимо установить SEGGER J-Link, инструменты командной строки nRF5x, набор инструментов ARM GNU и различные пакеты Linux. Если вы выполнили необходимые действия в рамках практического занятия по сборке сети Thread, у вас уже должно быть установлено все необходимое. В противном случае, выполните это занятие перед продолжением, чтобы убедиться, что вы можете собрать и прошить 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 по адресу ./openthread/include/openthread . Эти API предоставляют доступ к различным функциям и возможностям OpenThread как на уровне потоков, так и на уровне платформы для использования в ваших приложениях:

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

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

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

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

Например, в примере приложения командной строки, входящем в состав 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: Реагирование на изменения роли устройства.

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

  • LED1 = Лидер
  • LED2 = Маршрутизатор
  • 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-сообщения всем остальным устройствам в сети при нажатии кнопки 1 на одной из плат. Для подтверждения получения сообщения мы будем в ответ переключать светодиод 4 на других платах.

Для включения этой функциональности приложению необходимо:

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

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

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

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

В разделе includes в верхней части файла 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 , после раздела includes и перед любыми операторами #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 см. в разделе «Многоадресная рассылка» на openthread.io .

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

В файле 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

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

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

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

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

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

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

#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);
}

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

  • Канал = 15
  • Идентификатор PAN = 0x2222
  • Расширенный идентификатор PAN = C0DE1AB5C0DE1AB5
  • Сетевой ключ = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Название сети = OTCodelab

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

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

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

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

Примечание: устройства 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

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

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

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

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

Откройте файл ./third_party/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

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

$ 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. При правильной настройке светодиод LED5 загорится.

20a3b4b480356447.png

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

c00d519ebec7e5f0.jpeg

Перейдите в папку, где находятся инструменты командной строки nRFx, и прошейте файл OpenThread CLI FTD в формате hex на плату 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:

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

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

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

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

203dd094acca1f97.png

9bbd96d9b1c63504.png

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

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

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

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

4e1e885861a66570.png

Сместить лидера

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

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

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b.png

Перезагрузите доски

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

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

Разделы сети

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

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

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

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

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

f186a2618fdbe3fd.png

Нажмите кнопку 1 еще раз на той же плате. Светодиод 4 на всех остальных платах должен снова загореться.

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

f5865ccb8ab7aa34.png

Разделы сети

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

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

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

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

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

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

В продолжение этого урока по программированию попробуйте выполнить следующие упражнения:

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

Дополнительная информация

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

Ссылка: