前言

ESP32作为主机进行扫描并连接ble server,参考文章,此文章的内容是需要四个设备,一个作为主机进行搜索其他设备做ble server等待连接,连接之后设置MTU,本文将修改成只需要两个设备进行连接通讯。


实施程序

GATT 客户端实现包括以下步骤:

系统初始化,
扫描配置,
扫描附近的设备,
连接到感兴趣的设备,
注册通知。

一、初始化

A)应用配置文件

应用程序配置文件是一种对功能进行分组的方法。它们的设计使每个应用程序配置文件连接到一个对等设备,这样同一个 ESP32 可以通过为每个设备分配一个应用程序配置文件连接到多个设备,如下图所示。每个应用程序配置文件都会创建一个 GATT 接口以连接到其他设备。应用程序配置文件由 ID 号定义,此示例中有三个配置文件:

#define PROFILE_NUM  1
#define PROFILE_A_APP_ID  0 	
//	#define PROFILE_B_APP_ID  1 			//此文件只需要一个GATT即可
//	#define PROFILE_C_APP_ID  2			//此文件只需要一个GATT即可

该esp_ble_gattc_app_register()函数用于将每个应用程序配置文件注册到 BLE 堆栈。注册操作会生成一个 GATT 接口,该接口作为注册事件中的参数返回。此外,每个应用程序配置文件还由一个结构定义,该结构可用于在堆栈传播新数据时保持应用程序的状态并更新其参数。

代码中的应用程序配置文件是gattc_profile_inst结构的实例。有关详细信息,请参见应用程序配置文件在GATT客户实例演练。

/* 一个基于gatt的配置文件,一个app_id和一个gattc_if,这个数组将存储由ESP_GATTS_REG_EVT返回的gattc_if */
static struct gattc_profile_inst gl_profile_tab[PROFILE_NUM] = {
    [PROFILE_A_APP_ID] = {
        .gattc_cb = gattc_profile_a_event_handler,			// GATT 客户端回调函数
        .gattc_if = ESP_GATT_IF_NONE,       /* 此配置文件的 GATT 客户端接口编号 */
    },
 //   [PROFILE_B_APP_ID] = {
 //       .gattc_cb = gattc_profile_b_event_handler,
 //       .gattc_if = ESP_GATT_IF_NONE,       /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
 //   },
 //   [PROFILE_C_APP_ID] = {
 //       .gattc_cb = gattc_profile_c_event_handler,
 //       .gattc_if = ESP_GATT_IF_NONE,       /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
 //   },

};

配置文件注册触发一个ESP_GATTC_REG_EVT事件,该esp_gattc_cb()事件由事件处理程序处理。处理程序获取事件返回的 GATT 接口并将其存储在配置文件表中,上述的配置都会在esp_gattc_cb函数中调用。

static void esp_gattc_cb(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param)
{
    //ESP_LOGI(GATTC_TAG, "EVT %d, gattc if %d, app_id %d", event, gattc_if, param->reg.app_id);

    /* 如果event是register event,则为每个配置文件存储gattc_if */
    if (event == ESP_GATTC_REG_EVT) {
        if (param->reg.status == ESP_GATT_OK) {
            gl_profile_tab[param->reg.app_id].gattc_if = gattc_if;
        } else {
            ESP_LOGI(GATTC_TAG, "Reg app failed, app_id %04x, status %d",
                    param->reg.app_id,
                    param->reg.status);
            return;
        }
    }

    /* 如果gattc_if等于配置文件A,调用配置文件A cb处理程序,
	 * 所以这里调用每个配置文件的回调 */
    do {
        int idx;
        for (idx = 0; idx < PROFILE_NUM; idx++) {
            if (gattc_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE,不指定某个gatt_if,需要调用每个profile cb函数 */
                    gattc_if == gl_profile_tab[idx].gattc_if) {
                if (gl_profile_tab[idx].gattc_cb) {
                    gl_profile_tab[idx].gattc_cb(event, gattc_if, param);
                }
            }
        }
    } while (0);
}

B)设置扫描参数

这是在注册应用程序配置文件之后完成的,因为注册一旦完成,就会触发一个ESP_GATTC_REG_EVT事件。第一次触发此事件时,GATT 事件处理程序捕获它并为 Profile A 分配一个 GATT 接口,然后将事件转发给 Profile A 的 GATT 事件处理程序(之前注册的gattc_profile_a_event_handler函数)。在此事件处理程序中,该事件用于调用esp_ble_gap_set_scan_params()函数,它接受一个esp_ble_scan_params_t结构实例作为参数。该结构定义为:

