使用 OpenThread API 进行开发

1. 简介

26b7f4f6b3ea0700

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

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

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

2a6db2e258c32237

学习内容

  • 如何为 Nordic 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 之前,您应完成 Build a Thread Network with nRF52840 Boards and OpenThread Codelab,其中:

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

此 Codelab 并未详细说明构建 OpenThread 和刷写开发板所需的任何环境设置,只是关于刷写开发板的基本说明。假设您已完成“构建线程网络”Codelab。

Linux 机器

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

Nordic Semiconductor nRF52840 板

此 Codelab 使用三块 nRF52840 PDK 开发板

a6693da3ce213856.png

安装软件

如需构建并刷写 OpenThread,您需要安装 SEGGER J-Link、nRF5x 命令行工具、ARM GNU 工具链和各种 Linux 软件包。如果您已经按照要求完成了“构建 Thread Network”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
  • 网络凭据管理,以及 Commissioner 和 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 实例

使用 OpenThread API 时,您会经常用到 otInstance 结构。初始化后,此结构表示 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(新文件)

ACTION:添加定义。

这些定义充当 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 指示灯。让我们跟踪以下角色:领导者、路由器和终端设备。我们可以按如下方式将其分配到 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

在我们的应用中,我们还希望在一个开发板上按下 Button1 时,将 UDP 消息发送到网络中的所有其他设备。为了确认收到消息,我们会相应地开启其他板上的 LED4 指示灯。

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

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

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

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

操作:添加头文件包含。

main.c 文件顶部的 include 部分,添加多播 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 中,将此函数调用添加到 while 循环中的 otSysProcessDrivers 调用之后的 main() 函数中。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 网络

为了方便演示,我们希望设备在开机时能立即开始启动 Thread 并加入网络。为此,我们将使用 otOperationalDataset 结构。此结构包含将 Thread 网络凭据传输到设备所需的所有参数。

使用此结构将替换 OpenThread 内置的网络默认设置,以提高应用的安全性,并将网络中的 Thread 节点限制为仅运行该应用的 Thread 节点。

再次在首选文本编辑器中打开 ./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);

操作:实现 Thread 网络配置。

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 不应该在使用 OpenThread 的应用生产部署中使用。

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

注意:MTD 设备不会成为路由器,而且 MTD build 中并不包含对 otThreadSetRouterSelectionJitter 之类的功能的支持。稍后,我们需要指定 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,并在 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 驱动程序添加到 NordicSemiconductor CMakeLists.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

和之前一样,请记下 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.

重复上述“Flash the boards”(刷写开发板)执行步骤。每块开发板都应以相同的方式连接到 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

9bbd96d9b1c63504

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

您刷写的设备是一种特定的全线程设备 (FTD),称为适合路由器的终端设备 (REED)。这意味着它们可以充当路由器或终端设备,并且可以将自己从终端设备提升为路由器。

Thread 最多可支持 32 个路由器,但会尽量将路由器数量保持在 16 到 23 个之间。如果 REED 作为最终设备连接,并且路由器数低于 16,则它会自动将自己提升为路由器。此更改应随机发生,具体时间应介于您在应用中将 otThreadSetRouterSelectionJitter 值设置为的秒数(20 秒)内。

每个 Thread 网络还有一个主要副本,即负责管理 Thread 网络中的一组路由器的路由器。所有设备都开启,20 秒后,其中一个应该是主要设备(LED1 打开),另外两个应该是路由器(LED2 打开)。

4e1e885861a66570

移除主管

如果主要副本从 Thread 网络中移除,另一个路由器会将自己提升为主要副本,以确保该网络仍具有主要副本。

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

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b

重置开发板

关闭所有三个开发板,然后重新打开它们并观察 LED 指示灯。第一个接通电源的开发板应以主要角色启动(LED1 亮起),Thread 网络中的第一个路由器会自动成为主要路由器。

另外两块开发板最初作为终端设备(LED3 亮起)连接到网络,但应在 20 秒内将自身提升为路由器(LED2 亮起)。

网络分区

如果您的板供电不足,或它们之间的无线电连接较弱,则 Thread 网络可能会拆分为多个分区,并且可能会有多台设备显示为主要设备。

线程可以自我修复,因此各分区最终应合并回单个主要分区。

14. 演示:发送 UDP 多播

如果从之前的锻炼继续,任何设备上都不应亮起 LED4。

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

f186a2618fdbe3fd.png

再次按同一开发板的 Button1。所有其他开发板上的 LED4 应再次切换。

按下其他开发板上的 Button1,并观察其他开发板上的 LED4 如何切换。在目前亮起 LED4 的其中一个开发板上,按下 Button1。该开发板上的 LED4 保持开启状态,但会开启其他板。

f5865ccb8ab7aa34.png

网络分区

如果您的 Jamboard 已分区,并且其中有多个 Leader,则多播消息的结果在不同 Jamboard 之间会有所不同。如果您在已分区(因此是分区 Thread 网络的唯一成员)上按 Button1,其他开发板上的 LED4 不会响应亮起。如果发生这种情况,请重置板。理想情况下,它们会改造单个 Thread 网络,UDP 消息传递应该可以正常运行。

15. 恭喜!

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

您现在已了解:

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

后续步骤

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

  • 修改 GPIO 模块以使用 GPIO 引脚(而非板载 LED),并连接可根据路由器角色改变颜色的外部 RGB LED
  • 为其他示例平台添加了 GPIO 支持
  • 不使用多播,通过按下按钮来 ping 所有设备,而是使用 Router/Leader API 来定位和 ping 单个设备
  • 使用 OpenThread 边界路由器将您的网状网络连接到互联网,然后从 Thread 网络外部多播这些路由器,让 LED 灯亮起

深入阅读

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

参考: