使用 OpenThread API 進行開發

1. 簡介

26b7f4f6b3ea0700.png

Nest 發布的 OpenThreadThread® 網路通訊協定的開放原始碼實作項目。Nest 已推出 OpenThread,讓開發人員能夠充分運用 Nest 產品所採用的技術,加速開發智慧聯網家庭的產品。

Thread 規格定義了用於家用應用程式的 IPv6 穩定、安全和低功率無線裝置對通訊通訊協定。OpenThread 實作所有 Thread 網路層,包括 IPv6、6LoWPAN、IEEE 802.15.4 以及 MAC 安全性、網格連結建立和網格轉送。

在本程式碼研究室中,您將使用 OpenThread API 啟動 Thread 網路、監控並回應裝置角色的變更、傳送 UDP 訊息,以及將這些動作與實際硬體上的按鈕和 LED 連結。

2a6db2e258c32237.png

課程內容

  • 如何在 Nordic nRF52840 開發人員開發板上設計按鈕和 LED 燈
  • 如何使用常見的 OpenThread API 和 otInstance 類別
  • 如何監控及回應 OpenThread 狀態變更
  • 如何將 UDP 訊息傳送至 Thread 網路中的所有裝置
  • 如何修改 Makefile

軟硬體需求

硬體:

  • 3 Nordic Semiconductor nRF52840 開發人員專區
  • 3 條 USB 轉 micro-USB 傳輸線
  • 具備至少 3 個 USB 連接埠的 Linux 機器

軟體業:

  • GNU 工具鍊
  • Nordic nRF5x 指令列工具
  • Segger J-Link 軟體
  • OpenThread
  • Git

除非另有註明,否則本程式碼研究室的內容採用創用 CC 姓名標示 3.0 授權,程式碼範例則採用阿帕契 2.0 授權

2. 開始使用

完成硬體程式碼研究室

在開始這個程式碼研究室之前,請先完成使用 nRF52840 Boards 和 OpenThread 程式碼研究室的 Thread 網路,其中包含:

  • 提供建構和刷新裝置所需的軟體
  • 教導您如何建構 OpenThread,並在 Nordic nRF52840 白板上進行刷新
  • 示範 Thread 網路的基本概念

在這個程式碼研究室中,並未設定建構 OpenThread 和刷 Google 所需的任何環境,這只提供刷新裝置的基本操作說明。假設您已完成「建立執行緒網路程式碼研究室」的設定。

Linux 機器

本程式碼研究室的設計宗旨是使用 i386 或 x86 的 Linux 機器,將所有 Thread 開發板刷新。所有步驟均在 Ubuntu 14.04.5 LTS (Trusty Tahr) 上完成測試。

北歐半導體 nRF52840 白板

本程式碼研究室使用三個 nRF52840 PDK 主機

a6693da3ce213856.png

安裝軟體

如要建構及開啟 OpenThread,您必須安裝 SEGGER J-Link、nRF5x 指令列工具、ARM GNU Toolchain 和各種 Linux 套件。如果您已完成「建立 Thread 網路程式碼研究室」的操作,那麼您應該已經擁有一切所需。如果不是,請先完成該程式碼研究室,再確定可以建構 OpenThread 並儲存至 nRF52840 開發板。

3. 複製存放區

OpenThread 提供範例應用程式的程式碼,可供您做為本程式碼研究室的起點。

複製 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 都能在 Thread 和平台層級存取各種 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 按鈕的 GPIO 函式,必須在 openthread-system.h 中宣告 LED 燈。

使用您偏好的文字編輯器開啟 ./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 的 PIN 碼和模式
  • 控制針腳的電壓
  • 啟用 GPIO 中斷及註冊回呼

./src/src 目錄中,建立名為 gpio.c 的新檔案。在這個新檔案中,新增下列內容。

./src/src/gpio.c (新檔案)

動作:新增定義。

這些定義為 OpenThread 應用程式層級使用的 nRF52840 特定值和變數之間的抽象化機制。

