4/14 修正(かなり変わっています)
ダイレクトアドバタイズ
これまで通常のアドバタイズ(仕様上はUndirected Legacy Advertising)や拡張アドバタイズ(仕様上はUndirected Extended Advertising)などを扱ってきましたが、Bluetooth 5では他にもいくつかアドバタイズの種類があります。その一つにダイレクトアドバタイズ(仕様上はDirected Advertising)があります。文字通り直接相手を指定してアドバタイズするもので、指定された相手以外は接続できません。
詳細は省略
ここではダイレクトアドバタイズが何かということを説明するのは省略します。気になる場合は自分で仕様書を読むことをお勧めします。
(参考)iOS/iPadOSは対応していない
iPhoneやiPadのBluetooth APIはApple独自の手が加わっており、ダイレクトアドバタイズが無視される仕様になっています。このことからもスマートフォンでダイレクトアドバタイズが使われることは今後もなさそうな気がします(笑)
解説
ダイレクトアドバタイズを解説していきます。
ソースコード
実はソースコードもそこそこ修正しています・・・。
/*
* Copyright (c) 2012-2014 Wind River Systems, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr.h>
#include <sys/printk.h>
#include <settings/settings.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/gatt.h>
#define DEVICE_NAME CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1)
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID16_ALL, BT_UUID_16_ENCODE(BT_UUID_BAS_VAL), BT_UUID_16_ENCODE(BT_UUID_DIS_VAL)),
};
static const struct bt_data sd[] = {
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};
/* Bonded address queue. */
K_MSGQ_DEFINE(bonds_queue, sizeof(bt_addr_le_t), CONFIG_BT_MAX_PAIRED, 4);
static void connected(struct bt_conn *conn, uint8_t err)
{
char addr[BT_ADDR_LE_STR_LEN];
if (err)
{
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
if (err == BT_HCI_ERR_ADV_TIMEOUT)
{
printk("Direct advertising to %s timed out\n", addr);
}
else
{
printk("Failed to connect to %s (%u)\n", addr, err);
}
}
else
{
printk("Connected\n");
}
}
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
printk("Disconnected(reason %u)\n", reason);
// If you want to advertise again, You'll throw a thread here.
}
static struct bt_conn_cb conn_callbacks = {
.connected = connected,
.disconnected = disconnected,
};
static void bond_find(const struct bt_bond_info *info, void *user_data)
{
int err;
err = k_msgq_put(&bonds_queue, (void *) &info->addr, K_NO_WAIT);
if (err)
{
printk("No space in the queue for the bond.\n");
}
}
void main(void)
{
struct bt_le_adv_param *adv_param;
bt_addr_le_t addr;
int err = 0;
bt_conn_cb_register(&conn_callbacks);
err = bt_enable(NULL);
if (err)
{
printk("Blutooth failed to start (err %d)\n", err);
return;
}
if (IS_ENABLED(CONFIG_SETTINGS))
{
settings_load();
}
k_msgq_purge(&bonds_queue);
bt_foreach_bond(BT_ID_DEFAULT, bond_find, NULL);
if (!k_msgq_get(&bonds_queue, &addr, K_NO_WAIT))
{
char addr_buf[BT_ADDR_LE_STR_LEN];
adv_param = BT_LE_ADV_CONN_DIR(&addr);
err = bt_le_adv_start(adv_param, NULL, 0, NULL, 0);
if (err)
{
printk("Directed advertising failed to start\n");
return;
}
bt_addr_le_to_str(&addr, addr_buf, BT_ADDR_LE_STR_LEN);
printk("Direct advertising to %s started\n", addr_buf);
}
else
{
adv_param = BT_LE_ADV_CONN;
err = bt_le_adv_start(adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (err)
{
printk("Advertising failed to start (err %d)\n", err);
return;
}
printk("Regular advertising started\n");
}
}
# RTT
CONFIG_USE_SEGGER_RTT=y
CONFIG_RTT_CONSOLE=y
CONFIG_UART_CONSOLE=n
# LOG
CONFIG_LOG=y
CONFIG_BT_DEBUG_LOG=y
CONFIG_LOG_BACKEND_RTT=y
CONFIG_LOG_BACKEND_UART=n
# SERVICE
CONFIG_BT_BAS=y
# BLUETOOTH
CONFIG_SETTINGS=y
CONFIG_BT_SETTINGS=y
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="nRF connect SDK"
CONFIG_BT_SMP=y
CONFIG_BT_MAX_CONN=1
CONFIG_BT_MAX_PAIRED=1
CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
ボンディング情報を生成する
なにやらprj.confの設定がいっぱいありますが、nRF Connect SDK(以下NCS)ではボンディング情報も自分自身のソースコード上で扱うようになっています。この仕様変更(?)は地味にめんどくさくなったんじゃないかと思っています(笑)が、その代わりERASE FLASHでボンディング情報を消去することが可能になりました。
設定を保存および読み込み
ボンディング情報はいわゆる設定の一部という扱いになっており、そういった設定を保存および復元するためには
if (IS_ENABLED(CONFIG_SETTINGS))
{
settings_load();
}
CONFIG_SETTINGS=y
CONFIG_BT_SETTINGS=y
が必要になります。また、ボンディング情報はフラッシュメモリに記録されるため、これらの設定に加えて
CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
フラッシュメモリを操作する設定が必要になります。なお、後半部分のフラッシュメモリ関係の設定がないとデバッグログにエラーが出るので何かが足りないというのは気が付くのですが、フラッシュメモリの設定だというところに辿り着くのはなかなか時間がかかりました。
スキャナープロジェクトで接続する
そもそもボンディングとはどのような仕組みで行われているのでしょうか。そのことを調べるためにnRF SDKでスキャナープロジェクトを作成してみました。ベースとして使用したのはble_app_hrs_cですが自分で作ったものはHRSに関連する部分は削除しています。
(本来であればソースコードも記載したいのですが、あまりにも長くなるので割愛します)
単に接続するだけであればスキャナー側にフィルターをかけずに動かせば任意のアドバタイズを拾うことができますしコネクションは成立しますが、それだとなんでもかんでも拾ってしまうのでBASのUUIDでフィルターをかけるようにします。
/**@brief Function for initialization scanning and setting filters.
*/
static void scan_init(void)
{
ret_code_t err_code;
nrf_ble_scan_init_t init_scan;
memset(&init_scan, 0, sizeof(init_scan));
init_scan.p_scan_param = &m_scan_param;
init_scan.connect_if_match = true;
init_scan.conn_cfg_tag = APP_BLE_CONN_CFG_TAG;
err_code = nrf_ble_scan_init(&m_scan, &init_scan, scan_evt_handler);
APP_ERROR_CHECK(err_code);
ble_uuid_t uuid =
{
.uuid = BLE_UUID_BATTERY_SERVICE,
.type = BLE_UUID_TYPE_BLE,
};
err_code = nrf_ble_scan_filter_set(&m_scan, SCAN_UUID_FILTER, &uuid);
APP_ERROR_CHECK(err_code);
err_code = nrf_ble_scan_filters_enable(&m_scan, NRF_BLE_SCAN_ALL_FILTER, false);
APP_ERROR_CHECK(err_code);
}
実際に接続してみると・・・
先ほどのスキャナーのプロジェクトを使って実際に接続してみると・・・接続はできますが何か違和感があります。そうです、ボンディング情報が生成されていない気がするのです。実際、ボンディング情報が生成されたときはログに以下のようなメッセージが出ます。
<info> app: BLE_GAP_EVT_LESC_DHKEY_REQUEST
<info> nrf_ble_lesc: Calling sd_ble_gap_lesc_dhkey_reply on conn_handle: 0
<info> peer_manager_handler: Connection secured: role: Central, conn_handle: 0, procedure: Bonding
<info> app: BLE_GAP_EVT_AUTH_STATUS: status=0x0 bond=0x1 lv4: 0 kdist_own:0x3 kdist_peer:0x0
<info> peer_manager_handler: Peer data updated in flash: peer_id: 0, data_id: Bonding data, action: Update
<info> peer_manager_handler: Peer data updated in flash: peer_id: 0, data_id: Peer rank, action: Update
<info> peer_manager_handler: Peer data updated in flash: peer_id: 0, data_id: Central address resolution, action: Update
ボンディングにはセントラルとペリフェラルの両方でサービスが必要
ボンディング情報を生成する(Peer Manegerが動作する)にはペリフェラルが持つサービスをセントラル側が見つける必要があります。具体的にはnRF SDKのDiscoveryが動く必要があります。
えぇっ、でもちゃんとスキャナー側はBASを検出しているじゃん・・・はい、そのとおりです。サンプルにはAPIが足りていません。接続時にpm_conn_secureを呼び出す必要があります。
static void bas_c_evt_handler(ble_bas_c_t * p_bas_c, ble_bas_c_evt_t * p_bas_c_evt)
{
ret_code_t err_code;
switch (p_bas_c_evt->evt_type)
{
case BLE_BAS_C_EVT_DISCOVERY_COMPLETE:
{
err_code = ble_bas_c_handles_assign(p_bas_c, p_bas_c_evt->conn_handle, &p_bas_c_evt->params.bas_db);
APP_ERROR_CHECK(err_code);
// Battery service discovered. Enable notification of Battery Level.
NRF_LOG_DEBUG("Battery Service discovered. Reading battery level.");
// Initiate bonding.
err_code = pm_conn_secure(p_bas_c_evt->conn_handle, false);
if (err_code == NRF_ERROR_BUSY)
{
NRF_LOG_WARNING("pm_conn_secure(NUS) is busy.");
}
err_code = ble_bas_c_bl_read(p_bas_c);
APP_ERROR_CHECK(err_code);
NRF_LOG_DEBUG("Enabling Battery Level Notification.");
err_code = ble_bas_c_bl_notif_enable(p_bas_c);
APP_ERROR_CHECK(err_code);
} break;
ダイレクトアドバタイズは1秒間のみ
nRF SDKではタイムアウトを0に設定すれば無制限にダイレクトアドバタイズできたのですが、NCSではダイレクトアドバタイズは1秒間しかできないようです。もし、連続してダイレクトアドバタイズをしたい場合はもう一度ダイレクトアドバタイズをスタートするという形になります。
余談
本来なら僕が試したことを順を追って説明していけるのが一番よいのですが、スペースの都合上(笑)、重要なところだけを抜き出して説明しようと思います。
(というか、読むほうもかったるいですし)
ここまでちゃんと調べると理解がすごく深まります(笑)
今までなんとなく上辺で理解していた部分が非常にクリアになったので、自分としても非常に有意義なテーマだったと思っています。