一、简介
由于蓝牙主机和从机组网,如果不使用 Mesh 的话,只能组微微网。蓝牙5.0的微微网最大可以连接20个从机。
1.1 连接句柄
在主机与从机发生连接的时候会进行连接句柄的分配。连接句柄的作用是在蓝牙数据进行分组的时候进行设备区分的。连接句柄相当于一个“令牌”,从设备一旦和主设备发生连接,主设备就给从设备分配一个“令牌”。主设备通过这个“令牌”来识别与区分从设备。因此对于连接句柄的分配将是实现一主多从连接,并且进行通信的关键。
1.2 Bluedroid主机架构
在 ESP-IDF 中,使用经过大量修改后的 BLUEDROID 作为蓝牙主机 (Classic BT + BLE)。BLUEDROID 拥有较为完善的功能,⽀持常用的规范和架构设计,同时也较为复杂。经过大量修改后,BLUEDROID 保留了大多数 BTA 层以下的代码,几乎完全删去了 BTIF 层的代码,使用了较为精简的 BTC 层作为内置规范及 Misc 控制层。修改后的 BLUEDROID 及其与控制器之间的关系如下图:
二、API说明
以下 GATT 接口位于 bt/host/bluedroid/api/include/api/esp_gattc_api.h
2.1 esp_ble_gattc_open
2.2 esp_ble_gattc_search_service
2.3 esp_ble_gattc_get_char_by_uuid
2.4 esp_ble_gattc_get_descr_by_char_handle
2.5 esp_ble_gattc_get_attr_count
2.6 esp_ble_gattc_write_char
2.7 esp_ble_gattc_write_char_descr
2.8 esp_ble_gattc_register_for_notify
三、BT控制器和协议栈初始化
使用 esp-idf\examples\bluetooth\bluedroid\ble\gattc_multi_connect 中的例程
.........
//esp_bt_controller_config_t是蓝牙控制器配置结构体,这里使用了一个默认的参数
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
//初始化蓝牙控制器,此函数只能被调用一次,且必须在其他蓝牙功能被调用之前调用
ret = esp_bt_controller_init(&bt_cfg);
if (ret) {
ESP_LOGE(GATTC_TAG, "%s initialize controller failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
//使能蓝牙控制器,mode是蓝牙模式,如果想要动态改变蓝牙模式不能直接调用该函数,
//应该先用disable关闭蓝牙再使用该API来改变蓝牙模式
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) {
ESP_LOGE(GATTC_TAG, "%s enable controller failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
//初始化蓝牙并分配系统资源,它应该被第一个调用
/*
蓝牙栈bluedroid stack包括了BT和BLE使用的基本的define和API
初始化蓝牙栈以后并不能直接使用蓝牙功能,
还需要用FSM管理蓝牙连接情况
*/
ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(GATTC_TAG, "%s init bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
//使能蓝牙栈
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(GATTC_TAG, "%s enable bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
//建立蓝牙的FSM(有限状态机)
//这里使用回调函数来控制每个状态下的响应,需要将其在GATT和GAP层的回调函数注册
/*esp_gattc_cb和esp_gap_cb处理蓝牙栈可能发生的所有情况,达到FSM的效果*/
ret = esp_ble_gap_register_callback(esp_gap_cb);
if (ret){
ESP_LOGE(GATTC_TAG, "gap register error, error code = %x", ret);
return;
}
ret = esp_ble_gattc_register_callback(esp_gattc_cb);
if(ret){
ESP_LOGE(GATTC_TAG, "gattc register error, error code = %x", ret);
return;
}
//下面创建了BLE GATT服务A、B、C,相当于3个独立的应用程序
ret = esp_ble_gattc_app_register(PROFILE_A_APP_ID);
if (ret){
ESP_LOGE(GATTC_TAG, "gattc app register error, error code = %x", ret);
return;
}
ret = esp_ble_gattc_app_register(PROFILE_B_APP_ID);
if (ret){
ESP_LOGE(GATTC_TAG, "gattc app register error, error code = %x", ret);
return;
}
ret = esp_ble_gattc_app_register(PROFILE_C_APP_ID);
if (ret){
ESP_LOGE(GATTC_TAG, "gattc app register error, error code = %x", ret);
return;
}
/*
设置了MTU的值(经过MTU交换,从而设置一个PDU中最大能够交换的数据量)。
例如:主设备发出一个1000字节的MTU请求,但是从设备回应的MTU是500字节,那么今后双方要以较小的值500字节作为以后的MTU。
即主从双方每次在做数据传输时不超过这个最大数据单元。
*/
ret = esp_ble_gatt_set_local_mtu(200);
if (ret){
ESP_LOGE(GATTC_TAG, "set local MTU failed, error code = %x", ret);
}
.......
四、应用程序配置文件
应用程序配置文件是一种对功能进行分组的方法。它们的设计使每个应用程序配置文件连接到一个对等设备,这样同一个 ESP32 可以通过为每个设备分配一个应用程序配置文件连接到多个设备。每个应用程序配置文件都会创建一个 GATT 接口以连接到其他设备。应用程序配置文件由 ID 号定义,在此示例中有三个配置文件:
#define PROFILE_NUM 3
#define PROFILE_A_APP_ID 0
#define PROFILE_B_APP_ID 1
#define PROFILE_C_APP_ID 2
该esp_ble_gattc_app_register()
函数用于将每个应用程序配置文件注册到 BLE 堆栈。注册操作会生成一个 GATT 接口,该接口作为注册事件中的参数返回。此外,每个应用程序配置文件还由一个结构定义,该结构可用于在堆栈传播新数据时保持应用程序的状态并更新其参数。
代码中的应用程序配置文件是gattc_profile_inst
结构的实例。有关详细信息,请参见应用程序配置文件在GATT客户实例演练。
五、扫描
5.1 设置扫描参数
参见第设置扫描参数在GATT客户实例演练。
5.2 开始扫描
参阅Section开始扫描在GATT客户实例演练。
5.3 获取扫描结果
参阅Section获取扫描结果在GATT客户实例演练。
5.4 名称比较
- 首先,从广告数据中提取设备名称并存储在
adv_name
变量中:
adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len);
- 然后,将找到的设备名称与客户端想要连接的服务器名称进行比较。服务器名称在
remote_device_name
数组中定义:
static const char remote_device_name[3][20] = {"ESP_GATTS_DEMO_1", "ESP_GATTS_DEMO_2", “ESP_GATTS_DEMO_3"};
名称比较发生如下:
if (strlen(remote_device_name[0]) == adv_name_len && strncmp((char *)adv_name, remote_device_name[0], adv_name_len) == 0) {
if (find_device_1 == false) {
find_device_1 = true;
ESP_LOGI(GATTC_TAG, "Searched device %s", remote_device_name[0]);
memcpy(gl_profile_tab[PROFILE_A_APP_ID].remote_bda, scan_result->scan_rst.bda, 6);
}
break;
}
else if (strlen(remote_device_name[1]) == adv_name_len && strncmp((char *)adv_name, remote_device_name[1], adv_name_len) == 0) {
if (find_device_2 == false) {
find_device_2 = true;
ESP_LOGI(GATTC_TAG, "Searched device %s", remote_device_name[1]);
memcpy(gl_profile_tab[PROFILE_B_APP_ID].remote_bda, scan_result->scan_rst.bda, 6);
}
}
else if (strlen(remote_device_name[2]) == adv_name_len && strncmp((char *)adv_name, remote_device_name[2], adv_name_len) == 0) {
if (find_device_3 == false) {
find_device_3 = true;
ESP_LOGI(GATTC_TAG, "Searched device %s", remote_device_name[2]);
memcpy(gl_profile_tab[PROFILE_C_APP_ID].remote_bda, scan_result->scan_rst.bda, 6);
}
break;
}
- 如果找到的任何设备名称对应于远程设备名称,
find_device_X
则设置该标志并将远程设备的地址存储在gl_profile_tab
表中。设置所有标志后,客户端将停止扫描并连接到远程设备。
六、连接到远程设备
6.1 连接到第一个远程设备
找到所有设备后,客户端将停止扫描:
if (find_device_1 && find_device_2 && find_device_3 && stop_scan == false {
stop_scan = true;
esp_ble_gap_stop_scanning();
}
扫描停止触发一个ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
事件,该事件用于打开与第一个远程设备的连接。一旦客户端搜索服务、获取特征并在第一个设备上注册通知,第二个和第三个设备就会建立连接。此工作流旨在测试每个远程设备之间的通信是否正常工作,然后再尝试连接到下一个设备,或者如果出现错误,请跳到下一个设备。
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
if (param->scan_stop_cmpl.status != ESP_BT_STATUS_SUCCESS){
ESP_LOGE(GATTC_TAG, "Scan stop failed");
break;
}
ESP_LOGI(GATTC_TAG, "Stop scan successfully");
if (!stop_scan){
ESP_LOGE(GATTC_TAG, "Did not find all devices");
}
if (find_device_1){
esp_ble_gattc_open(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, gl_profile_tab[PROFILE_A_APP_ID].remote_bda, true);
}
break;
-
使用
esp_ble_gattc_open()
GATT 接口、远程设备地址和布尔值的函数打开连接,直接连接设置为 true,后台自动连接设置为 false。为了断开物理连接,使用了 GAP API 函数esp_ble_gap_disconnect()
。连接到第一个设备时,
ESP_GATTC_CONNECT_EVT
会生成一个事件,该事件将转发到所有配置文件。它还触发ESP_GATTC_OPEN_EVT
仅转发到 Profile A 事件处理程序或gattc_profile_a_event_handler()
函数的事件。该事件检查连接是否成功打开,如果没有,则忽略该设备,客户端尝试打开与第二个设备的连接:
case ESP_GATTC_OPEN_EVT:
if (p_data->open.status != ESP_GATT_OK){
//open failed, ignore the first device, connect the second device
ESP_LOGE(GATTC_TAG, "connect device failed, status %d", p_data->open.status);
if (find_device_2){
esp_ble_gattc_open(gl_profile_tab[PROFILE_B_APP_ID].gattc_if, gl_profile_tab[PROFILE_B_APP_ID].remote_bda, true);
}
break;
}
如果连接成功,客户端保存连接 ID,打印远程设备信息并将 MTU 大小配置为 200 字节。
gl_profile_tab[PROFILE_A_APP_ID].conn_id = p_data->open.conn_id;
ESP_LOGI(GATTC_TAG, "ESP_GATTC_OPEN_EVT conn_id %d, if %d, status %d, mtu %d", p_data->open.conn_id, gattc_if, p_data->open.status, p_data->open.mtu);
ESP_LOGI(GATTC_TAG, "REMOTE BDA:");
esp_log_buffer_hex(GATTC_TAG, p_data->open.remote_bda, sizeof(esp_bd_addr_t));
esp_err_t mtu_ret = esp_ble_gattc_config_mtu (gattc_if, p_data->open.conn_id, 200);
if (mtu_ret){
ESP_LOGE(GATTC_TAG, "config MTU error, error code = %x", mtu_ret);
}
break;
- 配置好MTU大小后,
ESP_GATTC_CFG_MTU_EVT
生成一个。此事件用于搜索远程设备上可用的已知服务。搜索是通过使用esp_ble_gattc_search_service()
函数和由以下定义的服务 ID 执行的:
static esp_bt_uuid_t remote_filter_service_uuid = {
.len = ESP_UUID_LEN_16,
.uuid = {.uuid16 = REMOTE_SERVICE_UUID,},
};
然后处理程序搜索服务:
case ESP_GATTC_CFG_MTU_EVT:
if (param->cfg_mtu.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG,"Config mtu failed");
}
ESP_LOGI(GATTC_TAG, "Status %d, MTU %d, conn_id %d", param->cfg_mtu.status, param->cfg_mtu.mtu, param->cfg_mtu.conn_id);
esp_ble_gattc_search_service(gattc_if, param->cfg_mtu.conn_id, &remote_filter_service_uuid);
break;
如果找到该服务,ESP_GATTC_SEARCH_RES_EVT
则会触发一个允许将 设置get_service_1 flag
为 true 的事件。该标志用于打印信息,稍后获取客户端感兴趣的特征。
- 一旦完成对所有服务的搜索,
ESP_GATTC_SEARCH_CMPL_EVT
就会生成一个事件,用于获取刚刚发现的服务的特征。这是通过esp_ble_gattc_get_characteristic()
函数完成的:
case ESP_GATTC_SEARCH_CMPL_EVT:
if (p_data->search_cmpl.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "search service failed, error status = %x", p_data->search_cmpl.status);
break;
}
if (get_service_1){
esp_ble_gattc_get_characteristic(gattc_if, p_data->search_cmpl.conn_id, &remote_service_id, NULL);
}
break;
该esp_ble_gattc_get_characteristic()
函数以 GATT 接口、连接 ID 和远程服务 ID 作为参数。此外,还传递了一个 NULL 值,以表明我们希望所有特征都从第一个开始。如果客户端对特定特征感兴趣,它可以在此字段中传递特征 ID 来指定该特征。一个ESP_GATTC_GET_CHAR_EVT
当特征发现了触发事件。此事件用于打印有关特征的信息。
- 如果特征 ID 与 定义的相同
REMOTE_NOTIFY_CHAR_UUID
,则客户端注册该特征值的通知。 - 最后,使用相同的
esp_ble_gattc_get_characteristic()
函数请求下一个特征,这次,最后一个参数设置为当前特征。这会触发另一个,ESP_GATTC_GET_CHAR_EVT
并重复该过程,直到获得所有特征。
case ESP_GATTC_GET_CHAR_EVT:
if (p_data->get_char.status != ESP_GATT_OK) {
break;
}
ESP_LOGI(GATTC_TAG, "GET CHAR: conn_id = %x, status %d", p_data->get_char.conn_id, p_data->get_char.status);
ESP_LOGI(GATTC_TAG, "GET CHAR: srvc_id = %04x, char_id = %04x", p_data->get_char.srvc_id.id.uuid.uuid.uuid16, p_data->get_char.char_id.uuid.uuid.uuid16);
if (p_data->get_char.char_id.uuid.uuid.uuid16 == REMOTE_NOTIFY_CHAR_UUID) {
ESP_LOGI(GATTC_TAG, "register notify");
esp_ble_gattc_register_for_notify(gattc_if, gl_profile_tab[PROFILE_A_APP_ID].remote_bda, &remote_service_id, &p_data->get_char.char_id);
}
esp_ble_gattc_get_characteristic(gattc_if, p_data->get_char.conn_id, &remote_service_id, &p_data->get_char.char_id);
break;
此时,客户端已从远程设备获取了所有特征,并订阅了有关感兴趣特征的通知。每次客户端注册通知时,ESP_GATTC_REG_FOR_NOTIFY_EVT
都会触发一个事件。在此示例中,此事件设置为使用该esp_ble_gattc_write_char_descr()
函数写入远程设备客户端配置特征 (CCC) 。反过来,此函数用于写入特征描述符。蓝牙规范定义了许多特征描述符,但是,对于本示例,感兴趣的描述符是处理启用通知的描述符,即客户端配置描述符。
6.2 连接到下一个远程设备
- 为了将此描述符作为参数传递,我们首先将其定义为:
static esp_gatt_id_t notify_descr_id = {
.uuid = {
.len = ESP_UUID_LEN_16,
.uuid = {.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG,},
},
.inst_id = 0,
};
其中ESP_GATT_UUID_CHAR_CLIENT_CONFIG
定义为 UUID 以识别 CCC:
#define ESP_GATT_UUID_CHAR_CLIENT_CONFIG 0x2902 /* Client Characteristic Configuration */
要写入的值为“1”以启用通知。该参数ESP_GATT_WRITE_TYPE_RSP
还传递给请求服务器响应写请求,以及ESP_GATT_AUTH_REQ_NONE
指示写请求不需要授权的参数:
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (p_data->reg_for_notify.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "reg notify failed, error status =%x", p_data->reg_for_notify.status);
break;
}
uint16_t notify_en = 1;
ESP_LOGI(GATTC_TAG, "REG FOR NOTIFY: status %d, srvc_id = %04x, char_id = %04x",
p_data->reg_for_notify.status,
p_data->reg_for_notify.srvc_id.id.uuid.uuid.uuid16,
p_data->reg_for_notify.char_id.uuid.uuid.uuid16);
esp_ble_gattc_write_char_descr(gattc_if,
gl_profile_tab[PROFILE_A_APP_ID].conn_id,
&remote_service_id,
&p_data->reg_for_notify.char_id,
¬ify_descr_id,
sizeof(notify_en),
(uint8_t *)¬ify_en,
ESP_GATT_WRITE_TYPE_RSP,
ESP_GATT_AUTH_REQ_NONE);
break;
}
- 启用通知后,远程设备会发送通知,触发
ESP_GATTC_NOTIFY_EVT
客户端上的事件。处理此事件以使用以下esp_ble_gattc_write_char()
函数写回特征:
case ESP_GATTC_NOTIFY_EVT:
ESP_LOGI(GATTC_TAG, "ESP_GATTC_NOTIFY_EVT, Receive notify value:");
esp_log_buffer_hex(GATTC_TAG, p_data->notify.value, p_data->notify.value_len);
//write back
esp_ble_gattc_write_char(gattc_if,
gl_profile_tab[PROFILE_A_APP_ID].conn_id,
&remote_service_id,
&p_data->notify.char_id,
p_data->notify.value_len,
p_data->notify.value,
ESP_GATT_WRITE_TYPE_RSP,
ESP_GATT_AUTH_REQ_NONE);
break;
- 如果写入过程得到确认,则远程设备已成功连接并且通信建立且没有错误。立即,写入过程生成一个
ESP_GATTC_WRITE_CHAR_EVT
事件,在此示例中用于打印信息并连接到第二个远程设备:
case ESP_GATTC_WRITE_CHAR_EVT:
if (p_data->write.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "write char failed, error status = %x", p_data->write.status);
}else{
ESP_LOGI(GATTC_TAG, "write char success");
}
//connect the second device
if (find_device_2){
esp_ble_gattc_open(gl_profile_tab[PROFILE_B_APP_ID].gattc_if, gl_profile_tab[PROFILE_B_APP_ID].remote_bda, true);
}
break;
- 这将触发由 Profile B 事件处理程序处理的打开事件。此处理程序遵循相同的步骤来搜索服务、获取特征、注册通知并作为第一个设备写入特征。第二个设备的序列也以一个
ESP_GATTC_WRITE_CHAR_EVT
事件结束,该事件又用于连接到第三个设备:
case ESP_GATTC_WRITE_CHAR_EVT:
if (p_data->write.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "Write char failed, error status = %x", p_data->write.status);
}else{
ESP_LOGI(GATTC_TAG, "Write char success");
}
//connect the third device
if (find_device_3){
esp_ble_gattc_open(gl_profile_tab[PROFILE_C_APP_ID].gattc_if, gl_profile_tab[PROFILE_C_APP_ID].remote_bda, true);
}
break;
- 第三设备还以与第一和第二设备相同的形式执行相同的配置和通信步骤。成功完成后,所有三个远程设备同时正确连接并无错误地接收通知。
• 由 Leung 写于 2021 年 7 月 14 日
• 参考:GATT 客户端多连接示例演练