OpenThread API を使用した開発

この Codelab について
schedule60 分
subject最終更新: 2025年5月5日
account_circle作成者: Jeff Bumgardner

1. はじめに

26b7f4f6b3ea0700.png

Nest がリリースした OpenThread は、Thread® ネットワーク プロトコルのオープンソース実装です。Google Nest は、Google Nest 製品で使用されている技術をデベロッパーが広く利用できるようにし、コネクテッド ホーム向け製品の開発を加速するために OpenThread をリリースしました。

Thread 仕様では、家庭用アプリケーション向けの IPv6 ベースの信頼性の高い、安全で低電力のワイヤレス デバイス間通信プロトコルが定義されています。OpenThread は、IPv6、6LoWPAN、MAC セキュリティを備えた IEEE 802.15.4、メッシュリンクの確立、メッシュ ルーティングなど、すべての Thread ネットワーキング レイヤを実装しています。

この Codelab では、OpenThread API を使用して Thread ネットワークを開始し、デバイスのロール変更をモニタリングして対応し、UDP メッセージを送信します。また、これらのアクションを実際のハードウェア上のボタンと LED に関連付けます。

2a6db2e258c32237.png

学習内容

  • Nordic nRF52840 開発ボードのボタンと LED をプログラムする方法
  • 一般的な OpenThread API と otInstance クラスを使用する方法
  • OpenThread の状態変化をモニタリングして対応する方法
  • Thread ネットワーク内のすべてのデバイスに UDP メッセージを送信する方法
  • Makefile を変更する方法

必要なもの

ハードウェア:

  • Nordic Semiconductor nRF52840 開発ボード 3 個
  • ボードを接続するための 3 本の USB - マイクロ USB ケーブル
  • USB ポートが 3 つ以上ある Linux マシン

ソフトウェア:

  • GNU ツールチェーン
  • Nordic nRF5x コマンドライン ツール
  • Segger J-Link ソフトウェア
  • OpenThread
  • Git

クリエイティブ コモンズの表示 3.0 ライセンスにより使用許諾されており、コードサンプルは Apache 2.0 ライセンスにより使用許諾されています。

2. 開始するには

ハードウェア Codelab を完了する

この Codelab を開始する前に、nRF52840 ボードと OpenThread を使用して Thread ネットワークを作成する Codelab を完了しておく必要があります。この Codelab では、次のことを学びます。

  • ビルドとフラッシュに必要なすべてのソフトウェアの詳細
  • OpenThread をビルドして Nordic nRF52840 ボードにフラッシュする方法について説明します。
  • Thread ネットワークの基本を説明します

この Codelab では、OpenThread をビルドしてボードをフラッシュするために必要な環境設定について詳しく説明しません。説明するのは、ボードのフラッシュに関する基本的な手順のみです。この Codelab は、Thread ネットワークの構築 Codelab をすでに完了していることを前提としています。

Linux マシン

この Codelab は、i386 ベースまたは x86 ベースの Linux マシンを使用して、すべての Thread 開発ボードをフラッシュすることを前提として設計されています。すべての手順は Ubuntu 14.04.5 LTS(Trusty Tahr)でテストしました。

Nordic Semiconductor nRF52840 ボード

この Codelab では、3 つの nRF52840 PDK ボードを使用します。

a6693da3ce213856.png

ソフトウェアをインストールする

OpenThread をビルドしてフラッシュするには、SEGGER J-Link、nRF5x コマンドライン ツール、ARM GNU ツールチェーン、さまざまな Linux パッケージをインストールする必要があります。必要に応じて「Thread ネットワークを作成する」Codelab を完了している場合は、必要なものがすべてインストールされています。まだ Codelab を完了していない場合は、OpenThread をビルドして nRF52840 デベロッパー ボードにフラッシュできるように、この Codelab を完了してから先に進んでください。

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 を使用すると、アプリで使用するために、Thread レベルとプラットフォーム レベルの両方でさまざまな OpenThread の機能にアクセスできます。

  • OpenThread インスタンスの情報と制御
  • IPv6、UDP、CoAP などのアプリケーション サービス
  • ネットワーク認証情報の管理、委員と参加者のロール
  • ボーダー ルーターの管理
  • 子どもの管理や干渉検知などの機能の強化

すべての OpenThread API に関するリファレンス情報は、openthread.io/reference で確認できます。

API の使用

API を使用するには、そのヘッダー ファイルをいずれかのアプリファイルに含めます。次に、目的の関数を呼び出します。

たとえば、OpenThread に含まれている CLI サンプルアプリでは、次の API ヘッダーが使用されます。

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

#include <openthread/config.h>
#include <openthread/cli.h>
#include <openthread/diag.h>
#include <openthread/tasklet.h>
#include <openthread/platform/logging.h>

OpenThread インスタンス

otInstance 構造は、OpenThread API を操作するときに頻繁に使用します。初期化されると、この構造体は OpenThread ライブラリの静的インスタンスを表し、ユーザーは OpenThread API を呼び出すことができます。

たとえば、OpenThread インスタンスは、CLI サンプルアプリの main() 関数で初期化されます。

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

int main(int argc, char *argv[])
{
    otInstance *instance

...

#if OPENTHREAD_ENABLE_MULTIPLE_INSTANCES
    // Call to query the buffer size
    (void)otInstanceInit(NULL, &otInstanceBufferLength);

    // Call to allocate the buffer
    otInstanceBuffer = (uint8_t *)malloc(otInstanceBufferLength);
    assert(otInstanceBuffer);

    // Initialize OpenThread with the buffer
    instance = otInstanceInit(otInstanceBuffer, &otInstanceBufferLength);
#else
    instance = otInstanceInitSingle();
#endif

...

    return 0;
}

プラットフォーム固有の関数

OpenThread に含まれているサンプル アプリケーションのいずれかにプラットフォーム固有の関数を追加する場合は、まず ./openthread/examples/platforms/openthread-system.h ヘッダーで宣言し、すべての関数に otSys 名前空間を使用します。次に、プラットフォーム固有のソースファイルに実装します。このように抽象化すると、他のサンプル プラットフォームでも同じ関数ヘッダーを使用できます。

たとえば、nRF52840 のボタンと LED に接続するために使用する GPIO 関数は、openthread-system.h で宣言する必要があります。

任意のテキスト エディタで ./openthread/examples/platforms/openthread-system.h ファイルを開きます。

./openthread/examples/platforms/openthread-system.h

対処: プラットフォーム固有の GPIO 関数宣言を追加。

openthread/instance.h ヘッダーの #include の後に、次の関数宣言を追加します。

/**
 * Init LED module.
 *
 */
void otSysLedInit(void);
void otSysLedSet(uint8_t aLed, bool aOn);
void otSysLedToggle(uint8_t aLed);

/**
* A callback will be called when GPIO interrupts occur.
*
*/
typedef void (*otSysButtonCallback)(otInstance *aInstance);
void otSysButtonInit(otSysButtonCallback aCallback);
void otSysButtonProcess(otInstance *aInstance);

これらは次のステップで実装します。

otSysButtonProcess 関数宣言では otInstance が使用されています。これにより、必要に応じて、ボタンが押されたときにアプリが OpenThread インスタンスに関する情報にアクセスできるようになります。すべてはアプリケーションのニーズによって異なります。関数の実装で必要ない場合は、OpenThread API の OT_UNUSED_VARIABLE マクロを使用して、一部のツールチェーンで未使用の変数に関するビルドエラーを抑制できます。後ほど例を紹介します。

5. GPIO プラットフォームの抽象化を実装する

前のステップでは、GPIO に使用できる ./openthread/examples/platforms/openthread-system.h のプラットフォーム固有の関数宣言について説明しました。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;
    }
}

アクション: ボタンの押下を初期化して処理する関数を追加。

最初の関数はボタンが押されたときにボードを初期化し、2 番目の関数はボタン 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 インスタンスを使用し、2 番目の部分では GPIO プラットフォーム抽象化を使用します。

任意のテキスト エディタで ./openthread/examples/apps/cli/main.c ファイルを開きます。

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

アクション: ヘッダーの包含を追加。

main.c ファイルの includes セクションに、ロール変更機能に必要な API ヘッダー ファイルを追加します。

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

アクション: OpenThread インスタンスの状態変化のハンドラ関数宣言を追加。

この宣言を main.c に追加します。ヘッダーの include の後、#if ステートメントの前に追加します。この関数は、メイン アプリケーションの後に定義されます。

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

アクション: 状態変更ハンドラ関数のコールバック登録を追加。

main.c で、otAppCliInit 呼び出しの後にこの関数を main() 関数に追加します。このコールバックの登録により、OpenThread インスタンスの状態が変化するたびに handleNetifStateChange 関数を呼び出すよう OpenThread に指示します。

/* 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 をオンにする

このアプリケーションでは、1 つのボードで Button1 が押されたときに、ネットワーク内の他のすべてのデバイスに UDP メッセージを送信することもできます。メッセージの受信を確認するため、他のボードの LED4 を切り替えます。

この機能を有効にするには、アプリが次の要件を満たしている必要があります。

  • 起動時に UDP 接続を初期化する
  • メッシュローカル マルチキャスト アドレスに UDP メッセージを送信できる
  • 受信した UDP メッセージを処理する
  • 受信した UDP メッセージに応じて LED4 を切り替える

任意のテキスト エディタで ./openthread/examples/apps/cli/main.c ファイルを開きます。

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

アクション: ヘッダーの包含を追加。

main.c ファイルの上部にある includes セクションに、マルチキャスト UDP 機能に必要な API ヘッダー ファイルを追加します。

#include <string.h>

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

#include "utils/code_utils.h"

code_utils.h ヘッダーは、実行時の条件を検証し、エラーを適切に処理する otEXPECT マクロと otEXPECT_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 のマルチキャストをご覧ください。

対処: 関数宣言を追加します。

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 で、これらの関数呼び出しを main() 関数の otSetStateChangedCallback 呼び出しの後に追加します。これらの関数は、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);
    }
}

otEXPECT マクロと otEXPECT_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>

アクション: ネットワーク設定を設定するための関数宣言を追加。

この宣言を main.c に追加します。ヘッダーの include の後、#if ステートメントの前に追加します。この関数は、メインのアプリケーション関数の後に定義されます。

static void setNetworkConfiguration(otInstance *aInstance);

アクション: ネットワーク構成呼び出しを追加します。

main.c で、この関数呼び出しを main() 関数の otSetStateChangedCallback 呼び出しの後に追加します。この関数は、Thread ネットワーク データセットを構成します。

/* Override default network credentials */
setNetworkConfiguration(instance);

アクション: Thread ネットワーク インターフェースとスタックを有効にする呼び出しを追加。

main.c で、これらの関数呼び出しを main() 関数の otSysButtonInit 呼び出しの後に追加します。

