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

使用 OpenThread API 进行开发

1. 简介

26b7f4f6b3ea0700.png

Nest 发布的 OpenThread 是一种 Thread® 网络协议的开源实现。Nest 发布了 OpenThread,以便让开发者广泛使用 Nest 产品中使用的技术,从而加速开发智能互联家居产品。

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

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

2a6db2e258c32237.png

学习内容

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

所需条件

硬件:

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

软件:

  • GNU 工具链
  • 北欧 nRF5x 命令行工具
  • Secker J-Link 软件
  • OpenThread
  • Git

除非另有说明,否则本 Codelab 的内容根据知识共享署名 3.0 许可和代码示例授权使用Apache 2.0 许可中披露政府所要求信息的数量和类型。

2. 开始使用

完成硬件 Codelab

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

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

此 Codelab 中详细阐述了构建 OpenThread 和刷写开发板所需的任何环境 - 仅包含有关刷写开发板的基本说明。假设您已完成“构建线程网络”Codelab。

Linux 机器

此 Codelab 旨在使用基于 i386 或 x86 的 Linux 机器刷写所有线程开发板。所有步骤均在 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 等应用服务
  • 网络凭据管理,以及 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(新文件)

操作:添加 define

这些定义充当 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,请参阅 Nordic 半导体信息中心

操作:添加标头包含

接下来,添加 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、Router、End Device。我们可以将其分配给 LED,如下所示:

  • LED1 = 领先
  • LED2 = 路由器
  • LED3 = 结束设备

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

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

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

操作:添加标头包含

main.c 文件的包含部分中,添加运行角色更改功能所需的 API 头文件。

#include <openthread/instance.h>
#include <openthread/thread.h>
#include <openthread/thread_ftd.h>

ACTION:针对 OpenThread 实例状态更改添加处理程序函数声明。

将此声明添加到 main.c 中,在标头的后面以及任何 #if 语句之前。此函数将在主应用之后定义。

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

ACTION:为状态更改处理程序函数添加回调注册。

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

在我们的应用中,我们还希望在某个 Jamboard 上按下 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 上的多播

ACTION:添加函数声明

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 中,在上一步中添加的 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:配置线程网络

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

使用此结构将替换 OpenThread 内置的网络默认设置,使应用更安全,并将网络中的线程节点限制为仅运行应用的线程节点。

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

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

操作:添加标头 include。

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

#include <openthread/dataset_ftd.h>

操作:添加用于设置网络配置的函数声明。

将此声明添加到 main.c 中,在标头的后面以及任何 #if 语句之前。此函数将在主应用函数之后定义。

static void setNetworkConfiguration(otInstance *aInstance);

操作:添加网络配置调用。

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

/* 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                      = 1;
    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 的某些 API 会修改设置,而这些属性应仅出于演示或测试目的而修改。这些 API 不应用在使用 OpenThread 的应用的生产环境部署中。

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

注意: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. 设置设备

完成所有代码更新后,您就可以构建应用并将其刷写到所有三个北欧 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 命令行工具的位置,然后使用 OpenBoard CLI 开发板的序列号将 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 指示灯反映了线程节点当前的角色:

  • 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 秒)。

每个线程网络还有一个领导者,即负责管理一个线程网络中一组路由器的路由器。所有设备都开启后,在 20 秒后,其中一个设备应成为主要设备(LED1 已开启),另外两个设备应为路由器(LED2 开启)。

4e1e885861a66570.png

移除领先变体

如果从线程网络中移除领导者,则另一个路由器会将自己提升为领导者,以确保该网络仍然有一个领导者。

使用电源开关关闭领导者板( LED1 指示灯亮起)。等待大约 20 秒钟。在剩下的两个板上,LED2(路由器)将关闭,LED1(引线)将开启。此设备现已是线程网络的领导者。

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b.png

重置 Jamboard

关闭所有三个板,然后重新打开它们并观察 LED 指示灯。已启动的第一个开发板将以领导者角色(LED1 亮起)开始,线程网络中的第一个路由器会自动成为领导者。

其他两块开发板最初都会作为端点设备(LED3 亮起)连接到网络,但应在 20 秒内提升到路由器(LED2 亮起)。

网络分区

如果您的开发板没有获得足够的电量,或者板之间的无线电连接信号较弱,则线程网络可能会拆分为多个分区,并且您可能会将多个设备显示为主要设备。

线程可自我修复,因此分区最终应合并到一个具有单个主节点的分区中。

14. 演示:发送 UDP 多播

如果继续之前的练习,那么任何设备上的 LED 4 都不应亮着。

选择任意 Jamboard 并按 Button1。运行应用的线程网络中的所有其他板上的 LED4 都应切换其状态。如果继续之前的练习,现在应该已经打开了。

f186a2618fdbe3fd.png

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

按不同开发板上的 Button1,观察 LED4 在其他开发板上的切换情况。按下 LED4 当前开启的其中一块主板上的 Button1。对于该开发板,LED4 仍保持打开状态,但处于其他状态。

f5865ccb8ab7aa34.png

网络分区

如果您的板已分区且其中有多个 Leader,则多板消息的结果将因板而异。如果您在已分区的板上(因此,也是已分区线程网络的唯一成员)按下 Button1,其他板上的 LED4 将不会亮起。如果出现此类情况,请重置 Jamboard 的数量,理想情况下,它们将改革为单个线程网络,而 UDP 消息传递功能也可正常运行。

15. 恭喜!

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

您现在已经了解:

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

后续步骤

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

  • 修改 GPIO 模块以使用 GPIO 引脚(而不是板载 LED),并连接外部的 RGB LED,后者会根据路由器角色改变颜色
  • 为其他示例平台添加 GPIO 支持
  • 您可以使用 Router/Leader API 对单个设备执行 ping 操作,而不必使用多播功能来按下按钮以 ping 所有设备。
  • 使用 OpenThread 边界路由器将网格网络连接到互联网,并从线程网络外部对它们进行多播,以点亮 LED 指示灯

更多详情

查看 openthread.ioGitHub,以获取各种 OpenThread 资源,包括:

参考: