ESP32學(xué)習(xí)筆記(33)——BLE GATT客戶端發(fā)現(xiàn)服務(wù)和讀寫特征值

一何恶、背景

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ù)器骨坑。

image

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 *)&notify_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 客戶端示例演練

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末衰伯,一起剝皮案震驚了整個濱河市铡羡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌意鲸,老刑警劉巖烦周,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異怎顾,居然都是意外死亡读慎,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門槐雾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夭委,“玉大人,你說我怎么就攤上這事募强≈昃模” “怎么了崇摄?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長慌烧。 經(jīng)常有香客問我逐抑,道長,這世上最難降的妖魔是什么屹蚊? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任厕氨,我火速辦了婚禮,結(jié)果婚禮上淑翼,老公的妹妹穿的比我還像新娘腐巢。我一直安慰自己品追,他們只是感情好玄括,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肉瓦,像睡著了一般遭京。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上泞莉,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天哪雕,我揣著相機(jī)與錄音,去河邊找鬼鲫趁。 笑死斯嚎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的挨厚。 我是一名探鬼主播堡僻,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼疫剃!你這毒婦竟也來了钉疫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤巢价,失蹤者是張志新(化名)和其女友劉穎牲阁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體壤躲,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡城菊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了碉克。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凌唬。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖棉胀,靈堂內(nèi)的尸體忽然破棺而出法瑟,到底是詐尸還是另有隱情冀膝,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布霎挟,位于F島的核電站窝剖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏酥夭。R本人自食惡果不足惜赐纱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望熬北。 院中可真熱鬧疙描,春花似錦、人聲如沸讶隐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽巫延。三九已至效五,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間炉峰,已是汗流浹背畏妖。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留疼阔,地道東北人戒劫。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像婆廊,于是被迫代替她去往敵國和親迅细。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容