/**
 * @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 Semiconductor Infocenter

動作:新增標頭包含。

接下來,新增標頭以納入 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 燈,如下所示:

  • LED 1 = 領導者
  • LED 2 = 路由器
  • LED 3 = 結束裝置

如要啟用這項功能,應用程式必須知道裝置角色何時變更,以及如何開啟正確的 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 訊息傳送至網路中的所有其他裝置。為確認收到訊息,我們會將其他白板的 LED40 切換鈕改為回應。

如要啟用這項功能,應用程式必須:

  • 啟動時設定 UDP 連線
  • 可傳送 UDP 訊息至網格本機多點傳送位址
  • 處理傳入的 UDP 訊息
  • 切換 LED44 以回應收到的 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 中,將您在上一步新增的 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,將通訊端繫結至 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 網路

為簡化示範流程,我們希望 Google 裝置可以立即啟動 Thread 並加入網路。為此,我們會使用 otOperationalDataset 結構。這個結構包含將 Thread 網路憑證傳輸至裝置所需的所有參數。

使用此結構將會覆寫 OpenThread 內建的網路預設值,讓應用程式更加安全,並將網路中的 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
  • 永久帳號 ID = 0x2222
  • 擴充永久帳號 ID = C0DE1AB5C0DE1AB5
  • 網路金鑰 = 1234C0DE1AB51234C0DE1AB51234C0DE
  • 網路名稱 = OTCodelab

此外,我們也會減少路由器選擇時基誤差,讓裝置能夠更快角色變更用於示範。請注意,只有在節點為 FTD (完整執行緒裝置) 時,系統才會執行這項作業。請參閱下一個步驟。

9. API:受限制的函式

部分 OpenThread 的 API 修改了應修改用於測試或測試的設定。這些 API 不應使用於 OpenThread 的應用程式部署作業。

例如,otThreadSetRouterSelectionJitter 函式會調整「結束」裝置將內容升級為路由器所需的時間 (以秒為單位)。根據執行緒規格,這個值預設為 120。為了方便本程式碼研究室使用,我們會將其變更為 20,這樣您就不必等待 Thread 節點變更角色。

注意事項:MTD 裝置不會成為路由器,且 MTD 版本不提供 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 檔案,使其納入北歐驅動程式的程式庫版本。

動作:將 Ggio 驅動程式新增至 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

前往使用 Thread 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 傳輸線連接至 Micro-USB 偵錯連接埠 (在 nRF52840 白板上外接電源圖釘旁),然後插入 Linux 電腦。正確設定,且 LED5 已開啟。

20a3b4b480356447.png

和之前一樣,記下 nRF52840 白板的序號:

c00d519ebec7e5f0.jpeg

前往 nRFx 指令列工具的位置,並使用 Jamboard 的序號,將 OpenThread CLI FTD 十六進位檔案閃爍至 nRF52840 白板:

$ cd ~/nrfjprog
$ ./nrfjprog -f nrf52 -s 683704924 --verify --chiperase --program \
       ~/openthread/output/nrf52840/bin/ot-cli-ftd.hex --reset

閃爍時,LE5 燈會短暫關閉。成功時會產生以下輸出內容:

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 電腦,刷新碼的指令則相同,但不包括 board 序號。請務必在

nrfjprog 閃爍指令。

如果成功,LED 燈、DLE2 或 LED3 燈兩旁會亮起。而且,你有時可能會看到閃光燈閃爍在 3 到 2 (或 2 到 1) 之間的 LED 燈亮起 (裝置角色變更功能)。

12. 應用程式功能

因此,這三款 nRF52840 白板應已支援並啟動 OpenThread 應用程式。如先前所述,這個應用程式有兩個主要功能。

裝置角色指標

各個面板上的光亮 LED 燈代表 Thread 節點目前的角色:

  • LED 1 = 領導者
  • LED 2 = 路由器
  • LED 3 = 結束裝置

隨著角色變化, LED 燈也亮起。你應該會在每部裝置開機後的 20 秒內看到這些變更。

UDP 多點傳送

按下主機板上的 Button1 時,UDP 訊息會傳送至網格的本機多點傳送位址,包括 Thread 網路中的所有其他節點。如要收到這則訊息,其他面板上的「LED4」按鈕開啟或關閉

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. 示範:觀察裝置角色變更

您刷絕的裝置是稱為「符合資格的路由器」(REED) 的完整完整執行緒裝置 (FTD)。這代表他們能夠做為路由器或裝置使用,也可從端對端裝置升級為路由器。

Thread 最多可支援 32 個路由器,但嘗試將路由器數量維持在 16 至 23 個之間。如果 REED 是做為端對端裝置附加,且路由器數量低於 16,則系統會自動將其升級為路由器。這項變更應會是您在應用程式中設定 otThreadSetRouterSelectionJitter 值 (以秒為單位) 的隨機時間。

每個 Thread 網路也各有一個待開發客戶,也就是負責管理 Thread 網路中的路由器組合的路由器。所有裝置開啟後,在 20 秒過後,其中一部裝置應為領導裝置 (LED1 燈),另一部則應該是路由器 (LED2 開啟)。

4e1e885861a66570.png

移除領先者

如果將 Lead 網路從 Thread 網路中移除,則其他路由器會自我升級至領導者,確保網路仍具備領先者。

使用電源開關關閉領導板 (設有 LED 燈亮起的主機)。等待約 20 秒。將其中一側的其中一個燈泡關閉,LED2 (路由器) 就會關閉,並開啟 LED1 (待開發客戶)。此裝置現在是 Thread 網路的領導者。

4c57c87adb40e0e3.png

重新開啟原本的領導板。這個選項應該會自動將 Thread 網路重新連接為最終裝置 (LED3 為光亮)。在 20 秒 (路由器選擇時基誤差) 內自動升級至路由器 (LED2 會亮)。

5f40afca2dcc4b5b.png

重設白板

將三個白板全部關閉,然後再重新開啟,然後看到 LED 燈。第一台電源裝置開始時,請由「領導者」(LED1 亮燈) 開始,也就是 Thread 網路中的第一個路由器會自動成為領導者。

其他兩張主機開始連線至網路,即「終端裝置」(LED3 是光亮),但應該會在 20 秒內升級為「路由器」 (LED2 燈亮起)。

網路分區

如果 Jamboard 的電力不足,或是 Thread 網路的訊號微弱,系統可能會將 Thread 網路分割為分區,而且你可能會以多部裝置做為領導者。

執行緒有自我修復性質,因此分區應最終合併為一個分區,以及一個領導者。

14. 示範:傳送 UDP 多點傳播

如果接續先前的運動,請勿對任何裝置亮起 LED 燈。

挑選任何白板,然後按下 Button1。因此,執行應用程式的所有 Thread 網路中的白板應可切換狀態。如果接續先前的練習,現在應已啟動。

f186a2618fdbe3fd.png

再按一次 Button1 鍵。所有其他主機上的 LED44 也應重新切換。

按下不同按鈕的按鈕 1,並看看 LED 燈中的其他白板切換開關。在已開啟 LED4 板上的其中一個遊戲板上按下按鈕 1。開啟該面板的 LED4 燈仍保持開啟狀態,但開啟其他應用程式的開關。

f5865ccb8ab7aa34.png

網路分區

如果白板已分區,且多個領導者之間產生了多個領導者,則多面板的內容將產生多則訊息。如按下分區上的「Button1」按鈕 (也就是該分區的 Thread 網路的唯一成員),則其他主機上的 LED4 燈不會亮起。在這種情況下,請重設白板,最好能夠形成單一 Thread 網路,而且 UDP 訊息應該可以正常運作。

15. 恭喜!

您已成功建立使用 OpenThread API 的應用程式!

你現在知道:

  • 如何在 Nordic nRF52840 開發人員開發板上設計按鈕和 LED 燈
  • 如何使用常見的 OpenThread API 和 otInstance 類別
  • 如何監控及回應 OpenThread 狀態變更
  • 如何將 UDP 訊息傳送至 Thread 網路中的所有裝置
  • 如何修改 Makefile

後續步驟

以本程式碼研究室為基礎,嘗試使用以下練習:

  • 修改 GPIO 模組以使用 GPIO 針腳,而非新手上路 LED 燈,並連接能夠根據路由器角色變更顏色的外部 RGB LED
  • 為其他範例平台新增 GPIO 支援
  • 使用 Router/Leader API 找出個別裝置並進行連線偵測 (ping),而不使用多點連線來對所有裝置進行連線偵測 (ping)
  • 使用 OpenThread 邊界路由器將網狀網路連線至網際網路,並在 Thread 網路外之處進行多點傳播,以亮起 LED 燈

其他資訊

請參考 openthread.ioGitHub,瞭解各種 OpenThread 資源,包括:

參考資料: