Google 致力于为黑人社区推动种族平等。查看具体行动

使用 OpenThread API 进行开发

1. 简介

26b7f4f6b3ea0700.png

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

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

在此 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 工具链
  • 北欧 nRF5x 命令行工具
  • Segger J-Link 软件
  • OpenThread
  • Git

除非另有说明,此 Codelab 的内容均遵循知识共享署名 3.0 许可,并且代码示例已获得 Apache 2.0 许可

2. 开始使用

完成硬件 Codelab

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

  • 详细说明构建和刷写所需的所有软件
  • 教您如何构建 OpenThread 并在 Nordic 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 等应用服务
  • 网络凭据管理,以及“佣金”和“联接”角色
  • 边界路由器管理
  • 儿童监督和 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 灯亮起。我们来追踪以下角色:主要、路由器、终端设备。我们可以将其分配给 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 文件顶部的包含部分中,添加多播 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.multicast 上的多播

操作:添加函数声明

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 中,在 otSysProcessDrivers 调用后的 while 循环中向 main() 函数添加此函数调用。此函数在 gpio.c 中声明,会检查是否按下了按钮,如果是,则调用在上述步骤中设置的处理程序 (handleButtonInterrupt)。

otSysButtonProcess(instance);

操作:实现按钮中断处理程序

main.c 中,在上一步中添加的 handleNetifStateChanged 函数之后添加 handleButtonInterrupt 函数的实现:

/**
 * 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 函数会在收到 UDP 消息时打开套接字并注册回调函数 (handleUdpReceive)。otUdpBind 通过传递 OT_NETIF_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 结构。此结构包含将线程网络凭据传输到设备所需的所有参数。

使用该结构将覆盖 OpenThread 中内置的网络默认值,以提高我们的应用的安全性,并限制网络中的线程线程,使其仅运行应用。

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

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

操作:添加标头包含。

main.c 文件顶部的包含部分中,添加配置线程网络所需的 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);

操作:添加调用以启用线程线程接口和堆栈。

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

正如我们在函数中详细介绍的一样,我们在此应用中使用的线程网络参数包括:

  • 渠道 = 15
  • PAN ID = 0x2222
  • PAN 扩展 ID = C0DE1AB5C0DE1AB5
  • 网络密钥 = 1234C0DE1AB51234C0DE1AB51234C0DE
  • 网络名称 = OTCodelab

此外,这也是我们降低路由器选择抖动的原因,因此我们的设备可以更快地更改角色,以进行演示。请注意,仅当节点为 FTD(全线程设备)时才会执行此操作。如需了解详情,请参阅下一步。

9. API:受限函数

部分 OpenThread&s33; 的 API 修改了应出于演示或测试目的而修改的设置。不应在使用 OpenThread 的应用的生产部署中使用这些 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 更新

在构建应用之前,需要对 3 个 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 驱动程序文件添加到北欧半导体 CMakeLists.txt 文件,这样该文件就会包含在北欧驱动程序的库 build 中。

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

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

...

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

11. 设置设备

完成所有代码更新后,您就可以构建应用并将其刷入三个北欧 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 将亮起。您甚至可能会在刷写后很快看到指示灯从 3 变为 2(或 2 到 1)的 LED 指示灯(设备角色更改功能)。

12. 应用功能

所有三个 nRF52840 开发板现在都应启动并运行我们的 OpenThread 应用。如前所述,此应用有两个主要功能。

设备角色指示器

每个开发板上的亮起 LED 灯反映线程线程的当前角色:

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

随着角色变化,照明 LED 也随之变化。在每台设备的开机前 20 秒内,您应该已经在板上或两个设备上看到这些更改。

UDP 多播

在板上按下 Button1 后,UDP 消息会发送到网格本地多播地址,其中包含线程线程中的所有其他节点。为了响应此消息,所有其他板上的 LED4 按钮将被开启或关闭。LED4 会在每个板卡上保持打开或关闭状态,直到它收到其他 UDP 消息。

203dd094acca1f97.png

9bbd96d9b1c63504.png

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

您刷写的设备是特定类型的全线程设备 (FTD),称为路由器符合要求的最终设备 (REED)。这意味着它们可以充当路由器或终端设备,并且可以从终端设备升级为路由器。

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

每个 Thread 网络还有一个 Leader,后者是负责管理 Thread 网络中的路由器集的路由器。所有设备都处于打开状态,20 秒后,其中一台应该是领导者(LED1 已开启),另外两台应该是路由器(LED2 已开启)。

4e1e885861a66570.png

移除领先变体

如果将领先者从线程网络中移除,则不同的路由器会将自己提升为领先变体,以确保网络仍具有领先变体。

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

4c57c87adb40e0e3.png

重新开启原始的排行榜。它应该会以最终用户设备(LED3 指示灯亮起)的形式自动重新加入线程网络。在 20 秒内(路由器选择抖动),它就会提升为路由器(LED2 会发光)。

5f40afca2dcc4b5b.png

重置开发板

关闭三块板,然后重新打开并观察 LED 指示灯。接通电源的第一块电路应该以主管的身份启动(LED1 亮起),线程网络中的第一个路由器会自动成为主管。

其他两块板最初以结束设备(LED3 的灯具)的形式连接到网络,但应在 20 秒内自行升级到路由器(LED2 的灯具)。

网络分区

如果您的开发板无法获得充足的电量,或者其之间的无线连接信号较弱,那么线程网可能会被拆分成多个分区,并且您可能会将多个设备显示为主要副本。

线程会自行修复,因此各个分区最终应合并成一个由单个主要块组成的分区。

14. 演示:发送 UDP 多播

如果继续从上一次运动开始,任何设备上都不应照亮 LED4。

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

f186a2618fdbe3fd.png

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

在其他板上按 Button1,并查看 LED4 如何在其他板上切换。在其中一个 LED4 板当前所在的板上,按 Button1。该开发板的 LED4 保持开启状态,但打开其他灯。

f5865ccb8ab7aa34.png

网络分区

如果您的开发板已分区,并且其中有多个领先变体,那么多块消息的多播消息结果会有所不同。如果您在已分区(因此是分区 Thread 网络的唯一成员)上的某个板上按下 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 Border Router 将网状网络连接到互联网,然后从线程网络外部将其多播,以使 LED 指示灯亮起

更多详情

查看 openthread.ioGitHub,了解各种 OpenThread 资源,包括:

参考: