NDK App 开发指南¶
本文主要通过一些示例,介绍蓝牙应用开发过程中常用的方法以及可能遇到的问题。
1 基础指标¶
1.1 功耗¶
蓝牙在不同的工作模式下功耗如下表所示:
测试条件:
基于例程
nimble\samples\bleprph_hr
测试配置:
CONFIG_SOC_DCDC_PAN1080
,CONFIG_PM_ENABLE
,CONFIG_LOW_SPEED_CLOCK_SRC
测试选项:LOW_POWER_TESET_CI_100MS
和LOW_POWER_TESET_CI_1000MS
2 开发流程¶
2.2 参考相关例程¶
蓝牙开发需要了解一些蓝牙协议相关的知识,可以参考蓝牙协议规范,网上也有很多协议的介绍,此处不作为重点。
当前SDK中提供了一些蓝牙相关的例程,涵盖了central、peripheral等。
在进行蓝牙开发之前,建议先看一下相关的文档,磨刀不误砍柴工,相信这些例程会对你的开发有所帮助。
2.3 了解蓝牙app代码的基本框架¶
2.3.1 app和host初始化¶
我们以bleprph_hr
为例,app和host的初始化默认都是在app_main
函数中:
void app_main(void)
{
int rc;
printf("app started\n");
/** set public address*/
uint8_t pub_mac[6]={8,2,3,4,5,6};
db_set_bd_address(pub_mac);
/* Initialize the NimBLE host configuration */
ble_hs_cfg.sync_cb = blehr_on_sync;
ble_npl_callout_init(&blehr_tx_timer, (struct ble_npl_eventq *)nimble_port_get_dflt_eventq(),
blehr_tx_hrate, NULL);
rc = gatt_svr_init();
assert(rc == 0);
/* Set the default device name */
rc = ble_svc_gap_device_name_set(device_name);
assert(rc == 0);
hs_thread_init();
}
从这个初始化我们可以将真正需要的初始化切割出来:
ble_hs_cfg.sync_cb = blehr_on_sync;
这个是host初始化完成后的回调函数,一般是将需要的自定义广播函数的回调添加此处。gatt_svr_init()
GATT服务初始化。hs_thread_init
host协议栈的初始化。blehr_gap_event
蓝牙状态事件的处理,比如广播,连接,断连等,可以关注下blehr_gap_event
是在广播启动函数中ble_gap_adv_start
注册的。注意:对于主机是在扫描启动函数中ble_gap_disc
注册的。
其实一般来说,一个蓝牙工程有广播,GATT服务,蓝牙事件处理,基本就搭起一个蓝牙应用的框架了,我们再由此进行展开。
同时ble_hs_cfg
是一个全局变量,很多关键回调函数和状态依赖它:
/** @brief Bluetooth Host main configuration structure
*
* Those can be used by application to configure stack.
*
* The only reason Security Manager (sm_ members) is configurable at runtime is
* to simplify security testing. Defaults for those are configured by selecting
* proper options in application's syscfg.
*/
struct ble_hs_cfg {
/**
* An optional callback that gets executed upon registration of each GATT
* resource (service, characteristic, or descriptor).
*/
ble_gatt_register_fn *gatts_register_cb;
/**
* An optional argument that gets passed to the GATT registration
* callback.
*/
void *gatts_register_arg;
/** Security Manager Local Input Output Capabilities */
uint8_t sm_io_cap;
/** @brief Security Manager OOB flag
*
* If set proper flag in Pairing Request/Response will be set.
*/
unsigned sm_oob_data_flag:1;
/** @brief Security Manager Bond flag
*
* If set proper flag in Pairing Request/Response will be set. This results
* in storing keys distributed during bonding.
*/
unsigned sm_bonding:1;
/** @brief Security Manager MITM flag
*
* If set proper flag in Pairing Request/Response will be set. This results
* in requiring Man-In-The-Middle protection when pairing.
*/
unsigned sm_mitm:1;
/** @brief Security Manager Secure Connections flag
*
* If set proper flag in Pairing Request/Response will be set. This results
* in using LE Secure Connections for pairing if also supported by remote
* device. Fallback to legacy pairing if not supported by remote.
*/
unsigned sm_sc:1;
/** @brief Security Manager Key Press Notification flag
*
* Currently unsupported and should not be set.
*/
unsigned sm_keypress:1;
/** @brief Security Manager Local Key Distribution Mask */
uint8_t sm_our_key_dist;
/** @brief Security Manager Remote Key Distribution Mask */
uint8_t sm_their_key_dist;
/** @brief Stack reset callback
*
* This callback is executed when the host resets itself and the controller
* due to fatal error.
*/
ble_hs_reset_fn *reset_cb;
/** @brief Stack sync callback
*
* This callback is executed when the host and controller become synced.
* This happens at startup and after a reset.
*/
ble_hs_sync_fn *sync_cb;
/* XXX: These need to go away. Instead, the nimble host package should
* require the host-store API (not yet implemented)..
*/
/** Storage Read callback handles read of security material */
ble_store_read_fn *store_read_cb;
/** Storage Write callback handles write of security material */
ble_store_write_fn *store_write_cb;
/** Storage Delete callback handles deletion of security material */
ble_store_delete_fn *store_delete_cb;
/** @brief Storage Status callback.
*
* This callback gets executed when a persistence operation cannot be
* performed or a persistence failure is imminent. For example, if is
* insufficient storage capacity for a record to be persisted, this
* function gets called to give the application the opportunity to make
* room.
*/
ble_store_status_fn *store_status_cb;
/** An optional argument that gets passed to the storage status callback. */
void *store_status_arg;
};
2.3.2 蓝牙广播或者扫描¶
广播函数:
static void
blehr_advertise(void)
{
struct ble_gap_adv_params adv_params;
struct ble_hs_adv_fields fields;
int rc;
/*
* Set the advertisement data included in our advertisements:
* o Flags (indicates advertisement type and other general info)
* o Advertising tx power
* o Device name
*/
memset(&fields, 0, sizeof(fields));
/*
* Advertise two flags:
* o Discoverability in forthcoming advertisement (general)
* o BLE-only (BR/EDR unsupported)
*/
fields.flags = BLE_HS_ADV_F_DISC_GEN |
BLE_HS_ADV_F_BREDR_UNSUP;
fields.name = (uint8_t *)device_name;
fields.name_len = strlen(device_name);
fields.name_is_complete = 1;
rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
printf("error setting advertisement data; rc=%d\n", rc);
return;
}
/* Begin advertising */
memset(&adv_params, 0, sizeof(adv_params));
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
#if LOW_POWER_TESET_CI_100MS || LOW_POWER_TESET_LATENCY_100MS
adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(100);
adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(100);
#endif
#if LOW_POWER_TESET_CI_1000MS || LOW_POWER_TESET_LATENCY_1000MS
adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(1000);
adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(1000);
#endif
rc = ble_gap_adv_start(blehr_addr_type, NULL, BLE_HS_FOREVER,
&adv_params, blehr_gap_event, NULL);
if (rc != 0) {
printf("error enabling advertisement; rc=%d\n", rc);
return;
}
}
扫描函数:
/**
* Initiates the GAP general discovery procedure.
*/
static void
blecent_scan(void)
{
uint8_t own_addr_type;
struct ble_gap_disc_params disc_params;
int rc;
/* Figure out address to use while advertising (no privacy for now) */
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
printf("error determining address type; rc=%d\n", rc);
return;
}
/* Tell the controller to filter duplicates; we don't want to process
* repeated advertisements from the same device.
*/
disc_params.filter_duplicates = 0;
/**
* Perform a passive scan. I.e., don't send follow-up scan requests to
* each advertiser.
*/
disc_params.passive = 1;
/* Use defaults for the rest of the parameters. */
disc_params.itvl = 60;
disc_params.window = 50;
disc_params.filter_policy = 0;
disc_params.limited = 0;
rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
blecent_gap_event, NULL);
if (rc != 0) {
printf("Error initiating GAP discovery procedure; rc=%d\n",
rc);
}
}
在初始化状态这两个函数最终会注册到ble_hs_cfg.sync_cb
。
2.3.3 GATT服务初始化¶
对于GATT服务初始化,我们看以下一段代码:
int gatt_svr_init(void)
{
int rc;
rc = ble_gatts_count_cfg(gatt_svr_svcs);
if (rc != 0) {
return rc;
}
rc = ble_gatts_add_svcs(gatt_svr_svcs);
if (rc != 0) {
return rc;
}
return 0;
}
ble_gatts_count_cfg
获取GATT config描述个数
和 ble_gatts_add_svcs``注册GATT服务
这两个函数都会涉及到gatt_svr_svcs
这个变量,跳转过去我们发现gatt_svr_svcs
真正的定义GATT服务的数据库,我们可以在这个变量中自定义实现GATT服务。
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
{
/* Service: Heart-rate */
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = BLE_UUID16_DECLARE(GATT_HRS_UUID),
.characteristics = (struct ble_gatt_chr_def[]) { {
/* Characteristic: Heart-rate measurement */
.uuid = BLE_UUID16_DECLARE(GATT_HRS_MEASUREMENT_UUID),/*声明蓝牙GATT服务*/
.access_cb = gatt_svr_chr_access_heart_rate,
.val_handle = &hrs_hrm_handle,
.flags = BLE_GATT_CHR_F_NOTIFY,
}, {
/* Characteristic: Body sensor location */
.uuid = BLE_UUID16_DECLARE(GATT_HRS_BODY_SENSOR_LOC_UUID),
.access_cb = gatt_svr_chr_access_heart_rate,
.flags = BLE_GATT_CHR_F_READ,
}, {
0, /* No more characteristics in this service */
}, }
},
{
/* Service: Device Information */
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = BLE_UUID16_DECLARE(GATT_DEVICE_INFO_UUID),
.characteristics = (struct ble_gatt_chr_def[]) { {
/* Characteristic: * Manufacturer name */
.uuid = BLE_UUID16_DECLARE(GATT_MANUFACTURER_NAME_UUID),
.access_cb = gatt_svr_chr_access_device_info,
.flags = BLE_GATT_CHR_F_READ,
}, {
/* Characteristic: Model number string */
.uuid = BLE_UUID16_DECLARE(GATT_MODEL_NUMBER_UUID),
.access_cb = gatt_svr_chr_access_device_info,
.flags = BLE_GATT_CHR_F_READ,
}, {
0, /* No more characteristics in this service */
}, }
},
{
0, /* No more services */
},
};
对于心跳服务访问gatt_svr_chr_access_heart_rate
这个函数,实现相对简单。我们可以参考bleprph_enc
例程中的gatt_svc_access
函数,,它对特性的读,写,读描述符,写描述符等等做了不同的分类和实现:
static int
gatt_svc_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
const ble_uuid_t *uuid;
int rc;
switch (ctxt->op) {
case BLE_GATT_ACCESS_OP_READ_CHR:
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
MODLOG_DFLT("Characteristic read; conn_handle=%d attr_handle=%d\n",
conn_handle, attr_handle);
} else {
MODLOG_DFLT("Characteristic read by NimBLE stack; attr_handle=%d\n",
attr_handle);
}
uuid = ctxt->chr->uuid;
if (attr_handle == gatt_svr_chr_val_handle) {
rc = os_mbuf_append(ctxt->om,
&gatt_svr_chr_val,
sizeof(gatt_svr_chr_val));
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
}
goto unknown;
case BLE_GATT_ACCESS_OP_WRITE_CHR:
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
MODLOG_DFLT("Characteristic write; conn_handle=%d attr_handle=%d",
conn_handle, attr_handle);
} else {
MODLOG_DFLT("Characteristic write by NimBLE stack; attr_handle=%d",
attr_handle);
}
uuid = ctxt->chr->uuid;
if (attr_handle == gatt_svr_chr_val_handle) {
rc = gatt_svr_write(ctxt->om,
sizeof(gatt_svr_chr_val),
sizeof(gatt_svr_chr_val),
&gatt_svr_chr_val, NULL);
ble_gatts_chr_updated(attr_handle);
MODLOG_DFLT("Notification/Indication scheduled for "
"all subscribed peers.\n");
return rc;
}
goto unknown;
case BLE_GATT_ACCESS_OP_READ_DSC:
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
MODLOG_DFLT("Descriptor read; conn_handle=%d attr_handle=%d\n",
conn_handle, attr_handle);
} else {
MODLOG_DFLT("Descriptor read by NimBLE stack; attr_handle=%d\n",
attr_handle);
}
uuid = ctxt->dsc->uuid;
if (ble_uuid_cmp(uuid, &gatt_svr_dsc_uuid.u) == 0) {
rc = os_mbuf_append(ctxt->om,
&gatt_svr_dsc_val,
sizeof(gatt_svr_chr_val));
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
}
goto unknown;
case BLE_GATT_ACCESS_OP_WRITE_DSC:
goto unknown;
default:
goto unknown;
}
unknown:
/* Unknown characteristic/descriptor;
* The NimBLE host should not have called this function;
*/
assert(0);
return BLE_ATT_ERR_UNLIKELY;
}
我们可以参考这个函数对特性的读写做一些通用的实现。
2.3.4 GAP事件处理¶
从上文我们可以知道,GAP事件函数我们可以注册到广播或者扫描函数中,我们也可以对不同的事件进行分类处理,例如连接,断连,配对事件,订阅事件,扫描收到的广播包等等:
支持的事件如下:
#define BLE_GAP_EVENT_CONNECT 0
#define BLE_GAP_EVENT_DISCONNECT 1
/* Reserved 2 */
#define BLE_GAP_EVENT_CONN_UPDATE 3
#define BLE_GAP_EVENT_CONN_UPDATE_REQ 4
#define BLE_GAP_EVENT_L2CAP_UPDATE_REQ 5
#define BLE_GAP_EVENT_TERM_FAILURE 6
#define BLE_GAP_EVENT_DISC 7
#define BLE_GAP_EVENT_DISC_COMPLETE 8
#define BLE_GAP_EVENT_ADV_COMPLETE 9
#define BLE_GAP_EVENT_ENC_CHANGE 10
#define BLE_GAP_EVENT_PASSKEY_ACTION 11
#define BLE_GAP_EVENT_NOTIFY_RX 12
#define BLE_GAP_EVENT_NOTIFY_TX 13
#define BLE_GAP_EVENT_SUBSCRIBE 14
#define BLE_GAP_EVENT_MTU 15
#define BLE_GAP_EVENT_IDENTITY_RESOLVED 16
#define BLE_GAP_EVENT_REPEAT_PAIRING 17
#define BLE_GAP_EVENT_PHY_UPDATE_COMPLETE 18
#define BLE_GAP_EVENT_EXT_DISC 19
#define BLE_GAP_EVENT_PERIODIC_SYNC 20
#define BLE_GAP_EVENT_PERIODIC_REPORT 21
#define BLE_GAP_EVENT_PERIODIC_SYNC_LOST 22
#define BLE_GAP_EVENT_SCAN_REQ_RCVD 23
#define BLE_GAP_EVENT_PERIODIC_TRANSFER 24
#define BLE_GAP_EVENT_PATHLOSS_THRESHOLD 25
#define BLE_GAP_EVENT_TRANSMIT_POWER 26
static int blehr_gap_event(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
/* A new connection was established or a connection attempt failed */
printf("connection %s; status=%d\n",
event->connect.status == 0 ? "established" : "failed",
event->connect.status);
if (event->connect.status != 0) {
/* Connection failed; resume advertising */
blehr_advertise();
conn_handle = 0;
}
else {
conn_handle = event->connect.conn_handle;
#if LOW_POWER_TESET_CI_100MS || LOW_POWER_TESET_CI_1000MS || LOW_POWER_TESET_LATENCY_100MS || LOW_POWER_TESET_LATENCY_1000MS
LowPower_Test_Timer();
#endif
}
break;
case BLE_GAP_EVENT_DISCONNECT:
printf("disconnect; reason=0x%02x\n", (uint8_t)event->disconnect.reason);
conn_handle = BLE_HS_CONN_HANDLE_NONE; /* reset conn_handle */
/* Connection terminated; resume advertising */
blehr_advertise();
break;
case BLE_GAP_EVENT_ADV_COMPLETE:
printf("adv complete\n");
blehr_advertise();
break;
case BLE_GAP_EVENT_SUBSCRIBE:
printf("subscribe event; cur_notify=%d\n value handle; "
"val_handle=%d\n",
event->subscribe.cur_notify, hrs_hrm_handle);
if (event->subscribe.attr_handle == hrs_hrm_handle) {
notify_state = event->subscribe.cur_notify;
blehr_tx_hrate_reset();
} else if (event->subscribe.attr_handle != hrs_hrm_handle) {
notify_state = event->subscribe.cur_notify;
blehr_tx_hrate_stop();
}
break;
case BLE_GAP_EVENT_MTU:
printf("mtu update event; conn_handle=%d mtu=%d\n",
event->mtu.conn_handle,
event->mtu.value);
break;
}
return 0;
}
以下为主机中的gap事件处理:
static int blecent_gap_event(struct ble_gap_event *event, void *arg)
{
struct ble_gap_conn_desc desc;
struct ble_hs_adv_fields fields;
int rc;
switch (event->type) {
case BLE_GAP_EVENT_DISC:
rc = ble_hs_adv_parse_fields(&fields, event->disc.data,
event->disc.length_data);
if (rc != 0) {
return 0;
}
/* An advertisment report was received during GAP discovery. */
print_adv_fields(&fields);
/* Try to connect to the advertiser if it looks interesting. */
blecent_connect_if_interesting(&event->disc);
return 0;
case BLE_GAP_EVENT_CONNECT:
/* A new connection was established or a connection attempt failed. */
if (event->connect.status == 0) {
/* Connection successfully established. */
printf("Connection established ");
rc = ble_gap_conn_find(event->connect.conn_handle, &desc);
assert(rc == 0);
print_conn_desc(&desc);
printf("\n");
/* Remember peer. */
rc = peer_add(event->connect.conn_handle);
if (rc != 0) {
printf("Failed to add peer; rc=%d\n", rc);
return 0;
}
/* Perform service discovery. */
rc = peer_disc_all(event->connect.conn_handle,
blecent_on_disc_complete, NULL);
if (rc != 0) {
printf("Failed to discover services; rc=%d\n", rc);
return 0;
}
} else {
/* Connection attempt failed; resume scanning. */
printf("Error: Connection failed; status=%d\n",
event->connect.status);
blecent_scan();
}
return 0;
case BLE_GAP_EVENT_DISCONNECT:
/* Connection terminated. */
printf("disconnect; reason=0x%02x\n", (uint8_t)event->disconnect.reason);
print_conn_desc(&event->disconnect.conn);
printf("\n");
/* Forget about peer. */
peer_delete(event->disconnect.conn.conn_handle);
/* Resume scanning. */
blecent_scan();
return 0;
case BLE_GAP_EVENT_DISC_COMPLETE:
printf("discovery complete; reason=%d\n",
event->disc_complete.reason);
return 0;
case BLE_GAP_EVENT_ENC_CHANGE:
/* Encryption has been enabled or disabled for this connection. */
printf("encryption change event; status=%d ",
event->enc_change.status);
rc = ble_gap_conn_find(event->enc_change.conn_handle, &desc);
assert(rc == 0);
print_conn_desc(&desc);
return 0;
case BLE_GAP_EVENT_NOTIFY_RX:
/* Peer sent us a notification or indication. */
printf("received %s; conn_handle=%d attr_handle=%d "
"attr_len=%d\n",
event->notify_rx.indication ?
"indication" :
"notification",
event->notify_rx.conn_handle,
event->notify_rx.attr_handle,
OS_MBUF_PKTLEN(event->notify_rx.om));
/* Attribute data is contained in event->notify_rx.attr_data. */
return 0;
case BLE_GAP_EVENT_MTU:
printf("mtu update event; conn_handle=%d cid=%d mtu=%d\n",
event->mtu.conn_handle,
event->mtu.channel_id,
event->mtu.value);
return 0;
case BLE_GAP_EVENT_REPEAT_PAIRING:
/* We already have a bond with the peer, but it is attempting to
* establish a new secure link. This app sacrifices security for
* convenience: just throw away the old bond and accept the new link.
*/
/* Delete the old bond. */
rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
assert(rc == 0);
ble_store_util_delete_peer(&desc.peer_id_addr);
/* Return BLE_GAP_REPEAT_PAIRING_RETRY to indicate that the host should
* continue with the pairing operation.
*/
return BLE_GAP_REPEAT_PAIRING_RETRY;
default:
return 0;
}
}