使用 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

課程內容

  • 如何在北歐 nRF52840 開發板上設計按鈕和 LED 燈
  • 如何使用常見的 OpenThread API 和 otInstance 類別
  • 如何監控 OpenThread 狀態變更並做出回應
  • 如何將 UDP 訊息傳送到 Thread 網路中的所有裝置
  • 如何修改 Makefiles

軟硬體需求

硬體:

  • 3 個北歐半電體 nRF52840 Dev 板
  • 使用 3 條 USB 轉 Micro-USB 傳輸線連接 Jamboard
  • 擁有至少 3 個 USB 連接埠的 Linux 機器

軟體:

  • GNU 工具鍊
  • 北歐 nRF5x 指令列工具
  • Sgger J-Link 軟體
  • OpenThread
  • Git

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

2. 開始使用

完成硬體程式碼研究室

在開始這個程式碼研究室之前,請先完成「使用 nRF52840 董事會和 OpenThread」程式碼研究室來完成執行緒網路,其中需:

  • 提供建構和閃爍所需的一切軟體
  • 教導您如何建立 OpenThread,並在 Nordic nRF52840 板上進行閃光燈
  • 展示 Thread 網路的基本概念

本「程式碼研究室」並未針對建構 OpenThread 並對 Jamboard 執行任何必要的設定,而這只是對於 Jamboard 的基本使用說明。假設您已經完成「建構執行緒網路程式碼研究室」。

Linux 機器

此 Codelab 旨在使用基於 i386 或 x86 的 Linux 機器刷新所有 Thread 進行板。所有步驟均已在 Ubuntu 14.04.5 LTS (Trusty Tahr) 上測試。

北歐半導體 nRF52840 板

本程式碼研究室使用了三個 nRF52840 PDK 板

a6693da3ce213856.png

安裝軟體

要建立和閃存 OpenThread,您需要安裝 SEGGER J-Link、nRF5x 命令行工具、ARM GNU 工具鍊和各個 Linux 包。如果您已順利完成「建立 Thread 網路程式碼研究室」,您就已經安裝好了。如果沒有的話,請先完成該程式碼研究室,再確定您能夠建構和 Flash 到 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,請在其中一個應用程式檔案中加入 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 呼叫。

例如,在 CLI 範例應用程式的 main() 函式中會初始化 OpenThread 執行個體:

./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 上必須以用於 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 Dev 板上的按鈕和 LED,您需要為 nRF52840 平台執行以下功能。在這個程式碼中,您將會新增以下函式:

  • 初始化 GPIO PIN 和模式
  • 控制圖釘上的電壓
  • 啟用 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 燈,請參閱 Nordic Semiconductor 資訊中心

動作:新增標頭。

接下來,新增標頭,表示您需要 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;
    }
}

動作:新增函式以初始化並處理按鈕按下。

第一項函式會初始化用於按下按鈕的 Jamboard,第二個功能則是在按下按鈕 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 檔案頂端的「included」部分中,加入您用於多播 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 函式會開啟通訊端,並註冊接收 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 網路

為方便示範,我們希望裝置能立即啟動 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
  • PAN ID = 0x2222
  • 擴展 PAN ID = C0DE1AB5C0DE1AB5
  • 網絡密鑰 = 1234C0DE1AB51234C0DE1AB51234C0DE
  • 網路名稱 = OTCodelab

此外,我們也可藉此降低「路由器選取範圍時基誤差」,因此裝置能更快變更角色以供示範之用。請注意,只有在節點是 FTD (完整執行緒裝置) 時,才會執行此操作。我們將在下一個步驟中詳細說明這項功能。

9. API:受限函式

部分 OpenThread's API 會修改相關設定,而這類設定只應用於示範或測試目的。這些 API 不應透過 OpenThread 用於實際執行的應用程式部署。

舉例來說,otThreadSetRouterSelectionJitter 函式會調整「終端裝置」將升級到路由器所需的時間 (以秒為單位)。根據 Thread 規格,這個值的預設值是 120。對於本代碼實驗室的易用性,我們將其更改為 20,因此您不必等待很短的時間等待 Thread 數據包更改角色。

注意:MTD 裝置不會轉換成路由器,且 MTD 版本未包含對 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 驅動程式的程式庫版本中。

動作:將 gpio 驅動程式新增至 NordicSemiconductor CMakeLists.txt檔案。

使用您偏好的文字編輯器開啟「./third_party/NordicSemiconductor/CMakeLists.txt」,然後將檔案新增至「COMMON_SOURCES」部分。

...

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

11. 設定裝置

完成所有代碼更新後,您將準備為所有三個 Nordic nRF52840 Dev 板建立和刷新應用程序。每部裝置都可以做為完整執行緒裝置 (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

將 Jamboard 閃爍

ot-cli-ftd.hex 檔案閃爍到每個 nRF52840 板。

將 USB 傳輸線接上 nRF52840 板上外接電源接腳旁邊的 Micro-USB 偵錯連接埠,然後插入您的 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

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

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 板」翻開,接著是板上的兩個步驟。每台板上都應以相同方式連線至 Linux 電腦,且 Flash 的指令是相同的,但 Jamboard 的序號除外。務必在

nrfjprog 閃爍指令。

如果成功,LED1、LED2 或 LED3 的每個板上都已亮起。甚至可能在閃光後不久就看到 3 到 2 或 2 到 1 的 LED LED 開關 (裝置角色改變功能)。

12. 應用程式功能

所有 nRF52840 電路板皆應已供電並執行我們的 OpenThread 應用程式。如先前所述,此應用程式具有兩個主要功能。

裝置角色指標

每塊 Jamboard 的點亮 LED 指示燈代表 Thread 節點目前的角色:

  • LED1 = 領導者
  • LED2 = 路由器
  • LED3 = 結束裝置

隨著角色改變,LED 指示燈也會亮起。您應該會在每部裝置的 20 秒內,在一或兩個板上看到這些改變。

UDP 多播

當您在 Jamboard 上按下 Button1 時,系統會將 UDP 訊息傳送至網狀本機廣播位址,其中包含 Thread 網路中的其他節點。收到此訊息後,所有其他板上的 LED4 都會開啟或關閉。每台 LED4 都會保持開啟或關閉,直到收到另一個 UDP 訊息為止。

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. 示範:觀察裝置角色異動

您刷新的裝置是一種特殊的全執行緒裝置 (FTD),稱為路由器符合資格的終端裝置 (REED)。這代表用戶端可做為路由器或「最終裝置」使用,且可直接從「裝置」升級為「路由器」。

Thread 最多可支持 32 台路由器,但儘管將路由器數量保持在 16 到 23 之間。如果 REED 附加為結束裝置,且路由器數量低於 16,則會自動將本身升級為路由器。這項變更應該會在您在應用程式中設定 otThreadSetRouterSelectionJitter 值的秒數 (20 秒) 內隨機間隔一段時間。

每個 Thread 網路都有一個領導者,該路由器是負責管理 Thread 網路中的路由器組的路由器。所有設備都打開後,20 秒後,它們必須為領先者(LED1 亮),另外 2 個為路由器(LED2)。

4e1e885861a66570.png

移除主管

如果將某個領導者從 Thread 網路中移除,則其他路由器會將自己升級為領導者,確保該網路仍具有領先者身分。

使用電源開關關閉開關板 (LED1 亮起的插座)。等待約 20 秒。在剩下的一個板上,LED2(路由器)請為設備關閉,LED1(引線)打應。這個裝置目前是 Thread 網路的負責人。

4c57c87adb40e0e3.png

重新開啟原本的領導者委員會。應用程式會自動將 Thread 網路重新加入為結束裝置 (LED3 亮起)。在 20 秒內(路由器設計吉特)將其提升到路由器(LED2 已亮起)。

5f40afca2dcc4b5b.png

重設 Jamboard

關閉三個電路板,然後再重新開啟,並觀察 LED 燈。採用「第一個」 Jamboard 的電源啟動程序應以「領導者」角色為主 (LED1 點亮),而 Thread 網路中的第一個路由器會自動成為領導者。

其他另外的 2 板板最初以連接到網絡作為 End Devices(LED3 燈亮),但請在 20 秒內為自己升級到路由器(LED2 亮起)。

網路分區

如果你的 Jamboard 並未獲得足夠的電力,或者兩部之間的無線電連線訊號微弱,表示 Thread 網路可能會分割成數個分區,而你可能有一個以上的裝置顯示為領導者。

執行緒本身是自動修復,因此分區最終會合併為一個具有一個領導者的分區。

14. 示範:傳送 UDP 多播

如果繼續先前的運動,LED4 不得在任何裝置上亮起。

選擇任何白板,然後按下 Button1。在執行 Thread 網路的任何其他 Jamboard 上,LED4 切換開關狀態。如果要繼續先前的運動,現在應該已經開啟。

f186a2618fdbe3fd.png

再按一次按鈕可按下 1。所有其他 Jamboard 上的 LED4 應再次切換。

按下其他板上的 Button1,並觀察 LED 切換其他板上的開關。在目前在 LED4 開啟的任一板上按下 Button1。這個 Jamboard 的 LED4 仍處於開啟狀態,但會在另一個板上開啟。

f5865ccb8ab7aa34.png

網路分區

如果你的 Jamboard 已分區,但其中一位領導者超過一個,則多層訊息的傳送結果會有所不同。如果您在已分離的板上按鈕 1(因此是分離螺紙網絡中唯一的成據),其他板上的 LED4 不為反應而亮起。如果發生這種情形,請重設 Jamboard,最好將 Jamboard 網路重新格式化,且 UDP 訊息應可正常運作。

15. 恭喜!

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

你現在知道:

  • 如何在北歐 nRF52840 開發板上設計按鈕和 LED 燈
  • 如何使用常見的 OpenThread API 和 otInstance 類別
  • 如何監控 OpenThread 狀態變更並做出回應
  • 如何將 UDP 訊息傳送到 Thread 網路中的所有裝置
  • 如何修改 Makefiles

後續步驟

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

  • 修改 GPIO 模組以使用 GPIO 引線,而非 LED 燈,並連接可根據路由器角色調整色彩的外部 RGB LED
  • 針對其他範例平台新增 GPIO 支援
  • 請使用 Router/Leader API 找出個別裝置並連線偵測 (ping),而不是使用多點傳送連線偵測。
  • 使用 OpenThread Border Router 將網狀網路連上網際網路,並在 Thread 網路外的多端裝置投放內容,為 LED 燈點亮。

延伸閱讀

前往 openthread.ioGitHub,查看各種 OpenThread 資源,包括:

參考資料: