一、背景
1.1 GATT协议
GATT(Generic Attributes Profile)的缩写,中文是通用属性协议,是已连接的低功耗蓝牙设备之间进行通信的协议。
一旦两个设备建立起了连接,GATT 就开始起作用了,这也意味着,你必需完成前面的GAP协议。
GATT使用了 ATT(Attribute Protocol)协议,ATT 协议把 Service,Characteristic 对应的数据保存在一个查找表中,查找表使用 16bit ID 作为每一项的索引。
GATT定义的多层数据结构简要概括起来就是 服务(Service) 可以包含多个 特征(Characteristic),每个特征包含 属性(Properties) 和 值(Value),还可以包含多个 描述(Descriptor)。
1.2 属性协议(ATT)
属性协议层 负责数据检索,允许一个设备暴露一些数据块给其他设备,其他设备称之为“属性”。
在ATT环境中,展示属性的设备称之为服务器,与它配对的设备称之为客户端。链路层的主机从机和这里的服务器、客服端是两种概念,主设备既可以是服务器,也可以是客户端。从设备毅然。
1.3 GATT通信中角色
从GATT的角度来看,处于连接状态时的两个设备,它们各自充当两种角色中的一种:
服务端(Server)
包含被GATT客户端读取或写入的特征数据的设备。
客户端(Client)
从GATT服务器中读取数据或向GATT服务器写入数据的设备。
外围设备(从机)作为 GATT 服务端(Server),它维持了 ATT 的查找表以及 service 和 characteristic 的定义;
客户端和服务器的GATT角色独立于外围设备和*设备的GAP角色。外围设备可以是GATT客户端或GATT服务器,中心可以是GATT客户端或GATT服务器。
1.4 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_search_service
2.2 esp_ble_gattc_get_char_by_uuid
2.3 esp_ble_gattc_get_descr_by_char_handle
2.4 esp_ble_gattc_get_attr_count
2.5 esp_ble_gattc_write_char
2.6 esp_ble_gattc_write_char_descr
2.7 esp_ble_gattc_register_for_notify
三、发现服务
本篇是关于GATT客户端发现服务和读写特征值,连接服务端的流程查看 ESP32学习笔记(32)——BLE GAP主机端连接
MTU配置事件还用于开始发现客户端刚刚连接到的服务器中可用的服务。要发现服务,可以使用esp_ble_gattc_search_service()
函数。该函数的参数包括GATT接口、应用程序配置文件连接ID和客户端感兴趣的应用程序UUID。
我们正在寻找的服务定义为:
#define REMOTE_SERVICE_UUID 0x00FF
static esp_bt_uuid_t remote_filter_service_uuid = {
.len = ESP_UUID_LEN_16,
.uuid = {.uuid16 = REMOTE_SERVICE_UUID,},
};
随后进行查找服务:
esp_ble_gattc_search_service(gattc_if, param->cfg_mtu.conn_id, &remote_filter_service_uuid);
break;
找到的服务结果(如果有的话)将从ESP_GATTC_SEARCH_RES_EVT
返回。对于找到的每个服务,将触发事件来打印所发现服务的信息,具体取决于UUID的大小:
case ESP_GATTC_SEARCH_RES_EVT: {
esp_gatt_srvc_id_t *srvc_id = &p_data->search_res.srvc_id;
conn_id = p_data->search_res.conn_id;
if (srvc_id->id.uuid.len == ESP_UUID_LEN_16 && srvc_id->id.uuid.uuid.uuid16 ==
REMOTE_SERVICE_UUID) {
get_server = true;
gl_profile_tab[PROFILE_A_APP_ID].service_start_handle = p_data->search_res.start_handle;
gl_profile_tab[PROFILE_A_APP_ID].service_end_handle = p_data->search_res.end_handle;
ESP_LOGI(GATTC_TAG, "UUID16: %x", srvc_id->id.uuid.uuid.uuid16);
}
break;
如果客户端找到了它要查找的服务,就将get_server标记设置为true,并保存开始句柄值和结束句柄值,稍后将使用它们来获得该服务的所有特征。在返回所有服务结果之后,将完成搜索并触发ESP_GATTC_SEARCH_CMPL_EVT
事件。
四、获取特征
此示例实现从预定义服务获取特征数据。我们想要获得特征的服务UUID是0x00FF,我们感兴趣的特征UUID是0xFF01:
#define REMOTE_NOTIFY_CHAR_UUID 0xFF01
使用esp_gatt_srvc_id_t
结构定义服务:
/**
* @brief Gatt id, include uuid and instance id
*/
typedef struct {
esp_bt_uuid_t uuid; /*!< UUID */
uint8_t inst_id; /*!< Instance id */
} __attribute__((packed)) esp_gatt_id_t;
在这个例子中,我们定义了我们想要获取特征的服务:
static esp_gatt_srvc_id_t remote_service_id = {
.id = {
.uuid = {
.len = ESP_UUID_LEN_16,
.uuid = {.uuid16 = REMOTE_SERVICE_UUID,},
},
.inst_id = 0,
},
.is_primary = true,
};
定义之后,我们可以使用esp_ble_gattc_get_characteristic()
函数从该服务获取特征,该函数在服务搜索完成并且找到了它正在寻找的服务之后,在ESP_GATTC_SEARCH_CMPL_EVT
事件中调用。
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;
}
conn_id = p_data->search_cmpl.conn_id;
if (get_server){
uint16_t count = 0;
esp_gatt_status_t status = esp_ble_gattc_get_attr_count( gattc_if,
p_data->search_cmpl.conn_id,ESP_GATT_DB_CHARACTERISTIC, gl_profile_tab[PROFILE_A_APP_ID].service_start_handle, gl_profile_tab[PROFILE_A_APP_ID].service_end_handle,
INVALID_HANDLE,
&count);
if (status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_attr_count error");
}
if (count > 0){
char_elem_result = (esp_gattc_char_elem_t*)malloc
(sizeof(esp_gattc_char_elem_t) * count);
if (!char_elem_result){
ESP_LOGE(GATTC_TAG, "gattc no mem");
}else{
status = esp_ble_gattc_get_char_by_uuid( gattc_if,
p_data->search_cmpl.conn_id,
gl_profile_tab[PROFILE_A_APP_ID].service_start_handle,
gl_profile_tab[PROFILE_A_APP_ID].service_end_handle,
remote_filter_char_uuid,
char_elem_result,
&count);
if (status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_char_by_uuid error");
}
/* Every service have only one char in our 'ESP_GATTS_DEMO' demo,
so we used first 'char_elem_result' */
if (count > 0 && (char_elem_result[0].properties
&ESP_GATT_CHAR_PROP_BIT_NOTIFY)){
gl_profile_tab[PROFILE_A_APP_ID].char_handle =
char_elem_result[0].char_handle;
esp_ble_gattc_register_for_notify (gattc_if,
gl_profile_tab[PROFILE_A_APP_ID].remote_bda,
char_elem_result[0].char_handle);
}
}
/* free char_elem_result */
free(char_elem_result);
}else{
ESP_LOGE(GATTC_TAG, "no char found");
} }
break;
esp_ble_gattc_get_attr_count()
获取gattc缓存中给定服务或特征的属性计数。esp_ble_gattc_get_attr_count()
函数的参数是GATT接口,连接ID,esp_gatt_db_attr_type_t
中定义的属性类型,属性开始句柄,属性结束句柄,特征句柄(该参数只有类型设置为ESP_GATT_DB_DESCRIPTOR
时有效)和输出属性的数量一直在gattc缓存中找到和给定的属性类型。然后我们分配一个缓冲区来保存esp_ble_gattc_get_char_by_uuid()
函数的char信息。该函数使用给定的特征UUID在gattc缓存中查找特征。它只是从本地缓存而不是远程设备中获取特征。在服务端中,可能有多个特征共享相同的UUID,这就是为什么我们只在char_elem_result
中使用第一个char,它是指向服务特征的指针。count最初存储客户端想要查找的特征的数量,并使用esp_ble_gattc_get_char_by_uuid
在gattc缓存中实际找到的特征的数量进行更新。
五、注册通知
客户端可以在每次特征值更改时注册接收来自服务器的通知。在本例中,我们希望注册由UUID 0xFF01标识的特征的通知。在获得所有特征之后,我们检查接收到的特征的属性,然后使用esp_ble_gattc_register_for_notify()
函数来注册通知。函数参数是GATT接口、远程服务器地址和我们想注册通知的句柄。
…
/* Every service have only one char in our 'ESP_GATTS_DEMO' demo, so we used first 'char_elem_result' */
if(count > 0 && (char_elem_result[0].properties & ESP_GATT_CHAR_PROP_BIT_NOTIFY)){
gl_profile_tab[PROFILE_A_APP_ID].char_handle = char_elem_result[0].char_handle;
esp_ble_gattc_register_for_notify (gattc_if, gl_profile_tab[PROFILE_A_APP_ID].remote_bda,
char_elem_result[0].char_handle);
}
…
这个过程向BLE堆栈注册通知,并触发ESP_GATTC_REG_FOR_NOTIFY_EVT
事件。此事件用于写入服务器客户端配置描述符:
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
ESP_LOGI(GATTC_TAG, "ESP_GATTC_REG_FOR_NOTIFY_EVT");
if (p_data->reg_for_notify.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "REG FOR NOTIFY failed: error status = %d", p_data->reg_for_notify.status);
}else{
uint16_t count = 0;
uint16_t notify_en = 1;
esp_gatt_status_t ret_status = esp_ble_gattc_get_attr_count( gattc_if, gl_profile_tab[PROFILE_A_APP_ID].conn_id,
ESP_GATT_DB_DESCRIPTOR,
gl_profile_tab[PROFILE_A_APP_ID].service_start_handle,
gl_profile_tab[PROFILE_A_APP_ID].service_end_handle,
gl_profile_tab[PROFILE_A_APP_ID].char_handle, &count);
if (ret_status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_attr_count error");
}
if (count > 0){
descr_elem_result = malloc(sizeof(esp_gattc_descr_elem_t) * count);
if (!descr_elem_result){
ESP_LOGE(GATTC_TAG, "malloc error, gattc no mem");
}else{
ret_status = esp_ble_gattc_get_descr_by_char_handle(
gattc_if,
gl_profile_tab[PROFILE_A_APP_ID].conn_id,
p_data->reg_for_notify.handle,
notify_descr_uuid,
descr_elem_result,&count);
if (ret_status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_descr_by_char_handle
error");
}
/* Every char has only one descriptor in our 'ESP_GATTS_DEMO' demo, so we used first 'descr_elem_result' */
if (count > 0 && descr_elem_result[0].uuid.len == ESP_UUID_LEN_16 && descr_elem_result[0].uuid.uuid.uuid16 == ESP_GATT_UUID_CHAR_CLIENT_CONFIG){
ret_status = esp_ble_gattc_write_char_descr( gattc_if,
gl_profile_tab[PROFILE_A_APP_ID].conn_id,
descr_elem_result[0].handle,
sizeof(notify_en),
(Uint8 *)¬ify_en,
ESP_GATT_WRITE_TYPE_RSP,
ESP_GATT_AUTH_REQ_NONE);
}
if (ret_status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_write_char_descr error");
}
/* free descr_elem_result */
free(descr_elem_result);
}
}
else{
ESP_LOGE(GATTC_TAG, "decsr not found");
}
}
break;
}
该事件用于首先打印通知注册状态以及刚刚注册的通知的服务和特征UUID。然后,客户端使用esp_ble_gattc_write_char_descr()
函数将数据写入客户端配置描述符。蓝牙规范中定义了许多特征描述符。但是,在本例中,我们感兴趣的是写入处理启用通知的描述符,即客户端配置描述符。为了将这个描述符作为参数传递,我们首先将它定义为:
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定义的,以识别特征客户端配置:
#define ESP_GATT_UUID_CHAR_CLIENT_CONFIG 0x2902 /* Client Characteristic Configuration */
要写入的值为“1”以启用通知。我们还通过ESP_GATT_WRITE_TYPE_RSP
来请求服务器响应启用通知,并通过ESP_GATT_AUTH_REQ_NONE
来指示写请求不需要授权。
• 由 Leung 写于 2021 年 7 月 13 日
• 参考:GATT 客户端示例演练