nRF5 SDK for Meshで学ぶBluetooth mesh②~light_switch example概要~
実装を見てみないと、どういうものなのかよくわからないので、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として公開されているので、非常にありがたいです。
Mesh SDK自体のドキュメントはこちらにあります。
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
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が切り替える、というものです。
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(); } }
これで後は、基本的にイベントに対するハンドラで動作します。