ESP32学习笔记(33)——BLE GATT客户端发现服务和读写特征值

一、背景

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服务器

image

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 *)&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;
    }

该事件用于首先打印通知注册状态以及刚刚注册的通知的服务和特征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 客户端示例演练

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容