一何恶、背景
1.1 GATT協(xié)議
GATT(Generic Attributes Profile)的縮寫寄啼,中文是通用屬性協(xié)議恩够,是已連接的低功耗藍(lán)牙設(shè)備之間進(jìn)行通信的協(xié)議背率。
一旦兩個設(shè)備建立起了連接话瞧,GATT 就開始起作用了嫩与,這也意味著,你必需完成前面的GAP協(xié)議交排。
GATT使用了 ATT(Attribute Protocol)協(xié)議划滋,ATT 協(xié)議把 Service,Characteristic 對應(yīng)的數(shù)據(jù)保存在一個查找表中埃篓,查找表使用 16bit ID 作為每一項(xiàng)的索引处坪。
GATT定義的多層數(shù)據(jù)結(jié)構(gòu)簡要概括起來就是 服務(wù)(Service) 可以包含多個 特征(Characteristic),每個特征包含 屬性(Properties) 和 值(Value)架专,還可以包含多個 描述(Descriptor)同窘。
1.2 屬性協(xié)議(ATT)
屬性協(xié)議層 負(fù)責(zé)數(shù)據(jù)檢索,允許一個設(shè)備暴露一些數(shù)據(jù)塊給其他設(shè)備部脚,其他設(shè)備稱之為“屬性”塞椎。
在ATT環(huán)境中,展示屬性的設(shè)備稱之為服務(wù)器睛低,與它配對的設(shè)備稱之為客戶端案狠。鏈路層的主機(jī)從機(jī)和這里的服務(wù)器、客服端是兩種概念钱雷,主設(shè)備既可以是服務(wù)器骂铁,也可以是客戶端。從設(shè)備毅然罩抗。
1.3 GATT通信中角色
從GATT的角度來看拉庵,處于連接狀態(tài)時的兩個設(shè)備,它們各自充當(dāng)兩種角色中的一種:
服務(wù)端(Server)
包含被GATT客戶端讀取或?qū)懭氲奶卣鲾?shù)據(jù)的設(shè)備套蒂。
客戶端(Client)
從GATT服務(wù)器中讀取數(shù)據(jù)或向GATT服務(wù)器寫入數(shù)據(jù)的設(shè)備钞支。
外圍設(shè)備(從機(jī))作為 GATT 服務(wù)端(Server),它維持了 ATT 的查找表以及 service 和 characteristic 的定義操刀;
客戶端和服務(wù)器的GATT角色獨(dú)立于外圍設(shè)備和中央設(shè)備的GAP角色烁挟。外圍設(shè)備可以是GATT客戶端或GATT服務(wù)器,中心可以是GATT客戶端或GATT服務(wù)器骨坑。
1.4 Bluedroid主機(jī)架構(gòu)
在 ESP-IDF 中撼嗓,使用經(jīng)過大量修改后的 BLUEDROID 作為藍(lán)牙主機(jī) (Classic BT + BLE)。BLUEDROID 擁有較為完善的功能欢唾,?持常用的規(guī)范和架構(gòu)設(shè)計(jì)且警,同時也較為復(fù)雜。經(jīng)過大量修改后礁遣,BLUEDROID 保留了大多數(shù) BTA 層以下的代碼斑芜,幾乎完全刪去了 BTIF 層的代碼,使用了較為精簡的 BTC 層作為內(nèi)置規(guī)范及 Misc 控制層祟霍。修改后的 BLUEDROID 及其與控制器之間的關(guān)系如下圖:
二杏头、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
三盈包、發(fā)現(xiàn)服務(wù)
本篇是關(guān)于GATT客戶端發(fā)現(xiàn)服務(wù)和讀寫特征值,連接服務(wù)端的流程查看 ESP32學(xué)習(xí)筆記(32)——BLE GAP主機(jī)端連接
MTU配置事件還用于開始發(fā)現(xiàn)客戶端剛剛連接到的服務(wù)器中可用的服務(wù)大州。要發(fā)現(xiàn)服務(wù)续语,可以使用esp_ble_gattc_search_service()
函數(shù)垂谢。該函數(shù)的參數(shù)包括GATT接口厦画、應(yīng)用程序配置文件連接ID和客戶端感興趣的應(yīng)用程序UUID。
我們正在尋找的服務(wù)定義為:
#define REMOTE_SERVICE_UUID 0x00FF
static esp_bt_uuid_t remote_filter_service_uuid = {
.len = ESP_UUID_LEN_16,
.uuid = {.uuid16 = REMOTE_SERVICE_UUID,},
};
隨后進(jìn)行查找服務(wù):
esp_ble_gattc_search_service(gattc_if, param->cfg_mtu.conn_id, &remote_filter_service_uuid);
break;
找到的服務(wù)結(jié)果(如果有的話)將從ESP_GATTC_SEARCH_RES_EVT
返回滥朱。對于找到的每個服務(wù)根暑,將觸發(fā)事件來打印所發(fā)現(xiàn)服務(wù)的信息,具體取決于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;
如果客戶端找到了它要查找的服務(wù)排嫌,就將get_server標(biāo)記設(shè)置為true,并保存開始句柄值和結(jié)束句柄值缰犁,稍后將使用它們來獲得該服務(wù)的所有特征淳地。在返回所有服務(wù)結(jié)果之后,將完成搜索并觸發(fā)ESP_GATTC_SEARCH_CMPL_EVT
事件帅容。
四颇象、獲取特征
此示例實(shí)現(xiàn)從預(yù)定義服務(wù)獲取特征數(shù)據(jù)。我們想要獲得特征的服務(wù)UUID是0x00FF并徘,我們感興趣的特征UUID是0xFF01:
#define REMOTE_NOTIFY_CHAR_UUID 0xFF01
使用esp_gatt_srvc_id_t
結(jié)構(gòu)定義服務(wù):
/**
* @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;
在這個例子中遣钳,我們定義了我們想要獲取特征的服務(wù):
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()
函數(shù)從該服務(wù)獲取特征麦乞,該函數(shù)在服務(wù)搜索完成并且找到了它正在尋找的服務(wù)之后蕴茴,在ESP_GATTC_SEARCH_CMPL_EVT
事件中調(diào)用。
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緩存中給定服務(wù)或特征的屬性計(jì)數(shù)姐直。esp_ble_gattc_get_attr_count()
函數(shù)的參數(shù)是GATT接口倦淀,連接ID,esp_gatt_db_attr_type_t
中定義的屬性類型声畏,屬性開始句柄晃听,屬性結(jié)束句柄,特征句柄(該參數(shù)只有類型設(shè)置為ESP_GATT_DB_DESCRIPTOR
時有效)和輸出屬性的數(shù)量一直在gattc緩存中找到和給定的屬性類型砰识。然后我們分配一個緩沖區(qū)來保存esp_ble_gattc_get_char_by_uuid()
函數(shù)的char信息能扒。該函數(shù)使用給定的特征UUID在gattc緩存中查找特征。它只是從本地緩存而不是遠(yuǎn)程設(shè)備中獲取特征辫狼。在服務(wù)端中初斑,可能有多個特征共享相同的UUID,這就是為什么我們只在char_elem_result
中使用第一個char膨处,它是指向服務(wù)特征的指針见秤。count最初存儲客戶端想要查找的特征的數(shù)量砂竖,并使用esp_ble_gattc_get_char_by_uuid
在gattc緩存中實(shí)際找到的特征的數(shù)量進(jìn)行更新。
五鹃答、注冊通知
客戶端可以在每次特征值更改時注冊接收來自服務(wù)器的通知乎澄。在本例中,我們希望注冊由UUID 0xFF01標(biāo)識的特征的通知测摔。在獲得所有特征之后置济,我們檢查接收到的特征的屬性,然后使用esp_ble_gattc_register_for_notify()
函數(shù)來注冊通知锋八。函數(shù)參數(shù)是GATT接口浙于、遠(yuǎn)程服務(wù)器地址和我們想注冊通知的句柄。
…
/* 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堆棧注冊通知挟纱,并觸發(fā)ESP_GATTC_REG_FOR_NOTIFY_EVT
事件羞酗。此事件用于寫入服務(wù)器客戶端配置描述符:
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;
}
該事件用于首先打印通知注冊狀態(tài)以及剛剛注冊的通知的服務(wù)和特征UUID。然后紊服,客戶端使用esp_ble_gattc_write_char_descr()
函數(shù)將數(shù)據(jù)寫入客戶端配置描述符檀轨。藍(lán)牙規(guī)范中定義了許多特征描述符。但是欺嗤,在本例中参萄,我們感興趣的是寫入處理啟用通知的描述符,即客戶端配置描述符剂府。為了將這個描述符作為參數(shù)傳遞拧揽,我們首先將它定義為:
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
來請求服務(wù)器響應(yīng)啟用通知淤袜,并通過ESP_GATT_AUTH_REQ_NONE
來指示寫請求不需要授權(quán)。
? 由 Leung 寫于 2021 年 7 月 13 日
? 參考:GATT 客戶端示例演練