Skip to content

GATT

GATT (Generic Attribute Profile) 和 ATT (Attribute Protocol) 是蓝牙协议栈中的两个重要协议,主要用于在蓝牙设备之间传输数据和交换信息。

ATT 是蓝牙协议栈中的底层协议,它定义了蓝牙设备之间交换数据的规则和格式。ATT 协议使用一种基于属性的数据交换模型,其中每个属性都有一个唯一的标识符和对应的值。

GATT 是建立在 ATT 协议之上的协议,它定义了蓝牙设备之间如何组织和交换数据。GATT 协议使用一种基于属性的数据协议,其中每个属性都有一个唯一的UUID(Universally Unique Identifier)来标识,并且可以包含不同类型的数据,例如传感器数据、配置信息、命令等。

GATT 和 ATT 协议结合在一起,可以让蓝牙设备之间进行灵活地数据交换和通信。例如,一个蓝牙智能手表可以使用 GATT 协议向手机发送心率数据,手机可以使用 GATT 协议向手表发送配置信息,从而实现设备之间的交互和控制。 GATT/ATT 协议在蓝牙传感器、智能家居、健康监测等领域得到了广泛的应用。

  • ATT基本属性

属性句柄(属性表下标), 属性类型(UUID), 属性值, 访问权限, 相当于一个设备的数据库

  • GATT属性

定义了两个角色

服务器GATTS, 客户端GATTC

独立于GAP, 服务器提供数据, 客户端访问数据, 应用的时候外围设备为服务器

一个设备可以同时为服务器和客户端

可以进行交换配置, 发现对方设备服务和特性, 读写特征值, 特征值的通知和指示

image-20240611091558544

串口穿透

初始化

image-20240611091629583

image-20240611091833545

  • 扫描结果查询

SDP指的是Service Discovery Protocol,它是蓝牙技术中用于设备发现和服务查找的协议。SDP允许蓝牙设备发现其附近的其他蓝牙设备,并且查询这些设备上提供的各种服务和功能。通过SDP,一个蓝牙设备可以向其他设备发送查询请求,以获取它们所提供的服务的详细信息。

image-20240611092838141

image-20240611093444755

image-20240611093523396

属性表

image-20240611093749791

一个characteristic包含三种条目:characteristic声明,characteristic的值以及characteristic的描述符(可以有多个描述符):

Characteristic declaration

就是每个characteristic的分界符。解析时一旦遇到characteristicdeclaration,就可以认为接下来又是一个新的characteristic了,同时characteristic declaration还将包含value的读写属性等。

Characteristic value

就是数据的值了,这个比较好理解就不再说了。

Characteristic descriptor

就是数据的额外信息。比如温度的单位是什么,数据是用小数表示还是百分比表示等之类的数据描述信息。CCCD是一种特殊的characteristicdescriptor,当characteristic具有notify或者indicate操作功能时,那么必须为其添加相应CCCD,以方便client来使能或者禁止notify或者indicate功能。

image-20240611094753765

Attribute句柄。Client要访问Server的Attribute,都是通过这个句柄来访问的,也就是说ATT PDU一般都包含handle的值。用户在软件代码添加characteristic的时候,系统会自动按顺序地为相关attribute生成句柄。

Attribute类型。在BLE中我们使用UUID来定义数据的类型,UUID是128 bit的,所以我们有足够的UUID来表达万事万物。

就是数据真正的值,0到512字节长。

Attribute的权限属性,权限属性不会直接在空中包中体现,而是隐含在ATT命令的操作结果中。目前主要有如下四种权限属性:

  • Open,直接可以读或者写
  • No Access,禁止读或者写
  • Authentication,需要配对才能读或者写,由于配对有多种类型,因此authentication又衍生多种子类型,比如带不带MITM,有没有LESC
  • Authorization,跟open一样,不过server返回attribute的值之前需要应用先授权,也就是说应用可以在回调函数里面去修改读或者写的原始值。
  • Signed,签名后才能读或者写,这个用得比较少。

示例代码服务器

初始化

c
void app_main(void)
{
    esp_err_t ret;
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); //低功耗蓝牙参数

    // Initialize NVS 初始化falsh
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK( ret );
    //释放经典蓝牙的内存
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
    //初始化低功耗蓝牙
    ret = esp_bt_controller_init(&bt_cfg);
    //开启蓝牙控制器
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    //初始化bluedroid的协议栈
    ret = esp_bluedroid_init();
    ret = esp_bluedroid_enable();
    //注册事件回调
    esp_ble_gatts_register_callback(gatts_event_handler);
    esp_ble_gap_register_callback(gap_event_handler);
    //注册APP
    esp_ble_gatts_app_register(ESP_SPP_APP_ID);
    spp_task_init();
    return;
}

示例代码客户端

初始化

c
esp_err_t ret;

ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); //标准蓝牙释放
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_bt_controller_init(&bt_cfg); //初始化控制器
esp_bt_controller_enable(ESP_BT_MODE_BLE); //使能
esp_bluedroid_init(); //初始化框架
esp_bluedroid_enable(); //使能框架
ble_client_appRegister(); //申请APP
c
void ble_client_appRegister(void)
{
    esp_err_t status;
    char err_msg[20];

    ESP_LOGI(GATTC_TAG, "register callback");

    //register the scan callback function to the gap module
    //注册GAP的回调
    if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) {
        ESP_LOGE(GATTC_TAG, "gap register error: %s", esp_err_to_name_r(status, err_msg, sizeof(err_msg)));
        return;
    }
    //register the callback function to the gattc module
    //客户端的回调
    if ((status = esp_ble_gattc_register_callback(esp_gattc_cb)) != ESP_OK) {
        ESP_LOGE(GATTC_TAG, "gattc register error: %s", esp_err_to_name_r(status, err_msg, sizeof(err_msg)));
        return;
    }
    //注册GATTC的APP
    esp_ble_gattc_app_register(PROFILE_APP_ID);
    //设置最大回调单元
    esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(200);
    if (local_mtu_ret){
        ESP_LOGE(GATTC_TAG, "set local  MTU failed: %s", esp_err_to_name_r(local_mtu_ret, err_msg, sizeof(err_msg)));
    }
	//一个队列用于spp, 这一个会阻塞等待发送事件
    cmd_reg_queue = xQueueCreate(10, sizeof(uint32_t));
    xTaskCreate(spp_client_reg_task, "spp_client_reg_task", 2048, NULL, 10, NULL);
}

gattc回调函数

c
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", event, gattc_if);

    /* If event is register event, store the gattc_if for each profile */
    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;
        }
    }
    /* If the gattc_if equal to profile A, call profile A cb handler,
     * so here call each profile's callback */
    do {
        //遍历一下所有的回调函数, 实际是调用事件处理函数
        int idx;
        for (idx = 0; idx < PROFILE_NUM; idx++) {
            if (gattc_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
                    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);
}

GAP回调(获取连接)

c
//在这一个里面实现扫描对方, 检测名字之后连接
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    uint8_t *adv_name = NULL;
    uint8_t adv_name_len = 0;
    esp_err_t err;

    switch(event){
    case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: {
        //判断是不是设置参数成功
        if((err = param->scan_param_cmpl.status) != ESP_BT_STATUS_SUCCESS){
            ESP_LOGE(GATTC_TAG, "Scan param set failed: %s", esp_err_to_name(err));
            break;
        }
        //the unit of the duration is second
        uint32_t duration = 0xFFFF;
        ESP_LOGI(GATTC_TAG, "Enable Ble Scan:during time %04" PRIx32 " minutes.",duration);
        //开启扫描, 之后进入ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT
        esp_ble_gap_start_scanning(duration);
        break;
    }
    case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
        //scan start complete event to indicate scan start successfully or failed
        //成功打开了
        if ((err = param->scan_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(GATTC_TAG, "Scan start failed: %s", esp_err_to_name(err));
            break;
        }
        ESP_LOGI(GATTC_TAG, "Scan start successed");
        //之后进入 ESP_GAP_BLE_SCAN_RESULT_EVT
        break;
    case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
        //停止事件
        if ((err = param->scan_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(GATTC_TAG, "Scan stop failed: %s", esp_err_to_name(err));
            break;
        }
        ESP_LOGI(GATTC_TAG, "Scan stop successed");
        if (is_connect == false) {
            ESP_LOGI(GATTC_TAG, "Connect to the remote device.");
            //开启gattc, 使用获取的数据
            //gattc_if获取是在esp_gattc_cb里面的ESP_GATTC_REG_EVT事件的时候设置
            //其余的数据是scan的时候获取的地址和类型
            //这一个函数之后进入gattc回调函数的ESP_GATTC_CONNECT_EVT
            esp_ble_gattc_open(gl_profile_tab[PROFILE_APP_ID].gattc_if, scan_rst.scan_rst.bda,
                               scan_rst.scan_rst.ble_addr_type, true);
        }
        break;
    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 ( strncmp((char *)adv_name, device_name, adv_name_len) == 0) {
                    //记录一下参数在scan_rst这一个全局变量里面
                    memcpy(&(scan_rst), scan_result, sizeof(esp_ble_gap_cb_param_t));
                    //把这一个GAP停止了, 之后进入ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
                    esp_ble_gap_stop_scanning();
                }
            }
            break;
        case ESP_GAP_SEARCH_INQ_CMPL_EVT:
            break;
        default:
            break;
        }
        break;
    }
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
        if ((err = param->adv_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS){
            ESP_LOGE(GATTC_TAG, "Adv stop failed: %s", esp_err_to_name(err));
        }else {
            ESP_LOGI(GATTC_TAG, "Stop adv successfully");
        }
        break;
    default:
        break;
    }
}