/* 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 の API の中には、デモやテストの目的でのみ変更すべき設定を変更するものがあります。これらの API は、OpenThread を使用するアプリケーションの本番環境のデプロイでは使用しないでください。

たとえば、otThreadSetRouterSelectionJitter 関数は、エンドデバイスが自身をルーターとして昇格させるのにかかる時間(秒単位)を調整します。この値のデフォルトは、スレッド仕様に基づき 120 です。この Codelab では使いやすくするため、スレッドノードのロールが変更されるのを長時間待たなくても済むように、この値を 20 に変更します。

注: MTD デバイスはルーターになりません。また、otThreadSetRouterSelectionJitter などの関数のサポートは MTD ビルドに含まれていません。後で CMake オプション -DOT_MTD=OFF を指定する必要があります。指定しない場合、ビルドは失敗します。

これは、OPENTHREAD_FTD のプリプロセッサ ディレクティブに含まれる otThreadSetRouterSelectionJitter 関数定義で確認できます。

./openthread/src/core/api/thread_ftd_api.cpp

#if OPENTHREAD_FTD

#include <openthread/thread_ftd.h>

...

void otThreadSetRouterSelectionJitter(otInstance *aInstance, uint8_t aRouterJitter)
{
    Instance &instance = *static_cast<Instance *>(aInstance);

    instance.GetThreadNetif().GetMle().SetRouterSelectionJitter(aRouterJitter);
}

...

#endif // OPENTHREAD_FTD

10. CMake の更新

アプリケーションをビルドする前に、3 つの CMake ファイルにいくつかのマイナー アップデートが必要です。これらは、ビルドシステムがアプリケーションのコンパイルとリンクに使用します。

./third_party/NordicSemiconductor/CMakeLists.txt

次に、NordicSemiconductor CMakeLists.txt にフラグを追加して、GPIO 関数がアプリケーションで定義されるようにします。

アクション: CMakeLists.txt ファイルにフラグを追加します。

任意のテキスト エディタで ./third_party/NordicSemiconductor/CMakeLists.txt を開き、COMMON_FLAG セクションに次の行を追加します。

...
set(COMMON_FLAG
    -DSPIS_ENABLED=1
    -DSPIS0_ENABLED=1
    -DNRFX_SPIS_ENABLED=1
    -DNRFX_SPIS0_ENABLED=1
    ...

    # Defined in ./third_party/NordicSemiconductor/nrfx/templates/nRF52840/nrfx_config.h
    -DGPIOTE_ENABLED=1
    -DGPIOTE_CONFIG_IRQ_PRIORITY=7
    -DGPIOTE_CONFIG_NUM_OF_LOW_POWER_EVENTS=1
)

...

./src/CMakeLists.txt

./src/CMakeLists.txt ファイルを編集して、新しい gpio.c ソースファイルを追加します。

アクション: gpio ソースを ./src/CMakeLists.txt ファイルに追加します。

任意のテキスト エディタで ./src/CMakeLists.txt を開き、ファイルを NRF_COMM_SOURCES セクションに追加します。

...

set(NRF_COMM_SOURCES
  ...
  src/gpio.c
  ...
)

...

./third_party/NordicSemiconductor/CMakeLists.txt

最後に、nrfx_gpiote.c ドライバ ファイルを NordicSemiconductor CMakeLists.txt ファイルに追加して、Nordic ドライバのライブラリビルドに含めます。

アクション: NordicSemiconductor の CMakeLists.txt ファイルに gpio ドライバを追加。

任意のテキスト エディタで ./third_party/NordicSemiconductor/CMakeLists.txt を開き、ファイルを COMMON_SOURCES セクションに追加します。

...

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

11. デバイスをセットアップする

コードの更新がすべて完了したので、3 つの 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 組み込みツールチェーンを使用してバイナリを 16 進数形式に変換します。

$ cd build/bin
$ arm-none-eabi-objcopy -O ihex ot-cli-ftd ot-cli-ftd.hex

ボードをフラッシュする

ot-cli-ftd.hex ファイルを各 nRF52840 ボードにフラッシュします。

USB ケーブルを nRF52840 ボードの外部電源ピンの横にあるマイクロ 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.

他の 2 つのボードに対しても、この「ボードにフラッシュする」手順を繰り返します。各ボードは同じ方法で Linux マシンに接続する必要があります。また、ボードのシリアル番号を除き、フラッシュするコマンドは同じです。 各ボードの一意のシリアル番号を使用してください。

nrfjprog フラッシュ コマンド。

正常に完了すると、各ボードの LED1、LED2、または LED3 が点灯します。点滅後すぐに、点灯している LED が 3 個から 2 個(または 2 個から 1 個)に切り替わることもあります(デバイスの役割変更機能)。

12. アプリケーションの機能

これで、3 つの nRF52840 ボードの電源がオンになり、OpenThread アプリケーションが実行されているはずです。前述のように、このアプリケーションには主に 2 つの機能があります。

デバイスのロール インジケーター

各ボードの点灯した LED は、Thread ノードの現在のロールを示します。

  • LED1 = リーダー
  • LED2 = ルーター
  • LED3 = エンドデバイス

役割が変わると、点灯する LED も変わります。各デバイスの電源投入から 20 秒以内に、1 つまたは 2 つのボードでこれらの変化を確認できるはずです。

UDP マルチキャスト

ボードでボタン 1 が押されると、UDP メッセージがメッシュローカル マルチキャスト アドレスに送信されます。このアドレスには、Thread ネットワーク内の他のすべてのノードが含まれます。このメッセージを受信すると、他のすべてのボードの LED4 がオンまたはオフに切り替わる。LED4 は、別の UDP メッセージを受信するまで、各ボードでオンまたはオフのままになります。

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. デモ: デバイスのロール変更をモニタリングする

フラッシュしたデバイスは、ルーター対応エンドデバイス(REED)と呼ばれる特定の種類のフル Thread デバイス(FTD)です。つまり、ルーターまたはエンドデバイスとして機能し、エンドデバイスからルーターへと昇格できます。

Thread は最大 32 台のルーターをサポートできますが、ルーターの数は 16 ~ 23 台に保たれます。REED がエンドデバイスとして接続し、ルーターの数が 16 未満の場合、REED は自動的にルーターに昇格します。この変更は、アプリで otThreadSetRouterSelectionJitter の値に設定した秒数(20 秒)以内にランダムに発生します。

すべての Thread ネットワークにはリーダーもあります。これは、Thread ネットワーク内のルーター セットの管理を担当するルーターです。すべてのデバイスの電源を入れ、20 秒ほど待つと、1 台がリーダー(LED1 が点灯)になり、残りの 2 台がルーター(LED2 が点灯)になります。

4e1e885861a66570.png

リーダーを削除する

リーダーが Thread ネットワークから削除された場合、別のルーターをリーダーに昇格させて、ネットワークにリーダーが確実に存在するようにします。

電源スイッチを使用して、リーダーボード(LED1 が点灯しているもの)をオフにします。20 秒ほど待ちます。残りの 2 つのボードのいずれかで、LED2(ルーター)がオフになり、LED1(リーダー)がオンになります。このデバイスが Thread ネットワークのリーダーになりました。

4c57c87adb40e0e3.png

元のリーダーボードを再度オンにします。エンドデバイスとして Thread ネットワークに自動的に再参加します(LED3 が点灯します)。20 秒以内(ルーター選択ジッター)で、ルーターとして昇格します(LED2 が点灯します)。

5f40afca2dcc4b5b.png

ボードをリセットする

3 つのボードをすべてオフにしてからオンに戻し、LED を確認します。最初に電源を入れたボードはリーダーの役割で起動します(LED1 が点灯します)。Thread ネットワーク内の最初のルーターは自動的にリーダーになります。

他の 2 つのボードは、最初はエンドデバイスとしてネットワークに接続しますが(LED3 が点灯)、20 秒以内にルーターとして昇格します(LED2 が点灯)。

ネットワーク パーティション

ボードに十分な電力が供給されていない場合や、ボード間の無線接続が弱い場合、Thread ネットワークがパーティションに分割され、複数のデバイスがリーダーとして表示されることがあります。

スレッドは自己修復するため、パーティションは最終的に 1 つのリーダーを持つ単一のパーティションに再び統合されます。

14. デモ: UDP マルチキャストを送信する

前回の演習から続行する場合、どのデバイスでも LED4 は点灯していないはずです。

任意のボードを選択して、ボタン 1 を押します。アプリケーションを実行している Thread ネットワーク内の他のすべてのボードの LED4 の状態が切り替わります。前の演習から続行する場合は、オンになっているはずです。

f186a2618fdbe3fd.png

同じボードのボタン 1 をもう一度押します。他のすべてのボードの LED4 が再度切り替わります。

別のボードのボタン 1 を押して、他のボードの LED4 が切り替わる様子を確認します。LED4 が現在オンになっているボードのいずれかのボタン 1 を押します。そのボードでは LED4 が点灯したままになりますが、他のボードでは切り替わります。

f5865ccb8ab7aa34.png

ネットワーク パーティション

ボードがパーティショニングされ、複数のリーダーが存在する場合、マルチキャスト メッセージの結果はボードによって異なります。パーティショニングされたボード(パーティショニングされた Thread ネットワークの唯一のメンバー)のボタン 1 を押しても、他のボードの LED4 は点灯しません。この場合は、ボードをリセットします。理想的には、単一の Thread ネットワークが再形成され、UDP メッセージングが正しく機能するはずです。

15. 完了

OpenThread API を使用するアプリケーションが作成されました。

ここまでで、次のことを学びました。

  • Nordic nRF52840 開発ボードのボタンと LED をプログラムする方法
  • 一般的な OpenThread API と otInstance クラスを使用する方法
  • OpenThread の状態変化をモニタリングして対応する方法
  • Thread ネットワーク内のすべてのデバイスに UDP メッセージを送信する方法
  • Makefile を変更する方法

次のステップ

この Codelab で学んだことを活かして、以下の演習に挑戦してみましょう。

  • オンボード LED ではなく GPIO ピンを使用するように GPIO モジュールを変更し、ルーターのロールに基づいて色が変わる外部 RGB LED を接続する
  • 別のサンプル プラットフォームの GPIO サポートを追加する
  • ボタンの押下からマルチキャストを使用してすべてのデバイスに ping を送信するのではなく、Router/Leader API を使用して個々のデバイスを検出して ping を送信する
  • OpenThread ボーダー ルーターを使用してメッシュ ネットワークをインターネットに接続し、Thread ネットワークの外部からマルチキャストして LED を点灯する

関連情報

openthread.ioGitHub で、次のようなさまざまな OpenThread リソースを確認してください。

関連資料: