搞定蓝牙——第四章(GATT协议)

  • 文章下面用的英文表示:
    server和client:服务端和客户端
    char.:characteristic缩写,特征
    Attribute:属性
    ATT:Attribute Protocol缩写

原理介绍

GATT是蓝牙协议栈的一种协议,它定义了ble设备数据通讯的方法(设备角色、数据格式、服务发现、安全加密等等),也就是说两个ble设备是通讯的规范就是GATT协议。GAP也是一种协议,但是它是规范通讯之前的广播、连接等。

层次结构

GATT的层次结构是这样的。
在这里插入图片描述在这里插入图片描述
一个Profile包含多个server(与server和client中的server不是一个意思),一个server包含多个char.,profile只是一个定义,表示某个功能,例如测心率、测血氧等,server是一个服务,也表示某个功能,不传递实际的数据。char.是一个特征,可以理解为数据存放的地方。char.里面包含了属性、值和描述符,属性定义了访问权限,值就是一个要传递的数据,描述符描述了这个数据、例如单位。所以只有server和char.是实际运行的组件。

server和client端

Server端和client端,这个也是GATT定义的,一般server端提供服务,要传输的数据也在服务里面,client读取server的服务,也就是获取数据。有的服务可以接收client的数据,例如通知和指示。

Attribute

因为GATT是一种通讯规范,那通讯的数据是怎么样的呢?你想哈,好多地方都用到了蓝牙技术,但是应用场景差异有些很大,所以,这种数据格式必须能满足所有这些应用场景。于是,SIG(蓝牙技术联盟)就定义了一个统一的数据格式,称为Attribute(属性),也是ble传输最小单元了,每次最少发一个数据就是一个Attribute。
Attribute的结构是怎么样的呢。看图。
在这里插入图片描述
一个Attribute有四种类型的数据,一个一个展开说明。
Handle:因为ble发送数据都是一个一个Attribute发送的,所以需要加一个表示符来表示这个Attribute,可以看作为这个Attribute的名字,这个handle就是这个东西。
Type:表示Attribute的类型,使用16bit的UUID来表示,例如0x2800表示这个一个server声明的Attribute,更多看下图。
在这里插入图片描述
Value:表示这个Attribute的值,这个值有两种情况,如果从上面的Type中发现这是一个server声明的Attribute或者是char.声明的Attribute,那么这是一个UUID,这个可以自己定义,也可以用通用的,如果是其他,那么就是一个数据,例如心率、血氧。
Permissions:权限,也就是运行client对于这个Attribute的Value操作权限,因为server声明和char.声明是UUDI,所以该值为只读。

Ble是按照Attribute的格式来定义一个一个的数据的,在server端需要配置这一些Attribute,也就是Attribute表,这个表必须包含server声明、char.声明和char.值。
例如看看ESP32中的Attribute表,对于应用而言,维护这张表非常重要
在这里插入图片描述

ESP32部分

结构

参考官方手册蓝牙技术说明,链接:
https://www.espressif.com/sites/default/files/documentation/esp32_bluetooth_architecture_cn.pdf
有一张层次结构图
在这里插入图片描述

如下图所示,用户只关心USER APP和USER TASK这两部分,从图中可以看出GATT和GAP两部分是和USER APP有交互的,这就是通过两个回调函数来实现的,我们关系回调函数的实现就行了,至于回调函数什么时候被调用,那就是ESP底层写好的库实现的了。
在这里插入图片描述

初始化

直接看官方demo,打开路径,
在这里插入图片描述
拉到最底下找到app_main函数,看到初始化内容

esp_err_t ret;
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();

    // Initialize NVS
    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);
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s\n", __func__, esp_err_to_name(ret));
        return;
    }

    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s\n", __func__, esp_err_to_name(ret));
        return;
    }

    ESP_LOGI(GATTS_TABLE_TAG, "%s init bluetooth\n", __func__);
    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s init bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
        return;
    }
    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
        return;
    }

这里初始化nvs_flash、然后释放经典蓝牙内容(当前学习ble,不使用经典蓝牙)、然后初始蓝牙控制器、使能蓝牙控制器、初始化bluedroid、使能bluedroid。

esp_ble_gatts_register_callback(gatts_event_handler);
esp_ble_gap_register_callback(gap_event_handler);

注册两个回调函数,GAP和GATT服务端回调函数

esp_ble_gatts_app_register(ESP_SPP_APP_ID);

注册APP,这里可以注册多个APP
最后开启任务

spp_task_init();

所以,ble的控制都是在上面注册的两个回调函数里的。

两个回调函数

这个是GAP的回调函数

static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    esp_err_t err;
    ESP_LOGE(GATTS_TABLE_TAG, "GAP_EVT, event %d\n", event);

    switch (event) {
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
        esp_ble_gap_start_advertising(&spp_adv_params);
        break;
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        //advertising start complete event to indicate advertising start successfully or failed
        if((err = param->adv_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(GATTS_TABLE_TAG, "Advertising start failed: %s\n", esp_err_to_name(err));
        }
        break;
    default:
        break;
    }
}
这个是GATT服务端的回调函数

static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
    ESP_LOGI(GATTS_TABLE_TAG, "EVT %d, gatts if %d\n", event, gatts_if);

    /* If event is register event, store the gatts_if for each profile */
    if (event == ESP_GATTS_REG_EVT) {
        if (param->reg.status == ESP_GATT_OK) {
            spp_profile_tab[SPP_PROFILE_APP_IDX].gatts_if = gatts_if;
        } else {
            ESP_LOGI(GATTS_TABLE_TAG, "Reg app failed, app_id %04x, status %d\n",param->reg.app_id, param->reg.status);
            return;
        }
    }

    do {
        int idx;
        for (idx = 0; idx < SPP_PROFILE_NUM; idx++) {
            if (gatts_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
                    gatts_if == spp_profile_tab[idx].gatts_if) {
                if (spp_profile_tab[idx].gatts_cb) {
                    spp_profile_tab[idx].gatts_cb(event, gatts_if, param);
                }
            }
        }
    } while (0);
}

GATTS回调函数有两部分,if (event == ESP_GATTS_REG_EVT){…} 和do {…}while(0)。
在main注册会使用了esp_ble_gatts_app_register(ESP_SPP_APP_ID);会进入第一个if语句,修改profile表的gatts_if(这个是一个标识,表示这个是APP,注册不同APP会产生不同gatts_if),然后进入do {…}while(0),进行实际的操作,也就是执行gatts_cb的函数。

static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
    esp_ble_gatts_cb_param_t *p_data = (esp_ble_gatts_cb_param_t *) param;
    uint8_t res = 0xff;

    ESP_LOGI(GATTS_TABLE_TAG, "event = %x\n",event);
    switch (event) {
        case ESP_GATTS_REG_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "%s %d\n", __func__, __LINE__);
            esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);

            ESP_LOGI(GATTS_TABLE_TAG, "%s %d\n", __func__, __LINE__);
            esp_ble_gap_config_adv_data_raw((uint8_t *)spp_adv_data, sizeof(spp_adv_data));

            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);
        break;
        case ESP_GATTS_READ_EVT:
            res = find_char_and_desr_index(p_data->read.handle);
            if(res == SPP_IDX_SPP_STATUS_VAL){
                //TODO:client read the status characteristic
            }
         break;
        case ESP_GATTS_WRITE_EVT: {
            res = find_char_and_desr_index(p_data->write.handle);
            if(p_data->write.is_prep == false){
                ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_WRITE_EVT : handle = %d\n", res);
                if(res == SPP_IDX_SPP_COMMAND_VAL){
                    uint8_t * spp_cmd_buff = NULL;
                    spp_cmd_buff = (uint8_t *)malloc((spp_mtu_size - 3) * sizeof(uint8_t));
                    if(spp_cmd_buff == NULL){
                        ESP_LOGE(GATTS_TABLE_TAG, "%s malloc failed\n", __func__);
                        break;
                    }
                    memset(spp_cmd_buff,0x0,(spp_mtu_size - 3));
                    memcpy(spp_cmd_buff,p_data->write.value,p_data->write.len);
                    xQueueSend(cmd_cmd_queue,&spp_cmd_buff,10/portTICK_PERIOD_MS);
                }else if(res == SPP_IDX_SPP_DATA_NTF_CFG){
                    if((p_data->write.len == 2)&&(p_data->write.value[0] == 0x01)&&(p_data->write.value[1] == 0x00)){
                        enable_data_ntf = true;
                    }else if((p_data->write.len == 2)&&(p_data->write.value[0] == 0x00)&&(p_data->write.value[1] == 0x00)){
                        enable_data_ntf = false;
                    }
                }
#ifdef SUPPORT_HEARTBEAT
                else if(res == SPP_IDX_SPP_HEARTBEAT_CFG){
                    if((p_data->write.len == 2)&&(p_data->write.value[0] == 0x01)&&(p_data->write.value[1] == 0x00)){
                        enable_heart_ntf = true;
                    }else if((p_data->write.len == 2)&&(p_data->write.value[0] == 0x00)&&(p_data->write.value[1] == 0x00)){
                        enable_heart_ntf = false;
                    }
                }else if(res == SPP_IDX_SPP_HEARTBEAT_VAL){
                    if((p_data->write.len == sizeof(heartbeat_s))&&(memcmp(heartbeat_s,p_data->write.value,sizeof(heartbeat_s)) == 0)){
                        heartbeat_count_num = 0;
                    }
                }
#endif
                else if(res == SPP_IDX_SPP_DATA_RECV_VAL){
#ifdef SPP_DEBUG_MODE
                    esp_log_buffer_char(GATTS_TABLE_TAG,(char *)(p_data->write.value),p_data->write.len);
#else
                    uart_write_bytes(UART_NUM_0, (char *)(p_data->write.value), p_data->write.len);
#endif
                }else{
                    //TODO:
                }
            }else if((p_data->write.is_prep == true)&&(res == SPP_IDX_SPP_DATA_RECV_VAL)){
                ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_PREP_WRITE_EVT : handle = %d\n", res);
                store_wr_buffer(p_data);
            }
            break;
        }
        case ESP_GATTS_EXEC_WRITE_EVT:{
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_EXEC_WRITE_EVT\n");
            if(p_data->exec_write.exec_write_flag){
                print_write_buffer();
                free_write_buffer();
            }
            break;
        }
        case ESP_GATTS_MTU_EVT:
            spp_mtu_size = p_data->mtu.mtu;
            break;
        case ESP_GATTS_CONF_EVT:
            break;
        case ESP_GATTS_UNREG_EVT:
            break;
        case ESP_GATTS_DELETE_EVT:
            break;
        case ESP_GATTS_START_EVT:
            break;
        case ESP_GATTS_STOP_EVT:
            break;
        case ESP_GATTS_CONNECT_EVT:
            spp_conn_id = p_data->connect.conn_id;
            spp_gatts_if = gatts_if;
            is_connected = true;
            memcpy(&spp_remote_bda,&p_data->connect.remote_bda,sizeof(esp_bd_addr_t));
#ifdef SUPPORT_HEARTBEAT
            uint16_t cmd = 0;
            xQueueSend(cmd_heartbeat_queue,&cmd,10/portTICK_PERIOD_MS);
#endif
            break;
        case ESP_GATTS_DISCONNECT_EVT:
            is_connected = false;
            enable_data_ntf = false;
#ifdef SUPPORT_HEARTBEAT
            enable_heart_ntf = false;
            heartbeat_count_num = 0;
#endif
            esp_ble_gap_start_advertising(&spp_adv_params);
            break;
        case ESP_GATTS_OPEN_EVT:
            break;
        case ESP_GATTS_CANCEL_OPEN_EVT:
            break;
        case ESP_GATTS_CLOSE_EVT:
            break;
        case ESP_GATTS_LISTEN_EVT:
            break;
        case ESP_GATTS_CONGEST_EVT:
            break;
        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;
        }
        default:
            break;
    }
}

注册完成以后,所有的GATTS回调函数都在这个函数执行。首先,注册APP会进入这个函数执行case ESP_GATTS_REG_EVT:… ,在里面设置ble名字、设置广播数据(会进入GAP回调)、创建属性表(很重要,会进入GATTS回调)。创建属性表完成后会进入这个函数的case ESP_GATTS_CREAT_ATTR_TAB_EVT:…,这里打印一些内容后开始启动服务(属性表已经在程序上面定义的时候写好了)。
设置广播数据会进入GAP回调。在GAP回调函数中进入case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:…,开启广播,开启广播完成会进入这个函数的case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:…。至此,等待客户端ble来扫描、连接,然后客户端会读取属性表,所有的数据通讯就是通过这个属性表来完成的。

属性表

属性表在程序中定义。

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, (uint8_t *)&primary_service_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
    //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
    [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
    [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_gatts_attr_db_t的结构,

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;

由两部分组成,第一部分

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;

只有一个数据,前面两个define表示了这个数据可以使用的值,ble服务器与客户端通讯,客户端对于服务器数据的读写是需要服务端回复的,这个回复可以由ESP底层完成、也可以由用户自己写程序完成,为了方便,demo使用了ESP库完成,选择ESP_GATT_AUTO_RSP
第二部分

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*/
     uint16_t length;                   /*!< Current length of the element*/
     uint8_t  *value;                   /*!< Element value array*/
 } esp_attr_desc_t;

UUDI长度(demo使用16位)、UUDI数据、属性权限、数据最大长度、数据当前长度、数据。数据最大长度和数据当前长度区分:数据最大长度指示这个Attribute可以写入的数据最大的长度,数据当前长度指示Attribute当前只需要写入这么多个数据,两者可以不同的。
直接看属性表

//SPP -  Service Declaration
    [SPP_IDX_SVC]                       =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_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}},

根据前面的内容,server和char.是实体的,所以需要声明和定义它们,这个属性表就是这么一个作用,由于第一个参数是选择了ESP库自动回复,所以直接看第二个参数。
数组第一个元素是server的声明、第二个元素是数据接收char.声明、第三个元素是数据接收char.值定义、第四个是notify类的char.声明、第五个是notify类的char.值定义、第六个是客户端char.配置描述。
Server的声明是必须有的,它的UUID配置为primary_service_uuid,这个值为0x2800,查看下图,表明这个Attribute是一个服务声明
在这里插入图片描述
它的权限是只读,因为UUID都是固定的,所以只读。
它的值使用了自定义的UUID,只是一个标识。
Char.声明也是必须有的,其他和server声明类似,其中这里的值是ESP_GATT_CHAR_PROP_BIT_WRITE_NR|ESP_GATT_CHAR_PROP_BIT_READ,表示这个char.值是可以读写的,其中WRITE_NR表示无响应写操作,可以连续高效地接收客户端的数据
Char.数据定义也是必须有的,这里的值就是真正要传输的数据了。可以通过esp_ble_gatts_set_attr_value()函数来写这个属性表,当客户端来读取Attribute的时候就可以获取到这个值了。
Notify的Attribute和前面的类似,这个是一个通知,服务器可以通过这个Attribute发送一个通知,客户端就能马上收到通知就能获取数据(和上面修改属性表的方式发送数据相比,这种方式客户端能触发一个事件,不需要客户端主动读取属性表),还有一种类似的方式是指示。通知和指示的Attribute还必须配置一个Client Characteristic Configuration Descriptor(简称CCC)的Attribute,这里配置的值表示客户端是否能主动接收通知和指示,例如,在心率服务中,有一个称为心率测量的特征。 GATT 客户端(例如您的手机)可以使用该特征的 CCCD 来接收有关该特征的更新。因此,它通过在 CCCD 中启用所述特征的指示或通知来订阅心率测量特征。这意味着 GATT 服务器(很可能是心率传感器设备)会将这些测量结果推送到您的手机,而您的手机无需轮询这些测量结果。默认为0x00,0x00。这个Attribute可以通过客户端配置,所以必须可读可写。

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值