GATTC回调函数

c
static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param)
{
    esp_ble_gattc_cb_param_t *p_data = (esp_ble_gattc_cb_param_t *)param;

    switch (event) {
    case ESP_GATTC_REG_EVT:
        ESP_LOGI(GATTC_TAG, "REG EVT, set scan params");
        esp_ble_gap_set_scan_params(&ble_scan_params);
        break;
    case ESP_GATTC_CONNECT_EVT:
        //连接事件
        ESP_LOGI(GATTC_TAG, "ESP_GATTC_CONNECT_EVT: conn_id=%d, gatt_if = %d", 
                 spp_conn_id, gattc_if);
        ESP_LOGI(GATTC_TAG, "REMOTE BDA:");
        esp_log_buffer_hex(GATTC_TAG, gl_profile_tab[PROFILE_APP_ID].remote_bda,
                           sizeof(esp_bd_addr_t));
        //记录一下全局变量
        spp_gattc_if = gattc_if;
        is_connect = true;
        spp_conn_id = p_data->connect.conn_id;
        //记录设备地址
        memcpy(gl_profile_tab[PROFILE_APP_ID].remote_bda, p_data->connect.remote_bda,
               sizeof(esp_bd_addr_t));
        //对服务进行搜索, 进入ESP_GATTC_SEARCH_RES_EVT
        esp_ble_gattc_search_service(spp_gattc_if, spp_conn_id, &spp_service_uuid);
        break;
    case ESP_GATTC_DISCONNECT_EVT:
        ESP_LOGI(GATTC_TAG, "disconnect");
        free_gattc_srv_db();
        esp_ble_gap_start_scanning(SCAN_ALL_THE_TIME);
        break;
    case ESP_GATTC_SEARCH_RES_EVT:
        //搜索事件
        ESP_LOGI(GATTC_TAG, "ESP_GATTC_SEARCH_RES_EVT: start_handle = %d, end_handle = %d, UUID:0x%04x",
                 p_data->search_res.start_handle,
                 p_data->search_res.end_handle,
                 p_data->search_res.srvc_id.uuid.uuid.uuid16);
        //记录一下对方的Handler数据
        spp_srv_start_handle = p_data->search_res.start_handle;
        spp_srv_end_handle = p_data->search_res.end_handle;
        break;
    case ESP_GATTC_SEARCH_CMPL_EVT:
        //搜索完成
        ESP_LOGI(GATTC_TAG, "SEARCH_CMPL: conn_id = %x, status %d", spp_conn_id, p_data->search_cmpl.status);
        //发送以及确定最大传输单元
        //之后进入ESP_GATTC_CFG_MTU_EVT事件
        esp_ble_gattc_send_mtu_req(gattc_if, spp_conn_id);
        break;
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
        //注册通知esp_ble_gattc_register_for_notify这一个函数之后
        //这一个函数是在cmd设置以后, 在spp的任务里面启用的
        ESP_LOGI(GATTC_TAG,"Index = %d,status = %d,handle = %d\n",
                 cmd, p_data->reg_for_notify.status, p_data->reg_for_notify.handle);
        if(p_data->reg_for_notify.status != ESP_GATT_OK){
            ESP_LOGE(GATTC_TAG, "ESP_GATTC_REG_FOR_NOTIFY_EVT, status = %d", 
                     p_data->reg_for_notify.status);
            break;
        }
        uint16_t notify_en = 1;
        //写入特征描述符
        esp_ble_gattc_write_char_descr(
                spp_gattc_if,
                spp_conn_id,
                (db+cmd+1)->attribute_handle,
                sizeof(notify_en),
                (uint8_t *)&notify_en,
                ESP_GATT_WRITE_TYPE_RSP,
                ESP_GATT_AUTH_REQ_NONE);

        break;
    }
    case ESP_GATTC_NOTIFY_EVT:
        //接收通知事件
        ESP_LOGI(GATTC_TAG,"ESP_GATTC_NOTIFY_EVT\n");
        notify_event_handler(p_data);
        break;
    case ESP_GATTC_READ_CHAR_EVT:
        ESP_LOGI(GATTC_TAG,"ESP_GATTC_READ_CHAR_EVT\n");
        break;
    case ESP_GATTC_WRITE_CHAR_EVT:
        ESP_LOGI(GATTC_TAG,"ESP_GATTC_WRITE_CHAR_EVT:status = %d,handle = %d", param->write.status, param->write.handle);
        if(param->write.status != ESP_GATT_OK){
            ESP_LOGE(GATTC_TAG, "ESP_GATTC_WRITE_CHAR_EVT, error status = %d", p_data->write.status);
            break;
        }
        break;
    case ESP_GATTC_PREP_WRITE_EVT:
        break;
    case ESP_GATTC_EXEC_EVT:
        break;
    case ESP_GATTC_WRITE_DESCR_EVT:
        ESP_LOGI(GATTC_TAG,"ESP_GATTC_WRITE_DESCR_EVT: status =%d,handle = %d \n", p_data->write.status, p_data->write.handle);
        if(p_data->write.status != ESP_GATT_OK){
            ESP_LOGE(GATTC_TAG, "ESP_GATTC_WRITE_DESCR_EVT, error status = %d", p_data->write.status);
            break;
        }
        switch(cmd){
        case SPP_IDX_SPP_DATA_NTY_VAL:
            cmd = SPP_IDX_SPP_STATUS_VAL;
            xQueueSend(cmd_reg_queue, &cmd,10/portTICK_PERIOD_MS);
            break;
        case SPP_IDX_SPP_STATUS_VAL:
#ifdef SUPPORT_HEARTBEAT
            cmd = SPP_IDX_SPP_HEARTBEAT_VAL;
            xQueueSend(cmd_reg_queue, &cmd, 10/portTICK_PERIOD_MS);
#endif
            break;
#ifdef SUPPORT_HEARTBEAT
        case SPP_IDX_SPP_HEARTBEAT_VAL:
            xQueueSend(cmd_heartbeat_queue, &cmd, 10/portTICK_PERIOD_MS);
            break;
#endif
        default:
            break;
        };
        break;
    case ESP_GATTC_CFG_MTU_EVT:
        //配置最大传输单元
        if(p_data->cfg_mtu.status != ESP_OK){
            break;
        }
        ESP_LOGI(GATTC_TAG,"+MTU:%d\n", p_data->cfg_mtu.mtu);
        spp_mtu_size = p_data->cfg_mtu.mtu;//记录一下对方的值
        //申请服务特征数据库
        //这一个数据库和对方相比, 差了一个声明
        db = (esp_gattc_db_elem_t *)malloc(count*sizeof(esp_gattc_db_elem_t));
        if(db == NULL){
            ESP_LOGE(GATTC_TAG,"%s:malloc db falied\n",__func__);
            break;
        }
        //获取数据库的数据
        if(esp_ble_gattc_get_db(spp_gattc_if, spp_conn_id, spp_srv_start_handle, spp_srv_end_handle, db, &count) != ESP_GATT_OK){
            ESP_LOGE(GATTC_TAG,"%s:get db falied\n",__func__);
            break;
        }
        if(count != SPP_IDX_NB){
            //判断是不是获取成功了
            ESP_LOGE(GATTC_TAG,"%s:get db count != SPP_IDX_NB, count = %d, SPP_IDX_NB = %d\n",__func__,count,SPP_IDX_NB);
            break;
        }
        //根据数据的类型进行打印
        for(int i = 0;i < SPP_IDX_NB;i++){
            switch((db+i)->type){
            case ESP_GATT_DB_PRIMARY_SERVICE:
                ESP_LOGI(GATTC_TAG,"attr_type = PRIMARY_SERVICE,attribute_handle=%d,start_handle=%d,end_handle=%d,properties=0x%x,uuid=0x%04x\n",\
                        (db+i)->attribute_handle, (db+i)->start_handle, (db+i)->end_handle, (db+i)->properties, (db+i)->uuid.uuid.uuid16);
                break;
            case ESP_GATT_DB_SECONDARY_SERVICE:
                ESP_LOGI(GATTC_TAG,"attr_type = SECONDARY_SERVICE,attribute_handle=%d,start_handle=%d,end_handle=%d,properties=0x%x,uuid=0x%04x\n",\
                        (db+i)->attribute_handle, (db+i)->start_handle, (db+i)->end_handle, (db+i)->properties, (db+i)->uuid.uuid.uuid16);
                break;
            case ESP_GATT_DB_CHARACTERISTIC:
                ESP_LOGI(GATTC_TAG,"attr_type = CHARACTERISTIC,attribute_handle=%d,start_handle=%d,end_handle=%d,properties=0x%x,uuid=0x%04x\n",\
                        (db+i)->attribute_handle, (db+i)->start_handle, (db+i)->end_handle, (db+i)->properties, (db+i)->uuid.uuid.uuid16);
                break;
            case ESP_GATT_DB_DESCRIPTOR:
                ESP_LOGI(GATTC_TAG,"attr_type = DESCRIPTOR,attribute_handle=%d,start_handle=%d,end_handle=%d,properties=0x%x,uuid=0x%04x\n",\
                        (db+i)->attribute_handle, (db+i)->start_handle, (db+i)->end_handle, (db+i)->properties, (db+i)->uuid.uuid.uuid16);
                break;
            case ESP_GATT_DB_INCLUDED_SERVICE:
                ESP_LOGI(GATTC_TAG,"attr_type = INCLUDED_SERVICE,attribute_handle=%d,start_handle=%d,end_handle=%d,properties=0x%x,uuid=0x%04x\n",\
                        (db+i)->attribute_handle, (db+i)->start_handle, (db+i)->end_handle, (db+i)->properties, (db+i)->uuid.uuid.uuid16);
                break;
            case ESP_GATT_DB_ALL:
                ESP_LOGI(GATTC_TAG,"attr_type = ESP_GATT_DB_ALL,attribute_handle=%d,start_handle=%d,end_handle=%d,properties=0x%x,uuid=0x%04x\n",\
                        (db+i)->attribute_handle, (db+i)->start_handle, (db+i)->end_handle, (db+i)->properties, (db+i)->uuid.uuid.uuid16);
                break;
            default:
                break;
            }
        }
        cmd = SPP_IDX_SPP_DATA_NTY_VAL;
        xQueueSend(cmd_reg_queue, &cmd, 10/portTICK_PERIOD_MS);
        break;
    case ESP_GATTC_SRVC_CHG_EVT:
        break;
    default:
        break;
    }
}

属性表

c
/**
 * @brief attribute auto response flag
 */
typedef struct
{
#define ESP_GATT_RSP_BY_APP             0 //手动回复
#define ESP_GATT_AUTO_RSP               1 //自动回复
    /**
     * @brief if auto_rsp set to ESP_GATT_RSP_BY_APP, means the response of 
     Write/Read operation will by replied by application.
     if auto_rsp set to ESP_GATT_AUTO_RSP, means the response of Write/Read 
     operation will be replied by GATT stack automatically.
     */
    uint8_t auto_rsp;
} esp_attr_control_t;

/**
 * @brief Attribute description (used to create database)
 */
 typedef struct
 {
     uint16_t uuid_length;              /*!< UUID length */
     uint8_t  *uuid_p;                  /*!< UUID value */
     uint16_t perm;                     /*!< Attribute permission 权限*/
     uint16_t max_length;               /*!< Maximum length of the element value最大长度*/
     uint16_t length;                   /*!< Current length of the element 当前的长度*/
     uint8_t  *value;                   /*!< Element value array  数据*/
 } esp_attr_desc_t;

/**
 * @brief attribute type added to the gatt server database
 */
typedef struct
{
    esp_attr_control_t      attr_control;                   /*!< The attribute control type */
    esp_attr_desc_t         att_desc;                       /*!< The attribute type */
} esp_gatts_attr_db_t;


///Full HRS Database Description - Used to add attributes into the database
//实际是一个数据库的特征声明
static const esp_gatts_attr_db_t spp_gatt_db[SPP_IDX_NB] =
{
    //SPP -  Service Declaration
    [SPP_IDX_SVC]                      	=
    {
     {ESP_GATT_AUTO_RSP}, //使用自动回复
     {ESP_UUID_LEN_16,    //UUID长度
      (uint8_t *)&primary_service_uuid, //使用的UUID(实际是标记一个类型) 
      ESP_GATT_PERM_READ, 	//有读权限
      sizeof(spp_service_uuid), //两个长度
      sizeof(spp_service_uuid), 
      (uint8_t *)&spp_service_uuid  //数据
     } 
    },

    //SPP -  data receive characteristic Declaration  声明
    [SPP_IDX_SPP_DATA_RECV_CHAR]            =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
    CHAR_DECLARATION_SIZE,CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_write}},

    //SPP -  data receive characteristic Value  值
    [SPP_IDX_SPP_DATA_RECV_VAL]             	=
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&spp_data_receive_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
    SPP_DATA_MAX_LEN,sizeof(spp_data_receive_val), (uint8_t *)spp_data_receive_val}},

    //SPP -  data notify characteristic Declaration
    [SPP_IDX_SPP_DATA_NOTIFY_CHAR]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
    CHAR_DECLARATION_SIZE,CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_notify}},

    //SPP -  data notify characteristic Value
    [SPP_IDX_SPP_DATA_NTY_VAL]   =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&spp_data_notify_uuid, ESP_GATT_PERM_READ,
    SPP_DATA_MAX_LEN, sizeof(spp_data_notify_val), (uint8_t *)spp_data_notify_val}},

    //SPP -  data notify characteristic - Client Characteristic Configuration Descriptor
    [SPP_IDX_SPP_DATA_NTF_CFG]         =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
    sizeof(uint16_t),sizeof(spp_data_notify_ccc), (uint8_t *)spp_data_notify_ccc}},

    //SPP -  command characteristic Declaration
    [SPP_IDX_SPP_COMMAND_CHAR]            =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
    CHAR_DECLARATION_SIZE,CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_write}},

    //SPP -  command characteristic Value
    [SPP_IDX_SPP_COMMAND_VAL]                 =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&spp_command_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
    SPP_CMD_MAX_LEN,sizeof(spp_command_val), (uint8_t *)spp_command_val}},

    //SPP -  status characteristic Declaration
    [SPP_IDX_SPP_STATUS_CHAR]            =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
    CHAR_DECLARATION_SIZE,CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_notify}},

    //SPP -  status characteristic Value
    [SPP_IDX_SPP_STATUS_VAL]                 =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&spp_status_uuid, ESP_GATT_PERM_READ,
    SPP_STATUS_MAX_LEN,sizeof(spp_status_val), (uint8_t *)spp_status_val}},

    //SPP -  status characteristic - Client Characteristic Configuration Descriptor
    [SPP_IDX_SPP_STATUS_CFG]         =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
    sizeof(uint16_t),sizeof(spp_status_ccc), (uint8_t *)spp_status_ccc}},

#ifdef SUPPORT_HEARTBEAT
    //心跳, 实际使用的时候需要实现一个类似的 0x2803是一个没有使用的UUID
    //SPP -  Heart beat characteristic Declaration
    [SPP_IDX_SPP_HEARTBEAT_CHAR]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
    CHAR_DECLARATION_SIZE,CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_write_notify}},

    //SPP -  Heart beat characteristic Value
    //使用0xABF5
    [SPP_IDX_SPP_HEARTBEAT_VAL]   =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&spp_heart_beat_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
    sizeof(spp_heart_beat_val), sizeof(spp_heart_beat_val), (uint8_t *)spp_heart_beat_val}},

    //SPP -  Heart beat characteristic - Client Characteristic Configuration Descriptor
    //客户端配置  0x2902
    [SPP_IDX_SPP_HEARTBEAT_CFG]         =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
    sizeof(uint16_t),sizeof(spp_data_notify_ccc), (uint8_t *)spp_heart_beat_ccc}},
#endif
};


//创建属性列表(重点!!!)
ESP_LOGI(GATTS_TABLE_TAG, "%s %d\n", __func__, __LINE__);
esp_ble_gatts_create_attr_tab(spp_gatt_db, gatts_if, SPP_IDX_NB, SPP_SVC_INST_ID);
  • 建立成功的处理函数
c
case ESP_GATTS_CREAT_ATTR_TAB_EVT:{
    //创建属性表的事件
    ESP_LOGI(GATTS_TABLE_TAG, "The number handle =%x\n",param->add_attr_tab.num_handle);
    if (param->add_attr_tab.status != ESP_GATT_OK){
        ESP_LOGE(GATTS_TABLE_TAG, "Create attribute table failed, error code=0x%x", param->add_attr_tab.status);
    }
    else if (param->add_attr_tab.num_handle != SPP_IDX_NB){
        //判断创建的数据表的数量
        ESP_LOGE(GATTS_TABLE_TAG, "Create attribute table abnormally, num_handle (%d) doesn't equal to HRS_IDX_NB(%d)", param->add_attr_tab.num_handle, SPP_IDX_NB);
    }
    else {
        //记录属性表
        memcpy(spp_handle_table, param->add_attr_tab.handles, sizeof(spp_handle_table));
        //开启服务
        esp_ble_gatts_start_service(spp_handle_table[SPP_IDX_SVC]);
    }
    break;
}

实际使用

服务器

  • 服务器
  1. 在注册表里面添加使用的属性, 包括属性的声明, 值和描述符
  2. 可以使用esp_ble_gatts_get_attr_value获取本地的值
  3. 在ESP_GATTS_WRITE_EVT事件里面获取要写入的值, 使用esp_ble_gatts_set_attr_value进行更新
c
esp_ble_gatts_set_attr_value(p_data->write.handle,p_data->write.len,p_data->write.value)
  1. 完成写入以后会进入这一个ESP_GATTS_SET_ATTR_VAL_EVT事件里面
  2. 可以使用esp_ble_gatts_send_indicate发送通知给客户端

客户端

  • 客户端读取
  1. 在注册表里面添加自己的属性(下标, 描述符)
  2. 连接以后可以使用esp_ble_gattc_read_char函数获取数据库里面的值
  3. 在ESP_GATTC_READ_CHAR_EVT事件里面处理实际的读取
c
//判断是否读取成功
if(p_data->read.status == ESP_GATT_OK)
{
    //判断属性句柄句柄
    if(p_data->read.handle == (db+TEST_LED_VAL)->attribute_handle)//判断一下读取的属性
    {
        printf("read value = %d,len = %d\r\n",*p_data->read.value,p_data->read.value_len);
        //记录通知状态
        memcpy(&read_status,p_data->read.value,p_data->read.value_len);
    }
}
  • 客户端写入
  1. 添加属性表属性
  2. 使用esp_ble_gattc_write_char函数进行写入
  • 注册通知
  1. 使用esp_ble_gattc_register_for_notify注册通知, 绑定一个属性表的值
  2. ESP_GATTC_REG_FOR_NOTIFY_EVT事件里面使用esp_ble_gattc_write_char_descr告诉服务器
  3. ESP_GATTC_NOTIFY_EVT可以捕获到这一个属性表值的通知

API

注册通知

c
esp_err_t esp_ble_gattc_register_for_notify(esp_gatt_if_t gattc_if, esp_bd_addr_t server_bda, uint16_t handle);

注册一个通知, 之后会调用回调函数里面的ESP_GATTC_REG_FOR_NOTIFY_EVT这一个回调函数

发送一个值

c
esp_err_t esp_ble_gattc_write_char_descr(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t handle, uint16_t value_len, uint8_t *value, esp_gatt_write_type_t write_type, esp_gatt_auth_req_t auth_req);

gattc_if: 使用的客户端的类型, 是回调函数的参数

conn_id: 连接的对方的id, 这一个数据是在连接的时候获取spp_conn_id = p_data->connect.conn_id;

handle: 操作的对象的句柄

value_len, value写入的数据

write_type: 需不需要远程响应ESP_GATT_WRITE_TYPE_RSP\_NO_RSP, 示例使用后者

auth_req: 加密认证ESP_GATT_AUTH_REQ_NONE

最大传输单元设置

c
esp_ble_gatt_set_local_mtu(200); //设置本地的最大传输单元
esp_err_t esp_ble_gattc_send_mtu_req(esp_gatt_if_t gattc_if, uint16_t conn_id);//交换数据, 需要在本地设置以后

开启GATT

c
esp_err_t esp_ble_gattc_open(esp_gatt_if_t gattc_if, esp_bd_addr_t remote_bda, esp_ble_addr_type_t remote_addr_type, bool is_direct)

gattc_if: Gatt client access interface.注册的时候获取

remote_bda, remote_addr_type: 扫描的时候获取

is_direct: 直接连接还是后台连接, 使用true

ESP_GATTC_CONNECT_EVT

注册回调函数

c
esp_err_t esp_ble_gattc_register_callback(esp_gattc_cb_t callback); //在这里记录回调函数
esp_err_t esp_ble_gattc_app_register(uint16_t app_id); //实际用于这一个app_id

获取服务

c
esp_err_t esp_ble_gattc_search_service(esp_gatt_if_t gattc_if, uint16_t conn_id, esp_bt_uuid_t *filter_uuid);
  • gattc_if -- [in] Gatt client access interface. gattc申请的id
  • conn_id -- [in] connection ID. 连接id
  • filter_uuid -- [in] a UUID of the service application is interested in. If Null, discover for all services.

获取数据库信息

c
esp_gatt_status_t esp_ble_gattc_get_db(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t start_handle, uint16_t end_handle, esp_gattc_db_elem_t *db, uint16_t *count)

This function is called to get the GATT database. Note: It just get attribute data base from local cache, won't get from remote devices.

db: 传出参数, 获取到的数据库

注册一个通知

c
esp_err_t esp_ble_gattc_register_for_notify(esp_gatt_if_t gattc_if, esp_bd_addr_t server_bda, uint16_t handle);

服务器设置属性表的一个值

c
esp_err_t esp_ble_gatts_set_attr_value(uint16_t attr_handle, uint16_t length, const uint8_t *value);

服务器使用, 之后会调用ESP_GATTS_SET_ATTR_VAL_EVT这一个事件的处理

设置的这一个属性值可以使用

c
esp_gatt_status_t esp_ble_gatts_get_attr_value(uint16_t attr_handle, uint16_t *length, const uint8_t **value);

进行获取

attr_handle属性句柄

客户端读取

c
esp_err_t esp_ble_gattc_read_char(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t handle, esp_gatt_auth_req_t auth_req);
c
//读取特征值完成事件
case ESP_GATTC_READ_CHAR_EVT:
ESP_LOGI(GATTC_TAG,"ESP_GATTC_READ_CHAR_EVT\n");

#ifdef SUPPORT_LED
//判断是否读取成功
if(p_data->read.status == ESP_GATT_OK)
{
    //判断属性句柄句柄
    if(p_data->read.handle == (db+TEST_LED_VAL)->attribute_handle)
    {
        printf("read value = %d,len = %d\r\n",*p_data->read.value,p_data->read.value_len);
        //记录通知状态
        memcpy(&read_status,p_data->read.value,p_data->read.value_len);
    }

}
else
    printf("read faild, status = %d\r\n",p_data->read.status);

#endif
break;

服务器发通知

c
esp_err_t esp_ble_gatts_send_indicate(esp_gatt_if_t gatts_if, uint16_t conn_id, uint16_t attr_handle, uint16_t value_len, uint8_t *value, bool need_confirm);

发送一指示或者通知给客户端

need_confirm: 1指示 0通知

之后客户端会进入ESP_GATTC_NOTIFY_EVT这一个事件里面(需要注册这一个句柄的通知)

实现通知的时候, 需要自己手动调用

客户端注册通知

c
esp_err_t esp_ble_gattc_register_for_notify(esp_gatt_if_t gattc_if, esp_bd_addr_t server_bda, uint16_t handle);

给某一个句柄注册一个通知, 之后就可以获这一个句柄的通知, 这一个注册的时候不会对服务端进行通知, 需要在ESP_GATTC_REG_FOR_NOTIFY_EVT里面自己通知