OpenThread API を使用した開発

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 開発ボード x 3
  • ボードを接続するための USB - マイクロ USB ケーブル x 3
  • USB ポートが 3 つ以上ある Linux マシン

ソフトウェア:

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

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

2. 開始するには

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

この Codelab を開始する前に、「nRF52840 ボードと OpenThread を使用した Thread ネットワークの構築」Codelab を完了する必要があります。

  • ビルドとフラッシュに必要なソフトウェアがすべて詳しく説明されています
  • OpenThread をビルドして Nordic nRF52840 ボードに書き込む方法を教えます
  • Thread ネットワークの基本を紹介します

この Codelab では、OpenThread のビルドとボードのフラッシュに必要な環境設定について説明しません。ボードをフラッシュするための基本的な手順のみです。スレッド ネットワークの構築に関する 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 Toolchain、各種 Linux パッケージをインストールする必要があります。必要に応じて「スレッド ネットワークを構築する」Codelab を完了している場合、必要なものはすべてすでにインストールされています。まだ完了していない場合は、その Codelab を完了してから、OpenThread をビルドして nRF52840 開発ボードに書き込めるようにしてください。

3. リポジトリのクローンを作成する

OpenThread には、この Codelab の出発点として使用できるサンプル アプリケーション コードが用意されています。

OpenThread Nordic nRF528xx サンプル リポジトリのクローンを作成し、OpenThread をビルドします。

$ git clone --recursive https://github.com/openthread/ot-nrf528xx
$ cd ot-nrf528xx
$ ./script/bootstrap

4. OpenThread API の基本

OpenThread の公開 API は、OpenThread リポジトリの ./openthread/include/openthread にあります。これらの API は、アプリケーションで使用するために、スレッドレベルとプラットフォーム レベルの両方で OpenThread のさまざまな機能にアクセスできるようにします。

  • OpenThread インスタンスの情報と制御
  • IPv6、UDP、CoAP などのアプリケーション サービス
  • ネットワーク認証情報の管理(コミッショナーと参加者のロール)
  • ボーダー ルーターの管理
  • 拡張機能(子どもの管理、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 に含まれるサンプル アプリケーションの 1 つにプラットフォーム固有の関数を追加する場合は、まず ./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;
    }
}

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

1 つ目の関数はボタンを押したときにボードを初期化し、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 インスタンスを使用し、後半には 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 インスタンスの状態が変化するたびに、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 が押されたときに、ネットワーク内の他のすべてのデバイスにも UDP メッセージを送信します。メッセージの受信を確認するために、他のボードの LED4 をオンに切り替えます。

この機能を有効にするには、アプリケーションで次の処理を行う必要があります。

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

任意のテキスト エディタで ./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 ヘッダーは、otEXPECT マクロと otEXPECT_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.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 関数はソケットを開き、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 の 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 の API の一部は、デモまたはテスト目的でのみ変更する必要がある設定を変更します。これらの API は、OpenThread を使用したアプリケーションの本番環境デプロイでは使用しないでください。

たとえば、otThreadSetRouterSelectionJitter 関数は、エンドデバイスがルーターに昇格するまでにかかる時間(秒)を調整します。スレッド仕様により、この値のデフォルト値は 120 です。この Codelab では使いやすくするために、これを 20 に変更します。これにより、Thread ノードのロールの変更をそれほど長時間待つ必要がなくなります。

注: 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 ドライバのライブラリ ビルドに含まれるようにします。

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

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

...

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

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

コードの更新がすべて終わったので、アプリケーションをビルドして、Nordic の 3 つの nRF52840 開発ボードすべてに書き込む準備が整いました。各デバイスはフルスレッド デバイス(FTD)として機能します。

OpenThread をビルドする

nRF52840 プラットフォーム用の OpenThread FTD バイナリをビルドします。

$ cd ~/ot-nrf528xx
$ ./script/build nrf52840 UART_trans -DOT_MTD=OFF -DOT_APP_RCP=OFF -DOT_RCP=OFF

OpenThread FTD CLI バイナリがあるディレクトリに移動し、ARM 埋め込みツールチェーンで 16 進形式に変換します。

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

ボードをフラッシュする

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

nRF52840 ボードの外部電源ピンの横にあるマイクロ USB デバッグポートに USB ケーブルを接続し、Linux マシンに接続します。正しく設定すると、LED5 が点灯します。

20a3b4b480356447.png

先ほどと同様に、nRF52840 ボードのシリアル番号をメモします。

c00d519ebec7e5f0.jpeg

nRFx コマンドライン・ツールの場所に移動し、ボードのシリアル番号を使用して、OpenThread CLI FTD 16 進ファイルを 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」の繰り返し他の 2 つのボードのステップです各ボードを同じ方法で Linux マシンに接続する必要があります。ボードのシリアル番号を除き、フラッシュのコマンドは同じです。各ボードの一意のシリアル番号を

nrfjprog フラッシュ コマンド

成功すると、各ボードの LED1、LED2、または LED3 のいずれかが点灯します。点滅した直後に、点灯する LED が 3 から 2(または 2 から 1)に切り替わることもあります(デバイスロール変更機能)。

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

3 つの nRF52840 ボードすべてに電力が供給され、OpenThread アプリケーションが実行されているはずです。すでに説明したように、このアプリケーションには 2 つの主要な機能があります。

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

各ボードの点灯している LED は、Thread ノードの現在の役割を反映しています。

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

役割が変わると、LED の点灯も変わります。デバイスの電源がオンになってから 20 秒以内に、ボード上でこれらの変化がすでに確認されているはずです。

UDP マルチキャスト

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

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. デモ: デバイスロールの変更を監視する

書き込んだデバイスは、Router Eligible End Device(REED)と呼ばれる特定のフルスレッド デバイス(FTD)です。つまり、ルーターまたはエンドデバイスとして機能し、エンドデバイスからルーターに自身を昇格させることができます。

Thread は最大 32 台のルーターをサポートできますが、ルーターの数を 16 ~ 23 に維持しようとします。REED がエンドデバイスとして接続され、Router の数が 16 未満の場合、REED は自動的に Router に昇格します。この変更は、アプリケーションで otThreadSetRouterSelectionJitter 値を設定した秒数(20 秒)のランダムなタイミングで行う必要があります。

すべての Thread ネットワークにはリーダーがいます。リーダーは、Thread ネットワーク内のルーターのセットを管理するルーターです。すべてのデバイスの電源が入っている状態で、20 秒後に、そのうちの 1 台はリーダー(LED1 が点灯)になり、他の 2 台はルーター(LED2 が点灯)になります。

4e1e885861a66570.png

リーダーを削除する

リーダーが Thread ネットワークから削除されると、別の Router が自身をリーダーに昇格させ、ネットワークに引き続きリーダーが割り当てられるようにします。

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

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b.png

ボードをリセットする

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

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

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

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

Thread は自己回復性が高いため、パーティションは最終的に 1 つのリーダーで 1 つのパーティションにマージされる必要があります。

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

前のエクササイズから再開した場合、LED4 はどのデバイスでも点灯しません。

ボードを選んで、ボタン 1 を押します。アプリを実行している Thread ネットワーク内の他のすべてのボードの LED4 は、状態を切り替える必要があります。前のエクササイズから続けると、オンになっているはずです。

f186a2618fdbe3fd.png

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

別のボードで Button1 を押し、他のボードの LED4 がどのように切り替わるかを確認します。LED4 が現在点灯しているボードのいずれかで Button1 を押します。LED4 はそのボードではオンのままですが、他のボードではオンになります。

f5865ccb8ab7aa34.png

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

ボードが分割されていて、その中に複数のリーダーがある場合、マルチキャスト メッセージの結果はボードによって異なります。パーティション分割された(分割された Thread ネットワークの唯一のメンバーである)ボードで Button1 を押しても、他のボードの LED4 が点灯することはありません。このような場合は、ボードをリセットしてください。1 つの 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 リソースについては、openthread.ioGitHub をご覧ください。

関連資料: