使用 OpenThread API 进行开发

1. 简介

26b7f4f6b3ea0700.png

Nest 发布的 OpenThreadThread® 网络协议的开源实现。Nest 发布了 OpenThread,供开发者广泛使用 Nest 产品中使用的技术,以加快智能家居产品开发速度。

线程规范为家庭应用定义了基于 IPv6 的可靠、安全、低功耗无线设备到设备通信协议。OpenThread 实现了所有 Thread 网络层,包括 IPv6、6LoWPAN、IEEE 802.15.4,以及 MAC 安全机制、网格链路建立服务和网格路由。

在此 Codelab 中,您将使用 OpenThread API 启动 Thread 网络,监控设备角色的变化并做出响应,发送 UDP 消息,以及将这些操作与真实硬件上的按钮和 LED 关联。

2a6db2e258c32237.png

学习内容

  • 如何对北欧 nRF52840 开发板上的按钮和 LED 进行编程
  • 如何使用常见的 OpenThread API 和 otInstance
  • 如何监控和应对 OpenThread 状态变化
  • 如何向 Thread 网络中的所有设备发送 UDP 消息
  • 如何修改 Makefile

所需条件

硬件:

  • 3 款北欧半导体 nRF52840 开发板
  • 3 根 USB 转 Micro USB 线缆,用于连接开发板
  • 具有至少 3 个 USB 端口的 Linux 机器

软件:

  • GNU 工具链
  • Nordic nRF5x 命令行工具
  • Segger J-Link 软件
  • OpenThread
  • Git

此 Codelab 的内容已获得知识共享署名 3.0 许可许可,代码示例已获得 Apache 2.0 许可,另有说明的情况除外。

2. 使用入门

完成硬件 Codelab

在开始此 Codelab 之前,您应该先完成使用 nRF52840 Boards 和 OpenThread 构建线程网络 Codelab,该 Codelab 会:

  • 详细说明构建和刷写所需的所有软件
  • 教您如何构建 OpenThread 并将其刷写到北欧 nRF52840 开发板上
  • 演示 Thread 网络的基础知识

本 Codelab 不会详细列出构建 OpenThread 和刷写开发板所需的环境,仅提供刷写开发板的基本说明。假定您已完成“构建线程网络”Codelab。

Linux 机器

此 Codelab 旨在使用基于 i386 或 x86 的 Linux 机器刷写所有 Thread 开发板。所有步骤已在 Ubuntu 14.04.5 LTS (Trusty Tahr) 上进行了测试。

北欧半导体 nRF52840 开发板

此 Codelab 使用了三个 nRF52840 PDK 板

a6693da3ce213856.png

安装软件

如需构建和刷写 OpenThread,您需要安装 SEGGER J-Link、nRF5x 命令行工具、ARM GNU 工具链和各种 Linux 软件包。如果您已完成“构建线程网络”Codelab,就说明您已经安装了所需的全部资源。如果没有,请先完成此 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. OpenThread API 基础知识

OpenThread 的公共 API 位于 OpenThread 代码库中的 ./openthread/include/openthread 位置。这些 API 支持在线程级和平台级访问各种 OpenThread 特性和功能,以便在您的应用中使用:

  • OpenThread 实例信息和控制
  • 应用服务,例如 IPv6、UDP 和 CoAP
  • 网络凭据管理以及“Remissioner”和“Joiner”角色
  • 边界路由器管理
  • 儿童监管和 Jam 检测等增强功能

如需查看所有 OpenThread API 的参考信息,请访问 openthread.io/reference

使用 API

要使用 API,请在应用的一个文件中添加其头文件。然后调用所需的函数。

例如,OpenThread 中包含的 CLI 示例应用使用以下 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 结构是您使用 OpenThread API 时经常使用的结构。此结构在初始化后代表 OpenThread 库的静态实例,并允许用户发出 OpenThread API 调用。

例如,OpenThread 实例在 CLI 示例应用的 main() 函数中初始化:

./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 命名空间。然后,在平台特定的源文件中实现它们。因此,您可以将相同的函数标头用于其他示例平台。

例如,我们想要接入 nRF52840 按钮和 LED 的 GPIO 功能必须在 openthread-system.h 中声明。

在您的首选文本编辑器中打开 ./openthread/examples/platforms/openthread-system.h 文件。

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

操作:添加针对具体平台的 GPIO 函数声明

openthread/instance.h 标头的 #include 之后添加以下函数声明:

/**
 * 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 实例的相关信息。这完全取决于应用的需求。如果您在函数实现中不需要它,可以使用 OpenThread API 的 OT_UNUSED_VARIABLE 宏来抑制某些工具链未使用的变量的构建错误。我们稍后会看到示例。

5. 实现 GPIO 平台抽象化

在上一步中,我们了解了 ./openthread/examples/platforms/openthread-system.h 中可用于 GPIO 的平台专用函数声明。为了访问 nRF52840 开发板上的按钮和 LED,您需要为 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 按钮和 LED,请参阅北欧半导体信息中心

操作:添加标题包含内容

接下来,添加 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;
}

操作:添加函数以配置 LED。

添加以下代码,以便在初始化期间配置所有 LED 的模式和状态。

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

操作:添加函数以设置 LED 模式。

当设备角色发生变化时,将使用此函数。

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

操作:添加功能以切换 LED 模式。

此功能将用于在设备收到多播 UDP 消息时开启 LED4。

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

操作:添加函数以初始化并处理按钮按下动作

第一个函数用于初始化板以按下按钮,第二个函数会在用户按下按钮 1 时发送多播 UDP 消息。

/**
 * @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:响应设备角色更改

在我们的应用中,我们希望不同的 LED 根据设备角色而亮起。我们跟踪以下角色:Leader、路由器、End Device。我们可以将其分配给 LED,如下所示:

  • LED1 = 领先者
  • LED2 = 路由器
  • LED3 = 最终设备

为了启用此功能,应用需要知道设备角色何时发生变化,以及如何响应正确的 LED 指示灯。第一部分使用 OpenThread 实例,第二部分使用 GPIO 平台抽象功能。

在您的首选文本编辑器中打开 ./openthread/examples/apps/cli/main.c 文件。

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

操作:添加标题包含内容

main.c 文件的 include 部分,添加角色更改功能所需的 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 中,将此函数添加到 otAppCliInit 调用之后的 main() 函数中。此回调注册会告知 OpenThread 在 OpenThread 实例状态发生变化时调用 handleNetifStateChange 函数。

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

操作:添加状态更改实现

main.c 中的 main() 函数后面,实现 handleNetifStateChanged 函数。此函数检查 OpenThread 实例的 OT_CHANGED_THREAD_ROLE 标志,如果它已发生变化,则根据需要开启/关闭 LED。

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:使用多播开启 LED

在我们的应用中,我们还希望将 UDP 消息发送到网络中的所有其他设备,以便在一个板上按下 Button1 时。为了确认收到消息,我们将在其他开发板上切换 LED4 作为响应。

如需启用此功能,应用需要:

  • 启动时初始化 UDP 连接
  • 能够向网状网本地多播地址发送 UDP 消息
  • 处理传入的 UDP 消息
  • 开启 LED4 以响应 UDP 消息

在您的首选文本编辑器中打开 ./openthread/examples/apps/cli/main.c 文件。

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

操作:添加标题包含内容

main.c 文件顶部的包含部分,添加多播 UDP 功能所需的 API 头文件。

#include <string.h>

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

#include "utils/code_utils.h"

code_utils.h 标头用于 otEXPECTotEXPECT_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 中的多播支持,请参阅打开 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 LED 和按钮。

main.c 中,在 otSetStateChangedCallback 调用之后将这些函数调用添加到 main() 函数中。这些函数会初始化 GPIO 和 GPIOTE 引脚,并设置一个按钮处理程序来处理按钮推送事件。

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

操作:添加 UDP 初始化调用

main.c 中,将此函数添加到您刚刚添加的 otSysButtonInit 调用中的 main() 函数中:

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 中,在您刚刚添加的 handleButtonInterrupt 函数后面添加 initUdp 函数的实现:

/**
 * 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 通过传递 OT_NETIF_THREAD 将套接字绑定到 Thread 网络接口。如需了解其他网络接口选项,请参阅 UDP API 参考中的 otNetifIdentifier 枚举。

操作:实现 UDP 消息传递。

main.c 中,在您刚刚添加的 initUdp 函数后面添加 sendUdp 函数的实现:

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

请注意 otEXPECTotEXPECT_ACTION 宏。这可确保 UDP 消息有效并在缓冲区中正确分配;如果无效,函数会跳转到 exit 块以释放缓冲区,从而妥善处理错误。

如需详细了解用于初始化 UDP 的函数,请参阅 openthread.io 中的 IPv6UDP 参考文档。

操作:实现 UDP 消息处理。

main.c 中,在您刚刚添加的 sendUdp 函数后面添加 handleUdpReceive 函数的实现。此功能只会开启 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/examples/apps/cli/main.c

操作:添加标头包含

main.c 文件顶部的 include 部分,添加配置 Thread 网络所需的 API 头文件:

#include <openthread/dataset_ftd.h>

操作:添加函数声明以设置网络配置。

将此声明添加到 main.c(在标头包含任何 #if 语句之后)。此函数将在主应用函数之后定义。

static void setNetworkConfiguration(otInstance *aInstance);

操作:添加网络配置调用

main.c 中,将此函数调用添加到 otSetStateChangedCallback 调用后的 main() 函数中。此函数配置 Thread 网络数据集。

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

操作:添加调用以启用 Thread 网络接口和堆栈。

main.c 中,在 otSysButtonInit 调用之后将这些函数调用添加到 main() 函数中。

/* 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 中,在 main() 函数后面添加 setNetworkConfiguration 函数的实现:

/**
 * 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
  • PAN ID = 0x2222
  • 扩展型 PAN ID = C0DE1AB5C0DE1AB5
  • 网络密钥 = 1234C0DE1AB51234C0DE1AB51234C0DE
  • 网络名称 = OTCodelab

此外,这就是我们减少路由器选择抖动的原因,因此设备可以更快地更改角色,以便进行演示。请注意,仅当节点为 FTD(全线程设备)时,系统才会执行此操作。详见下一步。

9. API:受限函数

OpenThread 的一些 API 都会修改仅可出于演示或测试目的而修改的设置。在生产应用的部署中,不应使用这些 API。

例如,otThreadSetRouterSelectionJitter 函数会调整终端将设备提升到路由器所需的时间(以秒为单位)。根据线程规范,此值默认为 120。为便于在此 Codelab 中使用,我们将将其更改为 20,因此您无需等待很长时间等待 Thread 节点更改角色。

注意:MTD 设备不会成为路由器,并且 MTD build 中不包含对 otThreadSetRouterSelectionJitter 等功能的支持。之后,我们需要指定 CMake 选项 -DOT_MTD=OFF,否则会遇到构建失败的情况。

您可以查看 OPENTHREAD_FTD 的预处理器指令中包含的 otThreadSetRouterSelectionJitter 函数定义来进行确认:

./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,然后在 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 驱动程序的库 build 中。

操作:将 gpio 驱动程序添加到 NordicSemiconductorCMakeLists.txt 文件中。

在您的首选文本编辑器中打开 ./third_party/NordicSemiconductor/CMakeLists.txt,并将该文件添加到 COMMON_SOURCES 部分。

...

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

11. 设置设备

完成所有代码更新后,您就可以开始构建应用并将其刷写到所有三个 Nordic nRF52840 开发板上。每台设备都将充当全线程设备 (FTD)。

构建 OpenThread

为 nRF52840 平台构建 OpenThread FTD 二进制文件。

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

转到包含 OpenThread FTD CLI 二进制文件的目录,并使用 ARM 嵌入式工具链将其转换为十六进制格式:

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

刷写开发板

ot-cli-ftd.hex 文件刷写到每个 nRF52840 开发板。

将 USB 线连接到 nRF52840 开发板上外部电源引脚旁边的 Micro-USB 调试端口,然后将其插入 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 都会亮起。您甚至可能在刷写后,看到 LED 指示灯从 3 点切换为 2 点(或 2 点到 1 点),即设备角色更改功能。

12. 应用功能

全部三个 nRF52840 开发板现在都应开机并运行 OpenThread 应用。如前所述,此应用有两个主要功能。

设备角色指示器

每个开发板上的 LED 指示灯都会反映 Thread 节点的当前角色:

  • LED1 = 领先者
  • LED2 = 路由器
  • LED3 = 最终设备

随着角色发生变化,照明 LED 灯也会随之变化。在每台设备开机后的 20 秒内,您可能已经在主板或一两天内看到过这些更改。

UDP 多播

在板上按下 Button1 时,UDP 消息会发送到网格本地多播地址,其中包含 Thread 网络中的所有其他节点。为响应此消息,所有其他板上的 LED4 可打开或关闭。对于每个开发板,LED4 都会保持开启或关闭状态,直到收到另一条 UDP 消息。

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. 演示:观察设备角色更改

您刷写的设备是特定类型的全线程设备 (FTD),称为“路由器符合条件的最终设备”(REED)。这意味着它们既可作为路由器运行,也可以作为终端设备运行,并可自行从终端设备升级到路由器。

线程最多可支持 32 个路由器,但会尝试将路由器数量保持在 16 到 23 个之间。如果 REED 作为终端设备连接,但路由器数低于 16,路由器会自动将自己提升到路由器。此变更应该在您在应用中将 otThreadSetRouterSelectionJitter 值设置为 20 秒后的随机时间(20 秒)发生。

每个 Thread 网络还具有一个 Leader,它是负责管理 Thread 网络中的一组路由器的路由器。所有设备均开启后,20 秒后,其中一个设备应该为导线(LED1 开启),另外两个设备应为路由器(LED2 开启)。

4e1e885861a66570.png

移除领先变体

如果将 Leader 从 Thread 网络中移除,其他路由器会将自身提升为 Leader,从而确保该网络仍然具有 Leader。

使用电源开关关闭排行榜(LED1 指示灯亮起)。等待大约 20 秒。在其余的两个开发板上,LED2(路由器)将关闭,LED1(领先者)将开启。此设备现在是 Thread 网络主管。

4c57c87adb40e0e3.png

重新开启原来的排行榜。它应该会自动以终端设备(LED3 亮起)状态重新加入 Thread 网络。在 20 秒内(路由器选择抖动),它会将自身提升为路由器(LED2 亮起)。

5f40afca2dcc4b5b.png

重置开发板

关闭所有三块板,然后重新打开,并观察 LED 指示灯。开机的第一个板应该以“Leader”角色启动(LED1 亮起),Thread 网络中的第一个路由器会自动成为 Leader。

其他两块布线板最初会连接到网络(作为 LED3 的指示灯亮起),但应该会在 20 秒内将自己升级到路由器(LED2 亮起)。

网络分区

开发板没能获得足够电力,或者它们之间的无线装置信号较弱。Thread 网络可能会拆分为多个分区,您的多个设备可能显示为主要副本。

线程是自我修复的,因此子任务最终应合并成一个具有一个前导分区的分区。

14. 演示:发送 UDP 多播

如果从上一个练习继续,在任何设备上都不能点亮 LED4。

选择任意白板,然后按 Button1。Thread 网络在运行应用的所有其他开发板上的 LED4 应切换其状态。如果从之前的锻炼继续,现在应该处于开启状态。

f186a2618fdbe3fd.png

再次按下同一板上的 Button1。所有其他开发板上的 LED4 指示灯应会再次开启。

在另一个开发板上按 Button1,然后观察 LED4 在其他开发板上的切换方式。在 LED4 当前已开启的开发板上按 Button1。该开发板的 LED4 指示灯会保持开启状态,但其他灯会切换为开启状态。

f5865ccb8ab7aa34.png

网络分区

您的 Jamboard 已进行分区,且其中有多个 Leader。多播消息在不同 Jamboard 之间返回的结果有所不同。如果您在已分区的开发板上(因此也是分区线程网络的唯一成员)按下 Button1,其他开发板上的 LED4 不会响应。如果发生此类情况,请重置开发板,理想情况下,开发板会重塑单个线程网络,UDP 消息传递应该正常工作。

15. 恭喜!

您已创建了一个使用 OpenThread API 的应用!

您现在已经知道:

  • 如何对北欧 nRF52840 开发板上的按钮和 LED 进行编程
  • 如何使用常见的 OpenThread API 和 otInstance
  • 如何监控和应对 OpenThread 状态变化
  • 如何向 Thread 网络中的所有设备发送 UDP 消息
  • 如何修改 Makefile

后续步骤

在此 Codelab 的基础上,尝试执行以下练习:

  • 修改 GPIO 模块以使用 GPIO 引脚(而不是板载 LED)以及连接根据路由器角色更改颜色的外部 RGB LED
  • 添加了针对其他示例平台的 GPIO 支持
  • 您可以使用 Router/Leader API 查找单个设备并对其进行 ping 操作,而不是使用多播从按钮按下操作 ping 所有设备
  • 使用 OpenThread 边界路由器将您的网状网连接到互联网,然后从 Thread 网络外部多播并点亮 LED 指示灯

深入阅读

访问 openthread.ioGitHub,获取各种 OpenThread 资源,包括:

参考: