使用 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 開發板和 OpenThread 建構 Thread 網路程式碼研究室,其中:

  • 詳細列出建構及刷機所需的軟體
  • 說明如何建構 OpenThread,並在 Nordic nRF52840 開發板上刷入該程式碼
  • 示範 Thread 網路的基本概念

本程式碼研究室不會詳細說明建構 OpenThread 和刷寫電路板所需的環境設定,只會提供刷寫電路板的基本操作說明。假設您已完成「建立 Thread 網路」程式碼研究室。

Linux 電腦

本程式碼研究室的設計是使用 i386 或 x86 架構的 Linux 電腦,將所有 Thread 開發板刷寫為快閃記憶體。所有步驟都已在 Ubuntu 14.04.5 LTS (Trusty Tahr) 上測試。

Nordic Semiconductor 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
  • 網路憑證管理,以及 Commissioner 和 Joiner 角色
  • 管理邊界路由器
  • 強化功能,例如兒童監督和干擾偵測

如要查看所有 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 (新檔案)

動作:新增定義。

這些定義可做為 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 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,如下所示:

  • 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 執行個體狀態變更新增處理常式函式宣告。

在標頭包含項目之後,以及任何 #if 陳述式之前,將這項宣告新增至 main.c。這個函式會在主要應用程式之後定義。

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

動作:為狀態變更處理常式函式新增回呼註冊。

main.c 中,將這個函式新增至 otAppCliInit 呼叫後的 main() 函式。這個回呼註冊會告知 OpenThread,每當 OpenThread 執行個體狀態變更時,就呼叫 handleNetifStateChange 函式。

/* Register Thread state change handler */
otSetStateChangedCallback(instance, handleNetifStateChanged, instance);

動作:新增狀態變更實作。

main.c 中,於 main() 函式後方實作 handleNetifStateChanged 函式。這個函式會檢查 OpenThread 執行個體的 OT_CHANGED_THREAD_ROLE 旗標,並視需要開啟/關閉 LED。

void handleNetifStateChanged(uint32_t aFlags, void *aContext)
{
   if ((aFlags & OT_CHANGED_THREAD_ROLE) != 0)
   {
       otDeviceRole changedRole = otThreadGetDeviceRole(aContext);

       switch (changedRole)
       {
       case OT_DEVICE_ROLE_LEADER:
           otSysLedSet(1, true);
           otSysLedSet(2, false);
           otSysLedSet(3, false);
           break;

       case OT_DEVICE_ROLE_ROUTER:
           otSysLedSet(1, false);
           otSysLedSet(2, true);
           otSysLedSet(3, false);
           break;

       case OT_DEVICE_ROLE_CHILD:
           otSysLedSet(1, false);
           otSysLedSet(2, false);
           otSysLedSet(3, true);
           break;

       case OT_DEVICE_ROLE_DETACHED:
       case OT_DEVICE_ROLE_DISABLED:
           /* Clear LED4 if Thread is not enabled. */
           otSysLedSet(4, false);
           break;
        }
    }
}

7. API:使用多點傳播開啟 LED

在我們的應用程式中,當一個開發板按下 Button1 時,我們也想將 UDP 訊息傳送給網路中的所有其他裝置。為確認收到訊息,我們會切換其他開發板上的 LED4。

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

  • 啟動時初始化 UDP 連線
  • 能夠將 UDP 訊息傳送至網狀網路本機多點傳播位址
  • 處理傳入的 UDP 訊息
  • 切換 LED4 以回應傳入的 UDP 訊息

使用偏好的文字編輯器開啟 ./openthread/examples/apps/cli/main.c 檔案。

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

動作:新增標頭包含項目。

main.c 檔案頂端的 include 區段中,加入多點播送 UDP 功能所需的 API 標頭檔案。

#include <string.h>

#include <openthread/message.h>
#include <openthread/udp.h>

#include "utils/code_utils.h"

code_utils.h 標頭用於 otEXPECTotEXPECT_ACTION 巨集,可驗證執行階段條件並妥善處理錯誤。

動作:新增定義和常數:

main.c 檔案中,於 includes 區段後方和任何 #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 上的「Multicast」。

動作:新增函式宣告。

main.c 檔案中,於 otTaskletsSignalPending 定義後方和 main() 函式前方,新增 UDP 專屬函式,以及代表 UDP Socket 的靜態變數:

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 中,將這個函式新增至 main() 函式,並放在您剛新增的 otSysButtonInit 呼叫後方:

initUdp(instance);

這項呼叫可確保應用程式啟動時,UDP Socket 會完成初始化。否則裝置無法傳送或接收 UDP 訊息。

動作:新增呼叫來處理 GPIO 按鈕事件。

main.cwhile 迴圈中,於 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 函式會開啟通訊端,並註冊回呼函式 (handleUdpReceive),以便在收到 UDP 訊息時執行。otUdpBind 會透過傳遞 OT_NETIF_THREAD,將插座繫結至 Thread 網路介面。如需其他網路介面選項,請參閱 UDP API 參考資料中的 otNetifIdentifier 列舉。

動作:導入 UDP 訊息傳送。

main.c 中,於您剛新增的 initUdp 函式後方,新增 sendUdp 函式的實作:

/**
 * Send a UDP datagram
 */
void sendUdp(otInstance *aInstance)
{
    otError       error = OT_ERROR_NONE;
    otMessage *   message;
    otMessageInfo messageInfo;
    otIp6Address  destinationAddr;

    memset(&messageInfo, 0, sizeof(messageInfo));

    otIp6AddressFromString(UDP_DEST_ADDR, &destinationAddr);
    messageInfo.mPeerAddr    = destinationAddr;
    messageInfo.mPeerPort    = UDP_PORT;

    message = otUdpNewMessage(aInstance, NULL);
    otEXPECT_ACTION(message != NULL, error = OT_ERROR_NO_BUFS);

    error = otMessageAppend(message, UDP_PAYLOAD, sizeof(UDP_PAYLOAD));
    otEXPECT(error == OT_ERROR_NONE);

    error = otUdpSend(aInstance, &sUdpSocket, message, &messageInfo);

 exit:
    if (error != OT_ERROR_NONE && message != NULL)
    {
        otMessageFree(message);
    }
}

請注意 otEXPECTotEXPECT_ACTION 巨集。這些程式碼可確保 UDP 訊息有效,且在緩衝區中正確分配,如果不是,函式會跳至 exit 區塊,釋放緩衝區,從而妥善處理錯誤。

如要進一步瞭解用於初始化 UDP 的函式,請參閱 openthread.io 的「IPv6」和「UDP」參考資料。

動作:實作 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

動作:新增標頭 include。

main.c 檔案頂端的 includes 區段中,新增設定 Thread 網路所需的 API 標頭檔案:

#include <openthread/dataset_ftd.h>

動作:新增函式宣告,用於設定網路設定。

在標頭包含項目之後,以及任何 #if 陳述式之前,將這項宣告新增至 main.c。這個函式會在主要應用程式函式之後定義。

static void setNetworkConfiguration(otInstance *aInstance);

動作:新增網路設定呼叫。

main.c 中,將這個函式呼叫新增至 main() 函式,並放在 otSetStateChangedCallback 呼叫後方。這個函式會設定 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 (完整 Thread 裝置) 時,才會執行這項操作。詳情請見下一個步驟。

9. API:受限制的函式

OpenThread 的部分 API 會修改設定,這些設定只能用於展示或測試。請勿在應用程式的正式版部署中使用這些 API。

舉例來說,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 開發板。每部裝置都會做為完整 Thread 裝置 (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 Embedded Toolchain 將其轉換為十六進位格式:

$ 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.

針對其他兩個開發板重複執行「Flash the boards」步驟。每塊開發板都應以相同方式連線至 Linux 電腦,且除了開發板序號外,刷寫指令也相同。請務必在 中使用每個主機板的專屬序號。

nrfjprog 閃爍的指令。

如果成功,每個板子上的 LED1、LED2 或 LED3 都會亮起。閃爍後,LED 燈可能會從 3 變為 2 (或從 2 變為 1),這是裝置角色變更功能。

12. 應用程式功能

現在應該已為所有三個 nRF52840 開發板供電,並執行 OpenThread 應用程式。如先前所述,這個應用程式有兩項主要功能。

裝置角色指標

每個開發板上的 LED 燈亮起,代表 Thread 節點的目前角色:

  • LED1 = 領導者
  • LED2 = 路由器
  • LED3 = 終端裝置

LED 燈會隨著角色變更而亮起。裝置啟動後 20 秒內,您應該會在一個或兩個看板上看到這些變更。

UDP 多點傳播

當開發板上按下 Button1 時,系統會將 UDP 訊息傳送至網狀網路本機多點傳播位址,其中包含 Thread 網路中的所有其他節點。收到這則訊息後,所有其他開發板上的 LED4 會開啟或關閉。LED4 會保持開啟或關閉狀態,直到開發板收到另一則 UDP 訊息為止。

203dd094acca1f97.png

9bbd96d9b1c63504.png

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

您刷入的裝置是特定類型的完整 Thread 裝置 (FTD),稱為「符合路由器資格的終端裝置」(REED)。這表示裝置可做為路由器或終端裝置,並從終端裝置升級為路由器。

Thread 最多可支援 32 個路由器,但會盡量將路由器數量維持在 16 到 23 個之間。如果 REED 以終端裝置身分附加,且路由器數量少於 16 個,就會自動升級為路由器。這項變更應在您於應用程式中將 otThreadSetRouterSelectionJitter 值設為的秒數內 (20 秒),隨機發生。

每個 Thread 網路也有一個領導者,也就是負責管理 Thread 網路中路由器組合的路由器。開啟所有裝置電源後,其中一個裝置應在 20 秒後成為領導者 (LED1 亮起),另外兩個裝置則應成為路由器 (LED2 亮起)。

4e1e885861a66570.png

移除領導者

如果從 Thread 網路中移除領導者,其他路由器會升格為領導者,確保網路仍有領導者。

使用「Power」開關關閉 Leader 板 (LED1 亮起的那塊)。等待約 20 秒,在其中一個剩餘的開發板上,LED2 (路由器) 會關閉,LED1 (領導者) 則會開啟。這部裝置現在是 Thread 網路的領導者。

4c57c87adb40e0e3.png

重新開啟原始排行榜。裝置應會自動重新加入 Thread 網路,並成為終端裝置 (LED3 亮起)。在 20 秒內 (路由器選擇抖動),它會將自己升級為路由器 (LED2 亮起)。

5f40afca2dcc4b5b.png

重設開發板

關閉所有三塊電路板,然後重新開啟,並觀察 LED 燈。第一個啟動的開發板應會以領導者角色啟動 (LED1 亮起),Thread 網路中的第一個路由器會自動成為領導者。

另外兩塊開發板一開始會以終端裝置身分連線至網路 (LED3 亮起),但應會在 20 秒內升級為路由器 (LED2 亮起)。

網路分區

如果主機板電力不足,或主機板之間的無線電連線訊號微弱,Thread 網路可能會分割成多個區隔,導致多部裝置顯示為領導者。

執行緒會自我修復,因此分割區最終應會合併回單一分割區,並有一個領導者。

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

如果繼續進行上一個練習,任何裝置上的 LED4 都不應亮起。

選取任一板子,然後按下按鈕 1。執行應用程式的 Thread 網路中,所有其他開發板上的 LED4 應會切換狀態。如果接續先前的練習,現在應該已開啟。

f186a2618fdbe3fd.png

再次按下 Button1,即可使用同一塊開發板。其他所有開發板上的 LED4 應會再次切換。

按下其他開發板上的按鈕 1,觀察其他開發板上的 LED4 如何切換。在 LED4 目前亮起的其中一個開發板上,按下 Button1。LED4 會持續亮起,其他 LED 則會切換狀態。

f5865ccb8ab7aa34.png

網路分區

如果白板已分割,且有多個領導者,則多播訊息的結果會因白板而異。如果按下已分割的開發板上的按鈕 1 (因此該開發板是分割 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 尋找並連線偵測個別裝置
  • 使用 OpenThread 邊界路由器將網狀網路連上網際網路,並從 Thread 網路外部多點傳播這些網路,點亮 LED

延伸閱讀

如需各種 OpenThread 資源,請前往 openthread.ioGitHub,包括:

參考資料: