ESP32学习笔记(31)——BLE带有属性表的GATT服务

Posted Leung_ManWah

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ESP32学习笔记(31)——BLE带有属性表的GATT服务相关的知识,希望对你有一定的参考价值。

一、简介

1.1 通用属性协议(GATT)

GATT是用Attribute Protocal(属性协议)定义的一个service(服务)框架。这个框架定义了Services以及它们的Characteristics的格式和规程。规程就是定义了包括发现、读、写、通知、指示以及配置广播的characteristics。

为实现配置文件(Profile)的设备定义了两种角色:Client(客户端)、Server(服务器)。esp32的ble一般就处于Server模式。

一旦两个设备建立了连接,GATT就开始发挥效用,同时意味着GAP协议管理的广播过程结束了。

1.1.1 Profile(规范)

profile 可以理解为一种规范,建立的蓝牙应用任务,蓝牙任务实际上分为两类:标准蓝牙任务规范 profile(公有任务),非标准蓝牙任务规范 profile(私有任务)。

  • 标准蓝牙任务规范 profile:指的是从蓝牙特别兴趣小组 SIG 的官网上已经发布的 GATT 规范列表,包括警告通知(alert notification),血压测量(blood pressure),心率(heart rate),电池(battery)等等。它们都是针对具体的低功耗蓝牙的应用实例来设计的。目前蓝牙技术联盟还在不断的制定新的规范,并且发布。

  • 非标准蓝牙任务规范 profile:指的是供应商自定义的任务,在蓝牙 SIG 小组内未定义的任务规范。

1.1.2 Service(服务)

service 可以理解为一个服务,在 BLE 从机中有多个服务,例如:电量信息服务、系统信息服务等;
每个 service 中又包含多个 characteristic 特征值;
每个具体的 characteristic 特征值才是 BLE 通信的主题,比如当前的电量是 80%,电量的 characteristic 特征值存在从机的 profile 里,这样主机就可以通过这个 characteristic 来读取 80% 这个数据。
GATT 服务一般包含几个具有相关的功能,比如特定传感器的读取和设置,人机接口的输入输出。组织具有相关的特性到服务中既实用又有效,因为它使得逻辑上和用户数据上的边界变得更加清晰,同时它也有助于不同应用程序间代码的重用。

1.1.3 Characteristic(特征)

characteristic 特征,BLE 主从机的通信均是通过 characteristic 来实现,可以理解为一个标签,通过这个标签可以获取或者写入想要的内容。

1.1.4 UUID(通用唯一识别码)

uuid 通用唯一识别码,我们刚才提到的 service 和 characteristic 都需要一个唯一的 uuid 来标识;
每个从机都会有一个 profile,不管是自定义的 simpleprofile,还是标准的防丢器 profile,他们都是由一些 service 组成,每个 service 又包含了多个 characteristic,主机和从机之间的通信,均是通过characteristic来实现。

1.2 ESP32蓝牙应用结构

蓝牙是⼀种短距通信系统,其关键特性包括鲁棒性、低功耗、低成本等。蓝牙系统分为两种不同的技术:经典蓝牙 (Classic Bluetooth) 和蓝牙低功耗 (Bluetooth Low Energy)。
ESP32 支持双模蓝牙,即同时支持经典蓝牙和蓝牙低功耗。

从整体结构上,蓝牙可分为控制器 (Controller) 和主机 (Host) 两⼤部分:控制器包括了 PHY、Baseband、Link Controller、Link Manager、Device Manager、HCI 等模块,用于硬件接⼝管理、链路管理等等;主机则包括了 L2CAP、SMP、SDP、ATT、GATT、GAP 以及各种规范,构建了向应用层提供接口的基础,方便应用层对蓝牙系统的访问。主机可以与控制器运行在同⼀个宿主上,也可以分布在不同的宿主上。ESP32 可以支持上述两种方式。

1.3 Bluedroid主机架构

在 ESP-IDF 中,使用经过大量修改后的 BLUEDROID 作为蓝牙主机 (Classic BT + BLE)。BLUEDROID 拥有较为完善的功能,⽀持常用的规范和架构设计,同时也较为复杂。经过大量修改后,BLUEDROID 保留了大多数 BTA 层以下的代码,几乎完全删去了 BTIF 层的代码,使用了较为精简的 BTC 层作为内置规范及 Misc 控制层。修改后的 BLUEDROID 及其与控制器之间的关系如下图:

二、API说明

以下控制器和虚拟 HCI 接口位于 bt/include/esp32/include/esp_bt.h

2.1 esp_bt_controller_mem_release

2.2 esp_bt_controller_init

2.3 esp_bt_controller_enable


以下 GATT 接口位于 bt/host/bluedroid/api/include/api/esp_bt_main.hbt/host/bluedroid/api/include/api/esp_gatts_api.h

2.4 esp_bluedroid_init

2.5 esp_bluedroid_enable

2.6 esp_ble_gatts_register_callback

2.7 esp_ble_gatts_app_register

2.8 esp_ble_gatts_create_attr_tab

2.9 esp_ble_gatts_start_service

2.10 esp_ble_gatts_send_indicate

2.11 esp_ble_gatts_send_response

三、蓝牙4.0通信实现过程

  1. 扫描蓝牙BLE终端设备,对应esp32就是广播给大家供扫描
  2. 连接蓝牙BLE终端设备,pad扫描到后去连接
  3. 启动服务发现,连接到esp32后获取相应的服务。
    连接成功后,我们就要去寻找我们所需要的服务,这里需要先启动服务发现。
  4. 获取Characteristic
    之前我们说过,我们的最终目的就是获取Characteristic来进行通信,正常情况下,我们可以从硬件工程师那边得到serviceUUID和characteristicUUID,也就是我们所比喻的班级号和学号,以此来获得我们的characteristic。
  5. 开始通信
    我们在得到Characteristic后,就可以开始读写操作进行通信了。
    a. 对于读操作来说,读取BLE终端设备返回的数据会通过回调方法mGattCallback中的onCharacteristicChanged函数返回。
    b. 对于写操作来说,可以通过向Characteristic写入指令以此来达到控制BLE终端设备的目的

四、Demo程序GATT启动流程

使用 esp-idf\\examples\\bluetooth\\bluedroid\\ble\\gatt_server_service_table 中的例程

.........
//esp_bt_controller_config_t是蓝牙控制器配置结构体,这里使用了一个默认的参数
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    //初始化蓝牙控制器,此函数只能被调用一次,且必须在其他蓝牙功能被调用之前调用
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    //使能蓝牙控制器,mode是蓝牙模式,如果想要动态改变蓝牙模式不能直接调用该函数,
    //应该先用disable关闭蓝牙再使用该API来改变蓝牙模式
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }
    //初始化蓝牙并分配系统资源,它应该被第一个调用
    /*
    蓝牙栈bluedroid stack包括了BT和BLE使用的基本的define和API
    初始化蓝牙栈以后并不能直接使用蓝牙功能,
    还需要用FSM管理蓝牙连接情况
    */
    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }
    //使能蓝牙栈
    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    //建立蓝牙的FSM(有限状态机)
    //这里使用回调函数来控制每个状态下的响应,需要将其在GATT和GAP层的回调函数注册
    /*gatts_event_handler和gap_event_handler处理蓝牙栈可能发生的所有情况,达到FSM的效果*/
    ret = esp_ble_gatts_register_callback(gatts_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gatts register error, error code = %x", ret);
        return;
    }
    ret = esp_ble_gap_register_callback(gap_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gap register error, error code = %x", ret);
        return;
    }

    //下面创建了BLE GATT服务A,相当于1个独立的应用程序
    ret = esp_ble_gatts_app_register(PROFILE_A_APP_ID);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
        return;
    }
    //下面创建了BLE GATT服务B,相当于1个独立的应用程序
    ret = esp_ble_gatts_app_register(PROFILE_B_APP_ID);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
        return;
    }
    /*
    设置了MTU的值(经过MTU交换,从而设置一个PDU中最大能够交换的数据量)。
    例如:主设备发出一个1000字节的MTU请求,但是从设备回应的MTU是500字节,那么今后双方要以较小的值500字节作为以后的MTU。
    即主从双方每次在做数据传输时不超过这个最大数据单元。
    */
    esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);
    if (local_mtu_ret){
        ESP_LOGE(GATTS_TAG, "set local  MTU failed, error code = %x", local_mtu_ret);
    }
.......

五、服务数据结构体设置

一个GATT 服务器应用程序架构(由Application Profiles组织起来)如下:

每个Profile定义为一个结构体,结构体成员依赖于该Application Profile 实现的services服务和characteristic特征。结构体成员还包括GATT interface(GATT 接口)、Application ID(应用程序ID)和处理profile事件的回调函数。

每个profile包括GATT interface(GATT 接口)、Application ID(应用程序ID)、 Connection ID(连接ID)、Service Handle(服务句柄)、Service ID(服务ID)、Characteristic handle(特征句柄)、Characteristic UUID(特征UUID)、ATT权限、Characteristic Properties、描述符句柄、描述符UUID。

如果Characteristic支持通知(notifications)或指示(indicatons),它就必须是实现CCCD(Client Characteristic Configuration Descriptor)----这是额外的ATT。描述符有一个句柄和UUID。如:

struct gatts_profile_inst {
    esp_gatts_cb_t gatts_cb;       //GATT的回调函数
    uint16_t gatts_if;             //GATT的接口
    uint16_t app_id;               //应用的ID
    uint16_t conn_id;              //连接的ID
    uint16_t service_handle;       //服务Service句柄
    esp_gatt_srvc_id_t service_id; //服务Service ID
    uint16_t char_handle;          //特征Characteristic句柄
    esp_bt_uuid_t char_uuid;       //特征Characteristic的UUID
    esp_gatt_perm_t perm;          //特征属性Attribute 授权
    esp_gatt_char_prop_t property; //特征Characteristic的特性
    uint16_t descr_handle;         //描述descriptor句柄
    esp_bt_uuid_t descr_uuid;      //描述descriptorUUID    
};

此示例为心率服务实现了一个应用程序配置文件。应用程序配置文件是一种对功能进行分组的方法,这些功能旨在供一个客户端应用程序使用,例如一个智能手机移动应用程序。通过这种方式,可以在一台服务器中容纳不同类型的配置文件。应用程序配置文件 ID 是用户分配的用于标识每个配置文件的编号,用于在堆栈中注册配置文件,在此示例中,ID 为 0x55。

#define HEART_PROFILE_NUM                       1
#define HEART_PROFILE_APP_IDX                   0
#define ESP_HEART_RATE_APP_ID                   0x55

配置文件Application Profile存储在heart_rate_profile_tab数组中,由于本示例中只有一个配置文件,因此一个元素存储在数组中,索引为零,如HEART_PROFILE_APP_IDX。此外,还初始化了配置文件事件处理程序回调函数gatts_profile_event_handler。GATT 服务端上的不同应用程序使用不同的接口,由 gatts_if 参数表示。对于初始化,此参数设置为ESP_GATT_IF_NONE,稍后当应用程序注册时,gatts_if参数更新为堆栈生成的相应接口。

/* One gatt-based profile one app_id and one gatts_if, this array will store the gatts_if returned by ESP_GATTS_REG_EVT */
static struct gatts_profile_inst heart_rate_profile_tab[PROFILE_NUM] = {
    [PROFILE_APP_IDX] = {
        .gatts_cb = gatts_profile_event_handler,
        .gatts_if = ESP_GATT_IF_NONE,       /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
    },
};

应用程序注册在内部app_main()使用以下esp_ble_gatts_app_register()函数进行:

esp_ble_gatts_app_register(ESP_HEART_RATE_APP_ID);

六、GATT事件处理程序

其作用就是建立了蓝牙GATT的FSM(有限状态机),callback回调函数处理从BLE堆栈推送到应用程序的所有事件。

回调函数的参数:

  • event: esp_gatts_cb_event_t 这是一个枚举类型,表示调用该回调函数时的事件(或蓝牙的状态)
  • gatts_if: esp_gatt_if_t (uint8_t) 这是GATT访问接口类型,通常在GATT客户端上不同的应用程序用不同的gatt_if(不同的Application profile对应不同的gatts_if) ,调用esp_ble_gatts_app_register()时,注册Application profile 就会有一个gatts_if。
  • param: esp_ble_gatts_cb_param_t 指向回调函数的参数,是个联合体类型,不同的事件类型采用联合体内不同的成员结构体。
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) {
            heart_rate_profile_tab[HEART_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 < HEART_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 == heart_rate_profile_tab[idx].gatts_if) {
                if (heart_rate_profile_tab[idx].gatts_cb) {
                    heart_rate_profile_tab[idx].gatts_cb(event, gatts_if, param);
                }
            }
        }
    } while (0);
}

注册应用程序配置文件时,ESP_GATTS_REG_EVT会触发一个事件。ESP_GATTS_REG_EVT的参数是:

esp_gatt_status_t status;    /*!< Operation status */
uint16_t app_id;             /*!< Application id which input in register API */

七、使用属性表创建服务和特征

注册事件用于通过使用该esp_ble_gatts_create_attr_tab()函数创建配置文件属性表。该函数采用一个类型的参数,该参数esp_gatts_attr_db_t对应于由头文件中定义的枚举值键入的查找表。

esp_gatts_attr_db_t结构有两个成员:

esp_attr_control_t    attr_control;       /*!< The attribute control type*/
esp_attr_desc_t       att_desc;           /*!< The attribute type*/

attr_control 是自动响应参数,可以设置为ESP_GATT_AUTO_RSP允许 BLE 堆栈在读取或写入事件到达时处理响应消息。另一个选项是ESP_GATT_RSP_BY_APP允许使用该esp_ble_gatts_send_response()功能手动响应消息。

att_desc其中所述属性:

uint16_t uuid_length;      /* !< UUID 长度*/  
uint8_t   *uuid_p;          /* !< UUID 值*/  
uint16_t perm;             /* !< 属性权限*/        
uint16_t max_length;       /* !< 元素的最大长度*/    
uint16_t length;           /* !< 元素的当前长度*/    
uint8_t   *value;           /* !< 元素值数组*/ 

例如,本例中表格的第一个元素服务属性是:

[HRS_IDX_SVC]                       =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(heart_rate_svc), (uint8_t *)&heart_rate_svc}},

初始化值为:

  • [HRS_IDX_SVC]: 枚举表中的命名或指定初始值设定项。
  • ESP_GATT_AUTO_RSP:自动响应配置,设置由堆栈自动响应。
  • ESP_UUID_LEN_16: UUID 长度设置为 16 位。
  • (uint8_t *)&primary_service_uuid: UUID 将服务标识为主要服务 (0x2800)。
  • ESP_GATT_PERM_READ: 服务的读取权限。
  • sizeof(uint16_t):服务 UUID 的最大长度(16 位)。
  • sizeof(heart_rate_svc):当前服务长度设置为变量heart_rate_svc的大小,即 16 位。
  • (uint8_t *)&heart_rate_svc:服务属性值设置为包含心率服务 UUID (0x180D)的变量heart_rate_svc。

其余的属性以相同的方式初始化。某些属性还具有NOTIFY属性,该属性由&char_prop_notify. 完整的表结构初始化如下:

// / Full HRS 数据库描述 - 用于将属性添加到数据库中
static  const  esp_gatts_attr_db_t heart_rate_gatt_db[HRS_IDX_NB] =
{
    //心率服务声明
    [HRS_IDX_SVC] =
    {{ESP_GATT_AUTO_RSP}{ESP_UUID_LEN_16、( uint8_t *)&primary_service_uuid、ESP_GATT_PERM_READ、
       sizeof ( uint16_t )sizeof (heart_rate_svc)( uint8_t *)&heart_rate_svc

    //心率测量特征声明
    [HRS_IDX_HR_MEAS_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_notify}},

    //心率测量特征值
    [HRS_IDX_HR_MEAS_VAL] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, ( uint8_t *)&heart_rate_meas_uuid, ESP_GATT_PERM_READ,
      HRPS_HT_MEAS_MAX_LEN, 0 , NULL }},

    //心率测量特征 - 客户端特征配置描述符
    [HRS_IDX_HR_MEAS_NTF_CFG] =
    {{ESP_GATT_AUTO_RSP}{ESP_UUID_LEN_16,(uint8_t *)&character_client_config_uuid,ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
      的sizeofuint16_t)的sizeof(heart_measurement_ccc),(uint8_t *)heart_measurement_ccc}}//身体传感器位置特征声明
    [HRS_IDX_BOBY_SENSOR_LOC_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}},

    //身体传感器位置特征值
    [HRS_IDX_BOBY_SENSOR_LOC_VAL] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, ( uint8_t *)&body_sensor_location_uuid, ESP_GATT_PERM_READ,
       sizeof ( uint8_t ), sizeof (body_sensor_loc_val), ( uint8_t *)body_sensor_loc_val}

    //心率控制点特征声明
    [HRS_IDX_HR_CTNL_PT_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}},

    //心率控制点特征值
    [HRS_IDX_HR_CTNL_PT_VAL] =
    {{ESP_GATT_AUTO_RSP}{ESP_UUID_LEN_16,(uint8_t *)&heart_rate_ctrl_point,ESP_GATT_PERM_WRITE | ESP_GATT_PERM_READ,
      的sizeofuint8_t)的sizeof(heart_ctrl_point),(uint8_t *)heart_ctrl_point}}};

八、启动服务

创建属性表时,ESP_GATTS_CREAT_ATTR_TAB_EVT会触发一个事件。此事件具有以下参数:

esp_gatt_status_t status;    /*!< Operation status */
esp_bt_uuid_t svc_uuid;      /*!< Service uuid type */
uint16_t num_handle;         /*!< The number of the attribute handle to be added to the gatts database */
uint16_t *handles;           /*!< The number to the handles */

此示例使用此事件打印信息并检查创建的表的大小是否等于枚举 HRS_IDX_NB 中的元素数。如果表创建正确,则将属性句柄复制到句柄表 heart_rate_handle_table 中,并使用以下esp_ble_gatts_start_service()函数启动服务:

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 != HRS_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, HRS_IDX_NB);
        }
        else {
            memcpy(heart_rate_handle_table, param->add_attr_tab.handles, sizeof(heart_rate_handle_table));
            esp_ble_gatts_start_service(heart_rate_handle_table[HRS_IDX_SVC]);
        }
        break;

存储在事件参数的句柄指针中的句柄是标识每个属性的数字。句柄可用于知道正在读取或写入哪个特征,因此它们可以传递到应用程序的上层以处理不同的操作。


• 由 Leung 写于 2021 年 7 月 8 日

• 参考:GATT 服务器服务表示例演练

以上是关于ESP32学习笔记(31)——BLE带有属性表的GATT服务的主要内容,如果未能解决你的问题,请参考以下文章

ESP32学习笔记(32)——BLE GAP主机端连接

ESP32学习笔记(29)——BLE iBeacon广播

ESP32学习笔记(27)——BLE GAP主机端扫描

ESP32学习笔记(26)——BLE GAP从机端广播

ESP32学习笔记(28)——BLE GAP从机端广播自定义数据

ESP32学习笔记(34)——BLE一主多从连接