1. 简介
1.1 GAP协议
GAP(General Access Protocol),全称通用访问协议,它定义了低功耗蓝牙设备的发现流程,设备管理和设备连接的建立。
低功耗蓝牙设备定义了4种角色:
- 广播者(Broadcaster):处于这种角色的设备通过发送广播 (Advertising) 让接收者发现自己。这种角色只能发广播,不能被连接。
- 观察者(Observer):处于这种角色的设备通过接收广播事件并发送扫描 (Scan) 请求。这种角色只能发送扫描请求,不能被连接。
- 外围设备(Peripheral):当广播者接受了观察者发来的连接请求后就会进入这种角色。当设备进入了这种角色之后,将会作为从设备 (Slave) 在链路中进行通信。
- 中央设备(Central):当观察者主动进行初始化,并建立一个物理链路时就会进入这种角色。这种角色在链路中同样被称为主设备 (Master)。
1.1.1 广播
广播主要有 5 种类型:
- 可连接可扫描非定向广播(Connectable scannable undirected mode):指可被任何设备发现并可连接。可扫描是指当对端设备发送扫描请求 (Scan Request) 时,本端设备需要回复扫描应答 (Scan Response)。
- 高占空比定向广播(High duty cycle directed event type):只能被指定设备所发现和连接的广播,并且广播发送间隔用户不可调整由协议栈决定。
- 可扫描非定向广播(Scannable undirected mode):可被任何设备发现,但是既不可扫描也不可连接。不可扫描是指当对端设备发送扫描请求时不会回应扫描应答,不可连接是指不能被任何设备连接。
- 不可连接非定向广播(Non-connectable undirected mode):指可被任何设备发现但是不能被连接的广播。
- 可连接低占空比定向广播(Connectable low duty cycle directed mode):同样是只能被指定设备所发现和连接的广播,但用户可修改广播间隔,最小和最大间隔不能小于100ms。
1.2 NimBLE
前面经典蓝牙相关的文章都是基于Bluedroid框架进行开发的,这个协议栈即支持经典蓝牙也支持低功耗蓝牙,因为它的兼容性高所以资源占用也较高,如果在开发前期确认不使用经典蓝牙的情况下,应更优先选择NimBLE框架。
NimBLE其实是Apache Mynewt中自带的一个蓝牙协议栈,而Apache Mynewt是一个适用于微处理器的操作系统。ESP-IDF相当于魔改了这个组件,在FreeRTOS系统下移植了进来。NimBLE最大的优点就是资源占用少,更加适用于微处理器设备;当然它只支持低功耗蓝牙。
官方文档:BLE User Guide
2. 例程
第一个例程搭建一个简单的观察者角色扫描周围的蓝牙设备,把扫描到的设备信息打印出来。第二个例程搭建一个简单的广播者角色,不断广播自己的信息,然后使用手机上的蓝牙调试助手查看信息。
2.1 menuconfig
在写代码前要使能相关的menuconfig配置,不然是include不了相关的头文件的。首先配置蓝牙控制器为低功耗蓝牙模式。
接着配置蓝牙主机协议栈为NimBLE。
想更深度地定制的话可以看看协议栈配置这里,主要都是调整协议栈的一些运行配置,具体的作用基本一看就知道,一般来说都是保持默认即可。
按“S”保存配置,再按“Q”退出。
2.2 代码
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_log.h"
#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "console/console.h"
#include "services/gap/ble_svc_gap.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "modlog/modlog.h"
#define TAG "app"
#define OWN_NAME "ESP32"
#define BDASTR "%02X:%02X:%02X:%02X:%02X:%02X"
#define BDA2STR(x) (x)[0], (x)[1], (x)[2], (x)[3], (x)[4], (x)[5]
static char * addr_str(const void *addr);
static void print_uuid(const ble_uuid_t *uuid);
static void print_adv_fields(const struct ble_hs_adv_fields *fields);
static int blecent_gap_event(struct ble_gap_event *event, void *arg);
static void blecent_scan(void);
static void blecent_on_reset(int reason);
static void blecent_on_sync(void);
static char * addr_str(const void *addr)
{
static char buf[6 * 2 + 5 + 1];
const uint8_t *u8p;
u8p = addr;
sprintf(buf, "%02x:%02x:%02x:%02x:%02x:%02x",
u8p[5], u8p[4], u8p[3], u8p[2], u8p[1], u8p[0]);
return buf;
}
static void print_uuid(const ble_uuid_t *uuid)
{
char buf[BLE_UUID_STR_LEN];
ESP_LOGI(TAG, " %s", ble_uuid_to_str(uuid, buf));
}
static void print_adv_fields(const struct ble_hs_adv_fields *fields)
{
const uint8_t *u8p;
int i;
if (fields->flags != 0) {
ESP_LOGI(TAG, "- flags=0x%02X", fields->flags);
}
if (fields->uuids16 != NULL) {
ESP_LOGI(TAG, "- uuids16(%scomplete)=", fields->uuids16_is_complete ? "" : "in");
for (i = 0; i < fields->num_uuids16; i++) {
print_uuid(&fields->uuids16[i].u);
}
}
if (fields->uuids32 != NULL) {
ESP_LOGI(TAG, "- uuids32(%scomplete)=", fields->uuids32_is_complete ? "" : "in");
for (i = 0; i < fields->num_uuids32; i++) {
print_uuid(&fields->uuids32[i].u);
}
}
if (fields->uuids128 != NULL) {
ESP_LOGI(TAG, "- uuids128(%scomplete)=", fields->uuids128_is_complete ? "" : "in");
for (i = 0; i < fields->num_uuids128; i++) {
print_uuid(&fields->uuids128[i].u);
}
}
if (fields->name != NULL) {
char *name = malloc(fields->name_len);
memcpy(name, fields->name, fields->name_len);
ESP_LOGI(TAG, "- name(%scomplete)=%s", fields->name_is_complete ? "" : "in", name);
free(name);
}
if (fields->tx_pwr_lvl_is_present) {
ESP_LOGI(TAG, "- tx_pwr_lvl=%d", fields->tx_pwr_lvl);
}
if (fields->slave_itvl_range != NULL) {
ESP_LOGI(TAG, "- slave_itvl_range=");
ESP_LOG_BUFFER_HEX(TAG, fields->slave_itvl_range, BLE_HS_ADV_SLAVE_ITVL_RANGE_LEN);
}
if (fields->sm_tk_value_is_present) {
ESP_LOGI(TAG, "- sm_tk_value=");
ESP_LOG_BUFFER_HEX(TAG, fields->sm_tk_value, 16);
}
if (fields->sm_oob_flag_is_present) {
ESP_LOGI(TAG, "- sm_oob_flag=%d", fields->sm_oob_flag);
}
if (fields->sol_uuids16 != NULL) {
ESP_LOGI(TAG, "- sol_uuids16=");
for (i = 0; i < fields->sol_num_uuids16; i++) {
print_uuid(&fields->sol_uuids16[i].u);
}
}
if (fields->sol_uuids32 != NULL) {
ESP_LOGI(TAG, "- sol_uuids32=");
for (i = 0; i < fields->sol_num_uuids32; i++) {
print_uuid(&fields->sol_uuids32[i].u);
}
}
if (fields->sol_uuids128 != NULL) {
ESP_LOGI(TAG, "- sol_uuids128=");
for (i = 0; i < fields->sol_num_uuids128; i++) {
print_uuid(&fields->sol_uuids128[i].u);
}
}
if (fields->svc_data_uuid16 != NULL) {
ESP_LOGI(TAG, "- svc_data_uuid16=");
ESP_LOG_BUFFER_HEX(TAG, fields->svc_data_uuid16, fields->svc_data_uuid16_len);
}
if (fields->public_tgt_addr != NULL) {
u8p = fields->public_tgt_addr;
for (i = 0; i < fields->num_public_tgt_addrs; i++) {
ESP_LOGI(TAG, "- public_tgt_addr=%s", addr_str(u8p));
u8p += BLE_HS_ADV_PUBLIC_TGT_ADDR_ENTRY_LEN;
}
}
if (fields->random_tgt_addr != NULL) {
u8p = fields->random_tgt_addr;
for (i = 0; i < fields->num_random_tgt_addrs; i++) {
ESP_LOGI(TAG, "- random_tgt_addr=%s ", addr_str(u8p));
u8p += BLE_HS_ADV_PUBLIC_TGT_ADDR_ENTRY_LEN;
}
}
if (fields->appearance_is_present) {
ESP_LOGI(TAG, "- appearance=0x%04X", fields->appearance);
}
if (fields->adv_itvl_is_present) {
ESP_LOGI(TAG, "- adv_itvl=0x%04X", fields->adv_itvl);
}
if (fields->device_addr_is_present) {
u8p = fields->device_addr;
ESP_LOGI(TAG, "- device_addr=%s", addr_str(u8p));
u8p += BLE_HS_ADV_PUBLIC_TGT_ADDR_ENTRY_LEN;
ESP_LOGI(TAG, "- addr_type: %d ", *u8p);
}
if (fields->le_role_is_present) {
ESP_LOGI(TAG, "- le_role=%d", fields->le_role);
}
if (fields->svc_data_uuid32 != NULL) {
ESP_LOGI(TAG, "- svc_data_uuid32=");
ESP_LOG_BUFFER_HEX(TAG, fields->svc_data_uuid32, fields->svc_data_uuid32_len);
}
if (fields->svc_data_uuid128 != NULL) {
ESP_LOGI(TAG, "- svc_data_uuid128=");
ESP_LOG_BUFFER_HEX(TAG, fields->svc_data_uuid128, fields->svc_data_uuid128_len);
}
if (fields->uri != NULL) {
ESP_LOGI(TAG, "- uri=");
ESP_LOG_BUFFER_HEX(TAG, fields->uri, fields->uri_len);
}
if (fields->mfg_data != NULL) {
ESP_LOGI(TAG, "- mfg_data=");
ESP_LOG_BUFFER_HEX(TAG, fields->mfg_data, fields->mfg_data_len);
}
}
static int blecent_gap_event(struct ble_gap_event *event, void *arg)
{
int rc = 0;
switch (event->type) {
/* 设备发现事件 */
case BLE_GAP_EVENT_DISC:
{
ESP_LOGI(TAG, "[" BDASTR "] type: %d, data_len: %d, rssi: %d", BDA2STR(event->disc.addr.val), event->disc.event_type, event->disc.length_data, event->disc.rssi);
/* 解析 */
struct ble_hs_adv_fields fields;
rc = ble_hs_adv_parse_fields(&fields, event->disc.data, event->disc.length_data);
if (rc != 0) {
break;
}
/* 打印 */
print_adv_fields(&fields);
break;
}
/* 设备发现完成 */
case BLE_GAP_EVENT_DISC_COMPLETE:
MODLOG_DFLT(INFO, "discovery complete; reason=%d\n", event->disc_complete.reason);
blecent_scan();
break;
default:
break;
}
return rc;
}
static void blecent_scan(void)
{
int rc;
/* 获取地址类型 */
uint8_t own_addr_type;
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
MODLOG_DFLT(ERROR, "error determining address type; rc=%d\n", rc);
return;
}
struct ble_gap_disc_params disc_params;
disc_params.filter_duplicates = 1;
disc_params.passive = 1;
disc_params.itvl = 0;
disc_params.window = 0;
disc_params.filter_policy = 0;
disc_params.limited = 0;
rc = ble_gap_disc(own_addr_type, 5000, &disc_params, blecent_gap_event, NULL);
if (rc != 0) {
MODLOG_DFLT(ERROR, "Error initiating GAP discovery procedure; rc=%d\n", rc);
}
}
static void blecent_on_reset(int reason)
{
MODLOG_DFLT(ERROR, "Resetting state; reason=%d\n", reason);
}
static void blecent_on_sync(void)
{
/* 配置地址 */
if (ble_hs_util_ensure_addr(0) != 0) {
return;
}
/* 启动扫描 */
blecent_scan();
}
void blecent_host_task(void *param)
{
nimble_port_run();
nimble_port_freertos_deinit();
}
int app_main()
{
/* 初始化NVS */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
/* 初始化控制器和NimBLE协议栈 */
ret = nimble_port_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to init nimble %d ", ret);
return -1;
}
/* 配置主机 */
ble_hs_cfg.reset_cb = blecent_on_reset;
ble_hs_cfg.sync_cb = blecent_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
/* 设置设备名 */
if (ble_svc_gap_device_name_set(OWN_NAME) != 0) {
ESP_LOGE(TAG, "set device name failed");
return -1;
}
/* 使能NimBLE协议栈 */
nimble_port_freertos_init(blecent_host_task);
return 0;
}
1. 配置NVS。
NVS主要是用来保存协议栈的配置的,无论是WiFi还是蓝牙都是要配置的。
2. 初始化控制器和NimBLE协议栈。
NimBLE的封装做得比Bluedroid还要好,调用nimble_port_init函数即可初始化完成。
3. 配置主机参数。
通过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;
/** Callback to handle generation of security keys */
ble_store_gen_key_fn *store_gen_key_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;
};
我这里就配置三个回调函数。一个是reset_cb,在控制器复位的时候会触发,这里就是简单地打印log。一个是sync_cb,当控制器同步的时候触发,一般就是刚启动和复位的时候,在这里面会调用blecent_scan函数使能一次扫描。
这个函数里面,首先调用ble_hs_id_infer_auto函数来获取设备的地址类型。接着调用ble_gap_disc函数去使能扫描。参数一为前面获取到的地址类型;参数二为扫描配置参数,定义如下:
struct ble_gap_disc_params {
/** Scan interval in 0.625ms units */
uint16_t itvl;
/** Scan window in 0.625ms units */
uint16_t window;
/** Scan filter policy */
uint8_t filter_policy;
/** If limited discovery procedure should be used */
uint8_t limited:1;
/** If passive scan should be used */
uint8_t passive:1;
/** If enable duplicates filtering */
uint8_t filter_duplicates:1;
};
- itvl:扫描间隔,0.625ms为一个单位;
- window:扫描窗口,即一次扫描的时长,0.625ms为一个单位;
- filter_policy:过滤策略;
- limited:是否为有限制的扫描模式;
- passive:是否使用被动扫描;
- filter_duplicates:过滤重复结果。
参数三为回调函数,参数五为用户数据。回调函数里面,主要处理两个事件。一个是设备发现事件(BLE_GAP_EVENT_DISC),每当扫描到一个广播者就会触发一次该事件,回调函数会返回广播者的信息,结构体如下:
struct ble_gap_disc_desc {
/** Advertising PDU type. Can be one of following constants:
* - BLE_HCI_ADV_RPT_EVTYPE_ADV_IND
* - BLE_HCI_ADV_RPT_EVTYPE_DIR_IND
* - BLE_HCI_ADV_RPT_EVTYPE_SCAN_IND
* - BLE_HCI_ADV_RPT_EVTYPE_NONCONN_IND
* - BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP
*/
uint8_t event_type;
/** Advertising Data length */
uint8_t length_data;
/** Advertiser address */
ble_addr_t addr;
/** Received signal strength indication in dBm (127 if unavailable) */
int8_t rssi;
/** Advertising data */
const uint8_t *data;
/** Directed advertising address. Valid for BLE_HCI_ADV_RPT_EVTYPE_DIR_IND
* event type (BLE_ADDR_ANY otherwise).
*/
ble_addr_t direct_addr;
};
- event_type:广播PDU类型;
- length_data:广播数据长度;
- addr:广播者地址;
- rssi:信号值;
- data:广播数据;
- direct_addr:直接地址,只有广播类型为BLE_HCI_ADV_RPT_EVTYPE_DIR_IND时才有效。
一般来说广播数据会包含非常多的字段,所以下面要调用ble_hs_adv_parse_fields函数把所有的字段都解析出来,后面就是一个简单的打印操作。第二个处理设备发现完成事件(BLE_GAP_EVENT_DISC_COMPLETE),当结束扫描任务的时候会触发,这里面我的操作就是重新启动一次扫描。
4. 设置设备名。
下面调用ble_svc_gap_device_name_set函数设置自己的设备名。
5. 使能应用。
调用nimble_port_freertos_init函数启动蓝牙应用,其实内部就是创建一个FreeRTOS的任务,所以参数传的就是任务回调函数。任务的相关逻辑ESP-IDF也为我们封装好的,所以里面调一个nimble_port_run函数和nimble_port_freertos_deinit函数即可,前者就是任务主循环,后者就是当任务退出的时候做的去初始化操作。
2.3 测试
编译并烧录,就能看到类似下面的系统log。