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

1. Введение

26b7f4f6b3ea0700.png

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

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

В этом Codelab вы будете использовать 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 License , а образцы кода лицензируются под лицензией Apache 2.0 .

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

Пройдите аппаратную лабораторию кода

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

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

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

Linux-машина

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

Платы Nordic Semiconductor nRF52840

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

a6693da3ce213856.png

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

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

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

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

Клонируйте репозиторий примеров 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/примеры/приложения/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/примеры/приложения/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/примеры/платформы/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 из OpenThread API, чтобы подавить ошибки сборки вокруг неиспользуемых переменных для некоторых цепочек инструментов. Мы увидим примеры этого позже.

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/примеры/приложения/cli/main.c

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

В разделе include файла 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 на одной плате. Чтобы подтвердить получение сообщения, мы переключим LED4 на других платах в ответ.

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

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

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

./openthread/примеры/приложения/cli/main.c

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

В разделе include в верхней части файла 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, чтобы сделать наше приложение более безопасным и ограничить узлы потоков в нашей сети только теми, на которых запущено приложение.

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

./openthread/примеры/приложения/cli/main.c

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

В разделе include в верхней части файла 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);

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

В 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
  • Идентификатор карты = 0x2222
  • Расширенный идентификатор PAN = C0DE1AB5C0DE1AB5
  • Сетевой ключ = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Сетевое имя = OTCodelab

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

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

Теперь добавьте несколько флагов в 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 Embedded:

$ 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 на плату 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. Демонстрация: наблюдайте за изменениями роли устройства

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

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

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

4e1e885861a66570.png

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

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

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

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b.png

Сбросить доски

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

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

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

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

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

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

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

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

f186a2618fdbe3fd.png

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

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

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

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

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

Ссылка: