nRF5 SDK for Meshで学ぶBluetooth mesh②~light_switch example概要~

www.nordicsemi.com

実装を見てみないと、どういうものなのかよくわからないので、NordicのSDKを解析しながら、どういうものか理解していきます。

今回は、SDKの大枠とlight_switch exampleの概要を理解していきます。

README

なにはともあれ、READMEです。

This GitHub repository contains all the files from the official release zip package.

SDKをダウンロードしましたが、普通にGitHubソースコードが公開されていました。NordicのSDKは大部分がOSSとして公開されているので、非常にありがたいです。

github.com

Mesh SDK自体のドキュメントはこちらにあります。

www.nordicsemi.com

repository structure

  • bin: 全サンプルのプリビルドバイナリ
  • CMake: CMakeの設定ファイルとユーティリティ
  • doc: SDKのドキュメントと、Doxygenで生成したドキュメント
  • examples: サンプルアプリケーション
  • external: SoftDeviceなどの外部プロジェクト
  • mesh: meshスタックのソースコードとテストコード
  • models: mesh modelのソースコード
  • scripts: meshスタックを使ったシリアルインタフェースとの通信スクリプトなど
  • tools: 開発に役立つツール

Overview

nRF5 SDK for Mesh architecture

f:id:tomo-wait-for-it-yuki:20190212082833p:plain
mesh stack of nRF5 SDK for Mesh

component description
Models Bluetooth meshモデル。デバイスの動作とデータ形式を実装
Access バイスが受信したメッセージを適切なモデルへ転送
Device State Manager 暗号鍵とアドレスを保持。
Mesh Core ネットワーク層トランスポート層
Provisioning プロビジョナーと、ネットワークに参加する側のデバイス、両方のプロセスを実装
Bearer 低レベルの無線コントローラで、上位レイヤへの送受信インタフェースを提供
DFU (Device Firmware Upgrade) ブートローダとともに利用することでファームウェアを更新する機能を提供。Nordicのプロプライエタリ機能で一般的なmeshデバイスで使えるものではない
Mesh Stack meshの初期化など、トップレベルの機能を提供する薄いラッパ
Serial アプリケーションレベルのシリアル通信API

Resource usage

バイナリサイズを小さくするコンパイルオプションを付けると、Flashを100kB, RAMを10~13kBほど使用するようです。 Flashメモリの寿命計算方法などもあって面白いです。

Getting started

Quick start guide: Running a first example

light switch examplesを動かすための手順です。 nRF52840 DKの交換待ちなので、今は動かせないです。ぐぬぬ

このサンプルは、3つのサブアプリケーションから構成されています。

  • Generic OnOff server model
  • Generic OnOff client model
  • Provisioner

Hardware requirements

  • One nRF5 development board for the client.
  • One nRF5 development board for the provisioner.
  • One or more nRF5 development boards for the servers (maximum up to 30 boards).

あれ、ということは、nRF52840が3台必要…? サンプルのREADMEを見ると、static provisionerを使わない場合、最低2台あれば動かせるようです。安心。

デモの内容は、下記のように、clientのボタンを押すと、server側LEDのOn/Offが切り替える、というものです。

f:id:tomo-wait-for-it-yuki:20190212094621p:plain
mesh network demo

light_switchサンプル

nRF5 SDK for Meshのexamples/light_switchを見ていきます。掲載するソースコードは、主観で重要な部分のみを抽出しています。

nRF Meshアプリケーションがあれば、Provisionerとして使えるようです。

ディレクトリ構成

$ tree -L 1
.
├── client
├── CMakeLists.txt
├── img
├── include
├── provisioner
├── README.md
└── server

CMakeLists.txt

トップレベルのCMakeはシンプルです。

add_subdirectory("provisioner")

# Proxy only supported on nRF52 series devices due to nRF5 SDK dependency:
if (${${PLATFORM}_FAMILY} STREQUAL "NRF52" AND NOT PLATFORM MATCHES "nrf52810")
    add_subdirectory("client")
    add_subdirectory("server")
endif()

clientのCMakeを見てみます。プロトコルスタックを明示的に書いてあるので、長いですが、普通です。

|> examples/light_switch/client/CMakeLists.txt

set(target "light_switch_client_${PLATFORM}_${SOFTDEVICE}")

add_executable(${target}
    "${CMAKE_CURRENT_SOURCE_DIR}/src/main.c"
    "${CMAKE_SOURCE_DIR}/mesh/stack/src/mesh_stack.c"
    "${CMAKE_SOURCE_DIR}/examples/common/src/mesh_provisionee.c"
    "${MBTLE_SOURCE_DIR}/examples/common/src/rtt_input.c"
    "${CMAKE_SOURCE_DIR}/examples/common/src/simple_hal.c"
    "${CMAKE_SOURCE_DIR}/examples/common/src/mesh_app_utils.c"
    ${BLE_SOFTDEVICE_SUPPORT_SOURCE_FILES}
# meshプロトコルスタックとボードのソースファイル
    ${${nRF5_SDK_VERSION}_SOURCE_FILES})

target_include_directories(${target} PUBLIC
    "${CMAKE_CURRENT_SOURCE_DIR}/include"
    "${CMAKE_CURRENT_SOURCE_DIR}/../include"
    "${CMAKE_SOURCE_DIR}/examples/common/include"
    "${CMAKE_SOURCE_DIR}/external/rtt/include"
    ${BLE_SOFTDEVICE_SUPPORT_INCLUDE_DIRS}
# meshプロトコルスタックとボードのヘッダファイル
    ${${nRF5_SDK_VERSION}_INCLUDE_DIRS})

# example内にあるリンカスクリプトを利用
set_target_link_options(${target}
    ${CMAKE_CURRENT_SOURCE_DIR}/linker/${PLATFORM}_${SOFTDEVICE})

target_compile_options(${target} PUBLIC
    ${${ARCH}_DEFINES})

target_compile_definitions(${target} PUBLIC
    ${USER_DEFINITIONS}
    -DUSE_APP_CONFIG
    -DCONFIG_APP_IN_CORE
    ${${PLATFORM}_DEFINES}
    ${${SOFTDEVICE}_DEFINES}
    ${${BOARD}_DEFINES})

target_link_libraries(${target}
    rtt_${PLATFORM}
    uECC_${PLATFORM})

# hexファイルを作成
create_hex(${target})
add_flash_target(${target})

get_property(target_include_dirs TARGET ${target} PROPERTY INCLUDE_DIRECTORIES)
add_pc_lint(${target}
    "${CMAKE_CURRENT_SOURCE_DIR}/src/main.c"
    "${target_include_dirs}"
    "${${PLATFORM}_DEFINES};${${SOFTDEVICE}_DEFINES};${${BOARD}_DEFINES}")

add_ses_project(${target})

main.c

アプリケーションコードは、main.cのみで、client側は350行、server側は300行ほどです。 server側を見てみます。

main

main関数は、これでもか、というくらいシンプルです。

int main(void)
{
    initialize();
    start();

    for (;;)
    {
        (void)sd_app_evt_wait();
    }
}

initialize

static void initialize(void)
{
    // GPIOを有効化し、出力に設定します
    // 定義は、`exmaples/common/src/simple_hal.c`
    hal_leds_init();

    // SoftDeviceのAPIを呼び出して、BLEスタックを有効かします
    // 定義は、`exmaples/common/src/ble_softdevice_support.c`
    ble_stack_init();

    mesh_init();
}

mesh_init()は次の実装になっています。パラメータ (コールバックハンドラ含む) を用意して、mesh_stack_init()を呼び出します。

static void mesh_init(void)
{
    mesh_stack_init_params_t init_params =
    {
        .core.irq_priority       = NRF_MESH_IRQ_PRIORITY_LOWEST,
        .core.lfclksrc           = DEV_BOARD_LF_CLK_CFG,
        .core.p_uuid             = NULL,
        .models.models_init_cb   = models_init_cb,
        .models.config_server_cb = config_server_evt_cb
    };
    ERROR_CHECK(mesh_stack_init(&init_params, &m_device_provisioned));
}

mesh_stack_init()SDK側で提供されているトップレベルのAPIです。初期化処理を抜粋します。 今回は、これより下は追いかけませんが、nRF Meshアーキテクチャで見たDevice State ManagerとAccessを初期設定しているようです。

|> mesh/stack/src/mesh_stack.c

uint32_t mesh_stack_init(const mesh_stack_init_params_t * p_init_params,
                         bool * p_device_provisioned)
{
    uint32_t status;

    /* Initialize the mesh stack */
    status = nrf_mesh_init(&p_init_params->core);

    /* Initialize the access layer */
    dsm_init();
    access_init();

    /* Initialize the configuration server */
    status = config_server_init(p_init_params->models.config_server_cb);

    /* Initialize the health server for the primary element */
    status = health_server_init(&m_health_server, 0, DEVICE_COMPANY_ID,
                                p_init_params->models.health_server_attention_cb,
                                p_init_params->models.p_health_server_selftest_array,
                                p_init_params->models.health_server_num_selftests);

    /* Load configuration, and check if the device has already been provisioned */
    mesh_config_load();

    (void) dsm_flash_config_load();
    (void) access_flash_config_load();

    bool is_provisioned = mesh_stack_is_device_provisioned();

    return NRF_SUCCESS;
}

start

プロビジョニング用のパラメータを用意して、プロビジョニングを開始します。 その後、mesh_stack_start()プロトコルスタックを起動しています。

static void start(void)
{
    if (!m_device_provisioned)
    {
        static const uint8_t static_auth_data[NRF_MESH_KEY_SIZE] = STATIC_AUTH_DATA;
        mesh_provisionee_start_params_t prov_start_params =
        {
            .p_static_data    = static_auth_data,
            .prov_complete_cb = provisioning_complete_cb,
            .prov_device_identification_start_cb = device_identification_start_cb,
            .prov_device_identification_stop_cb = NULL,
            .prov_abort_cb = provisioning_aborted_cb,
            .p_device_uri = EX_URI_LS_CLIENT
        };
        ERROR_CHECK(mesh_provisionee_prov_start(&prov_start_params));
    }

    mesh_app_uuid_print(nrf_mesh_configure_device_uuid_get());

    ERROR_CHECK(mesh_stack_start());

    hal_led_mask_set(LEDS_MASK, LED_MASK_STATE_OFF);
    hal_led_blink_ms(LEDS_MASK, LED_BLINK_INTERVAL_MS, LED_BLINK_CNT_START);
}

プロトコルスタックを起動してから、プロビジョニング、ではないんですね? プロビジョニングのlistenを開始してから、プロトコルスタックの動的な部分が動き始めれば良い、ということでしょう。多分。

mesh_stack_start()の中では、ネットワーク層トランスポート層を有効化し、ベアラ層からのイベント通知を開始しています。

最後に、LEDをOFF状態にして、点滅の期間を設定しています。

start()が終わって、main()に戻ると、sd_app_evt_wait()を無限ループしています。省電力化のための割り込み待ちですね。

int main(void)
{
    initialize();
    start();

    for (;;)
    {
        (void)sd_app_evt_wait();
    }
}

これで後は、基本的にイベントに対するハンドラで動作します。