typedef struct {
    esp_ble_scan_type_t     scan_type;              /*!< 扫描类型 */
    esp_ble_addr_type_t     own_addr_type;          /*!< 主人地址类型 */
    esp_ble_scan_filter_t   scan_filter_policy;     /*!< 扫描过滤政策 */
    uint16_t                scan_interval;          /*!< 扫描间隔。这被定义为从控制器开始其最后一次 LE 扫描到开始后续 LE 扫描的时间间隔*/
    //范围:0x0004 到 0x4000 
    //默认值:0x0010 (10 ms) 
    //时间 = N * 0.625 毫秒
    //时间范围:2.5 毫秒到 10.24 秒
    uint16_t                scan_window;            /*!</* !< 扫描窗口。LE 扫描的持续时间。LE_Scan_Window 应小于或等于 LE_Scan_Interval */ 
    //范围:0x0004 到 0x4000 //默认值:0x0010(10 毫秒)
    //时间 = N * 0.625 毫秒
    //时间范围:2.5 毫秒到 10240 毫秒
    esp_ble_scan_duplicate_t  scan_duplicate;       /*!< Scan_Duplicates参数控制链路层是否应该被过滤复制的广告报告(BLE_SCAN_DUPLICATE_ENABLE)给主机,或者链路层是否应该生成收到的每个包的广告报告 */
} esp_ble_scan_params_t;

二、扫描

A)开始扫描

一旦设置了扫描参数,ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT就会触发一个事件,该事件由 GAP 事件处理程序处理esp_gap_cb()。此事件用于开始扫描附近的 GATT server:

case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: {
    // 持续时间单位为秒
    uint32_t duration = 30;
    esp_ble_gap_start_scanning(duration);
    break;
}

使用该esp_ble_gap_start_scanning()函数开始扫描,该函数采用表示连续扫描持续时间(以秒为单位)的参数。一旦扫描周期结束,ESP_GAP_SEARCH_INQ_CMPL_EVT就会触发一个事件,如果想要一直扫描就在扫描结束后的事件重新调用开始扫描函数。

B)获取扫描结果并创建连接

扫描结果与ESP_GAP_BLE_SCAN_RESULT_EVT事件一起到达时立即显示,其中包括以下参数:

*/ struct ble_scan_result_evt_param {
         esp_gap_search_evt_t search_evt;            /* !< 搜索事件类型*/ 
         esp_bd_addr_t bda;                          /* !< 搜索到的蓝牙设备地址*/ 
         esp_bt_dev_type_t dev_type;                 /* !< 设备类型*/ 
         esp_ble_addr_type_t ble_addr_type;          /* !< Ble 设备地址类型*/ 
         esp_ble_evt_type_t ble_evt_type;            /* !< Ble 扫描结果事件类型*/
         int rssi;                                   /* !< 搜索设备的 RSSI */
         uint8_t   ble_adv[ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX]; /* !< 收到 EIR */
         int flag;                                  /* !< 广告数据标志位*/
         int num_resps;                              /* !< 扫描结果编号*/
         uint8_t adv_data_len;                       /* !< Adv 数据长度*/
         uint8_t scan_rsp_len;                       /*!< 扫描响应长度*/ 
    } scan_rst;                                      /* !< ESP_GAP_BLE_SCAN_RESULT_EVT 的事件参数*/

此事件还包括子事件列表,如下所示:

// / ESP_GAP_BLE_SCAN_RESULT_EVT 的子事件
typedef  enum {
    ESP_GAP_SEARCH_INQ_RES_EVT = 0 ,      	 	/* !< 对端设备的查询结果。*/ 
    ESP_GAP_SEARCH_INQ_CMPL_EVT = 1 ,       	/* !< 查询完成。*/ 
    ESP_GAP_SEARCH_DISC_RES_EVT = 2 ,       	/* !< 对等设备的发现结果。*/ 
    ESP_GAP_SEARCH_DISC_BLE_RES_EVT = 3 ,       /* !< 对等设备上基于 BLE GATT 的服务的发现结果。*/ 
    ESP_GAP_SEARCH_DISC_CMPL_EVT = 4 ,       	/* !< 发现完成。*/ 
    ESP_GAP_SEARCH_DI_DISC_CMPL_EVT = 5 ,       /* !< 发现完成。*/ 
    ESP_GAP_SEARCH_SEARCH_CANCEL_CMPL_EVT = 6 , /* !< 搜索取消*/ 
} esp_gap_search_evt_t ;

三、连接

A)创建连接

  case ESP_GAP_BLE_SCAN_RESULT_EVT: {
    esp_ble_gap_cb_param_t *scan_result = (esp_ble_gap_cb_param_t *)param;
    switch (scan_result->scan_rst.search_evt) {
        case ESP_GAP_SEARCH_INQ_RES_EVT:
	        esp_log_buffer_hex(GATTC_TAG, scan_result->scan_rst.bda, 6);		// 打印远端设备的地址
	        ESP_LOGI(GATTC_TAG, "searched Adv Data Len %d, Scan Response Len %d", scan_result->scan_rst.adv_data_len, scan_result->scan_rst.scan_rsp_len);
	        adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len);
	        ESP_LOGI(GATTC_TAG, "searched Device Name Len %d", adv_name_len);
	        esp_log_buffer_char(GATTC_TAG, adv_name, adv_name_len);
	        ESP_LOGI(GATTC_TAG, "\n");
	        if (adv_name != NULL) {			//名字不为空
		        if (strlen(remote_device_name) == adv_name_len && strncmp((char *)adv_name, remote_device_name, adv_name_len) == 0) {
		        // 名称长度一致并且是我们要找的名称就停止扫描开始连接。
                ESP_LOGI(GATTC_TAG, "searched device %s\n", remote_device_name);
                if (connect == false) {
                    connect = true;
                    ESP_LOGI(GATTC_TAG, "connect to the remote device.");
                    esp_ble_gap_stop_scanning();
                    esp_ble_gattc_open(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, scan_result->scan_rst.bda, scan_result->scan_rst.ble_addr_type, true);
                }
            }
        }
        break;

首先解析设备名称并与remote_device_name. 如果它等于我们感兴趣的 GATT
服务器的设备名称,则停止扫描。最后,如果远程设备名称与我们上面定义的相同,本地设备将停止扫描并尝试使用
esp_ble_gattc_open()功能。此函数将应用程序配置文件 GATT 接口、远程服务器地址和布尔值作为参数。布尔值用于指示连接是直接完成还是在后台完成(自动连接),此时必须将此布尔值设置为 true
才能建立连接
。请注意,客户端打开了到服务器的虚拟连接。虚拟连接返回连接
ID。虚拟连接是应用程序配置文件和远程服务器之间的连接。由于许多应用程序配置文件可以在一个 ESP32
上运行,因此可能会有许多虚拟连接打开到同一个远程服务器。还有物理连接,它是客户端和服务器之间的实际 BLE
链接
。因此,如果物理连接断开与esp_ble_gap_disconnect()功能,所有其他虚拟连接也将关闭。在这个例子中,每个应用程序配置文件都创建了一个与该esp_ble_gattc_open()函数相同的服务器的虚拟连接,所以当调用关闭函数时,只有来自应用程序配置文件的那个连接被关闭,而如果调用间隙断开函数,两个连接都将被关闭。此外,连接事件会传播到所有配置文件,因为它与物理连接相关,而打开事件仅传播到创建虚拟连接的配置文件。

B)配置 MTU 大小

ATT_MTU 定义为客户端和服务器之间发送的任何数据包的最大大小。当客户端连接到服务器时,它通过交换 MTU 请求和响应协议数据单元 (PDU) 来通知服务器要使用的 MTU 大小。这是在打开连接后完成的。打开连接后,ESP_GATTC_CONNECT_EVT或者在ESP_GATTC_OPEN_EVT(当GATT虚拟连接建立时,事件发生*/)中添加发送MTU设置的函数。
连接打开还会触发一个ESP_GATTC_OPEN_EVT,用于检查连接打开是否成功,否则打印错误并退出。

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);
        conn_device_a = false;
        //start_scan();
        break;
    }
    memcpy(gl_profile_tab[PROFILE_A_APP_ID].remote_bda, p_data->open.remote_bda, 6);
    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_send_mtu_req (gattc_if, p_data->open.conn_id);		//发送之前在main函数中调用esp_ble_gatt_set_local_mtu设置的MTU参数
    if (mtu_ret){
        ESP_LOGE(GATTC_TAG, "config MTU error, error code = %x", mtu_ret);
    }
    break;

蓝牙 4.0 连接的典型 MTU 大小为 23 字节。客户端可以使用esp_ble_gattc_send_mtu_req()函数更改
MUT 的大小,该函数采用 GATT 接口和连接 ID。请求的 MTU 的大小由
定义esp_ble_gatt_set_local_mtu()。然后服务器可以接受或拒绝请求。ESP32 支持最大 517 字节的
MTU 大小,由ESP_GATT_MAX_MTU_SIZEin定义esp_gattc_api.h。在本示例中,MTU 大小设置为 500
字节。如果配置失败,则打印返回的错误。 当 MTU 被交换时,ESP_GATTC_CFG_MTU_EVT会触发 ,在本例中用于打印新的
MTU 大小。

四、发现服务

MTU 配置事件还用于开始发现客户端刚连接到的服务器中可用的服务。为了发现服务,使用了该函数esp_ble_gattc_search_service()。该函数的参数是GATT接口、Application Profile连接ID和客户端感兴趣的服务应用程序的UUID。我们要查找的服务定义为(如果UUID是16位的):

#define REMOTE_SERVICE_UUID  0x00FF//此服务ID是要跟对端设备进行匹配,要和对端设备的服务ID一致
static esp_bt_uuid_t remote_filter_service_uuid = { 
    . len = ESP_UUID_LEN_16, 
    . uuid = {. uuid16 = REMOTE_SERVICE_UUID,}, 
};

如果用户感兴趣的服务应用程序的 UUID 是 128 位,那么下面有一个与处理器架构的 little-endian 存储模式相关的用户注意事项。UUID 的结构体定义为:

// UUID type
typedef struct {
#define ESP_UUID_LEN_16     2
#define ESP_UUID_LEN_32     4
#define ESP_UUID_LEN_128    16
    uint16_t len;							/*!< UUID length, 16bit, 32bit or 128bit */
    union {
        uint16_t    uuid16;                 /*!< 16bit UUID */
        uint32_t    uuid32;                 /*!< 32bit UUID */
        uint8_t     uuid128[ESP_UUID_LEN_128]; /*!< 128bit UUID */
    } uuid;									/*!< UUID */
} __attribute__((packed)) esp_bt_uuid_t;

注意:little-endian存储模式下,如果是16位或32位UUID,可以直接按正常顺序定义服务UUID,但如果服务UUID是128位,则有细微差别。例如用户感兴趣的服务应用的UUID为12345678-a1b2-c3d4-e5f6-9fafd205e457,REMOTE_SERVICE_UUID则应定义为{0x57,0xE4,0x05,0xD2,0xAF,0x9F,0x5,0xDx4 0xC3,0xB2,0xA1,0x78,0x56,0x34,0x12}。
然后发现服务如下:

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. 开始句柄;
        gl_profile_tab[PROFILE_A_APP_ID]。service_end_handle = p_data-> search_res。结束句柄;
        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,包括 uuid 和实例 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()功能。该函数在 gattc 缓存中查找具有给定特征 UUID 的特征。它只是从本地缓存中获取特征,而不是远程设备。在服务器中,可能有多个字符共享相同的 UUID。然而,在我们的 gatt_server 演示中,每个字符都有一个唯一的 UUID,这就是为什么我们只使用第一个字符 in char_elem_result,它是指向服务特征的指针。Count 最初存储了客户端想要查找的特征数量,并会更新为 gattc 缓存中实际找到的特征数量esp_ble_gattc_get_char_by_uuid。

六、注册通知

每次特征值发生变化时,客户端都可以注册以接收来自服务器的通知。在此示例中,我们要注册 UUID 为 0xff01 的特征的通知。获取所有特征后,我们检查接收到的特征的属性,然后使用该

esp_ble_gattc_register_for_notify() //调用此函数是为了注册服务的通知。

函数注册通知。函数参数是 GATT 接口、远程服务器的地址以及我们要注册通知的句柄。

 /*  在我们的 'ESP_GATTS_DEMO' 演示中,每个服务只有一个字符,所以我们首先使用了 …'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");
                    }

                    /* 在我们的'ESP_GATTS_DEMO'演示中,每个字符只有一个描述符,所以我们首先使用'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 / * 客户特征结构* /

要写入的值为“1”以启用通知。我们还通过ESP_GATT_WRITE_TYPE_RSP去请求服务器响应启用通知的请求并ESP_GATT_AUTH_REQ_NONE指示 Write 请求不需要授权。

七、数据交互

在上面注册通知的事件(ESP_GATTC_SEARCH_CMPL_EVT)里调用了esp_ble_gattc_register_for_notify函数,所以来自设备的通知就会直接触发ESP_GATTC_NOTIFY_EVT事件,在此事件中可以将数据通过消息队列发送到其他任务进行处理,如果直接在事件中处理可能会导致蓝牙响应速度变慢,内存溢出等问题。

  case ESP_GATTC_NOTIFY_EVT:
        if (p_data->notify.is_notify){
            ESP_LOGI(GATTC_TAG, "ESP_GATTC_NOTIFY_EVT, receive notify value:");
        }else{
            ESP_LOGI(GATTC_TAG, "ESP_GATTC_NOTIFY_EVT, receive indicate value:");
        }
        esp_log_buffer_hex(GATTC_TAG, p_data->notify.value, p_data->notify.value_len);
        break;

在注册通知完成后会触发完成事件(ESP_GATTC_REG_FOR_NOTIFY_EVT),在此事件中会调用esp_ble_gattc_write_char_descr()函数,此函数用于编写特征描述符值。如果写入成功会触发ESP_GATTC_WRITE_DESCR_EVT事件,此事件触发后如果成功就可以进行发送数据交互了,发送数据使用esp_ble_gattc_write_char函数。发送函数使用方法如下:

uint8_t write_char_data[35];
for (int i = 0; i < sizeof(write_char_data); ++i)
{
    write_char_data[i] = i % 256;
}
esp_ble_gattc_write_char( gattc_if,
                          gl_profile_tab[PROFILE_A_APP_ID].conn_id,
                          gl_profile_tab[PROFILE_A_APP_ID].char_handle,
                          sizeof(write_char_data),
                          write_char_data,
                          ESP_GATT_WRITE_TYPE_RSP,
                          ESP_GATT_AUTH_REQ_NONE);

这个gattc_if参数是Gatt客户端访问接口,如果想要在其他函数中调用就必须将此参数记录在全局或者其他函数能访问到的变量中,在esp_gattc_cb函数中有将此变量保存在gl_profile_tab变量中。所以调用的时候如下即可:

esp_ble_gattc_write_char(   gl_profile_tab[PROFILE_A_APP_ID].gattc_if,
                            gl_profile_tab[PROFILE_A_APP_ID].conn_id,
                            gl_profile_tab[PROFILE_A_APP_ID].char_handle,
                            sizeof(write_char_data),
                            write_char_data,
                            ESP_GATT_WRITE_TYPE_RSP,
                            ESP_GATT_AUTH_REQ_NONE);

八、断开连接

有两种断开方式:
断开物理连接:参数remote_device是要在建立物理连接事件(ESP_GATTC_CONNECT_EVT)中将对端蓝牙设备地址记录下来。

/**
* @brief           This function is to disconnect the physical connection of the peer device
*                  gattc may have multiple virtual GATT server connections when multiple app_id registered.
*                  esp_ble_gattc_close (esp_gatt_if_t gattc_if, uint16_t conn_id) only close one virtual GATT server connection.
*                  if there exist other virtual GATT server connections, it does not disconnect the physical connection.
*                  esp_ble_gap_disconnect(esp_bd_addr_t remote_device) disconnect the physical connection directly.
*
*
*
* @param[in]       remote_device : BD address of the peer device
*
* @return            - ESP_OK : success
*                    - other  : failed
*
*/
esp_err_t esp_ble_gap_disconnect(esp_bd_addr_t remote_device);

断开虚拟连接:

/**
 * @brief           Close the virtual connection to the GATT server. gattc may have multiple virtual GATT server connections when multiple app_id registered,
 *                  this API only close one virtual GATT server connection. if there exist other virtual GATT server connections,
 *                  it does not disconnect the physical connection.
 *                  if you want to disconnect the physical connection directly, you can use esp_ble_gap_disconnect(esp_bd_addr_t remote_device).
 *
 * @param[in]       gattc_if: Gatt client access interface.
 * @param[in]       conn_id: connection ID to be closed.
 *
 * @return
 *                  - ESP_OK: success
 *                  - other: failed
 *
 */
esp_err_t esp_ble_gattc_close (esp_gatt_if_t gattc_if, uint16_t conn_id);

结论

我们已经查看了 ESP32 的 GATT 客户端示例代码。此示例扫描附近的设备并搜索感兴趣的服务器的服务和特征。当找到感兴趣的服务器时,与该服务器建立连接并执行服务搜索。最后,客户端在找到的服务中查找特定特征,如果找到,则获取特征值并注册该特征的通知。这是通过注册一个应用程序配置文件并遵循一系列事件来配置所需的 GAP 和 GATT 参数来完成的。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