GAP全名是Generic Access Profile,通用访问配置文件,它定义了蓝牙设备的角色,中心和外设,并且控制他们的连接和广播数据。广播数据有两种方式:广播数据和扫描回复数据,数据包大小最长为31字节,其中广播数据方式是必需的。广播数据就是蓝牙设备自己定时广播数据出来,让周围环境的其它设备可以扫描到它,知道它的存在,是外设主动完成的。而扫描回复数据是可选的,它会根据需要响应请求,比如收到中心设备连接请求,会响应这个请求。这个协议决定了蓝牙设备的交互方式,比如iBeacon只能向外广播消息,而小米手环可以与中心设备双向通信。
GAP的工作流程如下图:
大部分情况外设广播自己来让中心设备发现自己并建立连接,然后GATT(后面讲到)就可以用来进行更多的数据交换,这种方式是独占的,就是建立连接了以后外设就不再广播了,只跟连接上的中心通信。但也有一种是不需要连接的,比如上面说的Beancon设备,它只需要向外广播数据,只要在一定的范围内,所有的中心设备都可以收到数据,不需要指定跟具体某个中心建立连接,他们的网络拓扑图如下:
GATT的介绍
GATT是用Attribute Protocol(属性协议)定义的一个Service服务框架。这个框架定义了Service以及他们的Characteristics的格式和规程。规程就是定义了包括发现、读、写、通知、指示以及配置广播的characteristics。
一个attribute由三个元素组成:
GATT是建立在GAP基础之上发挥作用的,就是两个BLE设备只有通过GAP建立连接之后才能用GATT进行通信。上面说了扫描回复数据交互方式是独占的,所以GATT通信只允许是一个外设和一个中心连接。如果两个外设想要通信,唯一的方式就是建立GATT连接,通过中心来中转。
一个外设只能跟一个中心建立连接(独占的),而一个中心可以同时连接多个外设(一个手机可以同时连接多个BLE设备)
GATT连接网络拓扑图
GATT通信的双方是C/S关系,外设作为GATT的服务器(Server),中心设备是GATT的客户端(Client),它向Server发起请求。注意,所有的通信都是由主设备(客户端Client)发起,服务器Server响应数据反馈给Client。一旦建立了通信连接,外设会建议中心设备做定时连接(connection interval),这样中心设备就会在每个连接间隔尝试重新连接,检查有没有新的数据。这个只是一个建议,中心设备可能不会严格按照这个时间间隔来执行,例如中心设备正忙于连接其它外设或者资源太忙。
外设与中心数据交换图:
GATT结构由嵌套的Profile、Service、Characteristics组成,如下图:
Service就是一个独立的逻辑项,它包含一个或多个Characteristic,每个Service都由唯一的UUID标识,UUID有16位的有128位的,16位的UUID是官方通过认证的,需要花钱购买,128位的可以随便自己定义。
Characteristic是GATT中最小的逻辑数据单元,当然它可能包含一组关联的数据;中心可以通过读取service、再进一步读取characteristic来获得具体的数值。
与Service类似,每个 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一标识。所有数据交互必须通过明确的UUID确定到service和characteristic。
实际上,和 BLE 外设打交道,主要是通过 Characteristic。你可以从 Characteristic 读取数据,也可以往 Characteristic 写数据。这样就实现了双向的通信。所以你可以自己实现一个类似串口(UART)的 Sevice,这个 Service 中包含两个 Characteristic,一个被配置只读的通道(RX),另一个配置为只写的通道(TX)。
在GATT的Profile的定义11个features,映射了程序Procedure。
ESP32例程分析
GATT连接是独占的,即一个BLE周边设备同时只能与一个中心设备连接。
profile 可以理解为一种规范,一个标准的通信协议中,存于从机(server)中。蓝牙组织规定了一些标准的profile。每个profile中包含多个service,每个service代表从机的一种能力。
GATT服务器的架构组织
一个GATT 服务器应用程序架构(由Application Profiles组织起来)如下:
每个Profile定义为一个结构体,结构体成员依赖于该Application Profile 实现的services服务和characteristic特征。结构体成员还包括GATT interface(GATT 接口)、Application ID(应用程序ID)和处理profile事件的回调函数。
如果Characteristic 支持通知(notifications)或指示(indicatons),它就必须是实现CCCD(Client Characteristic Configuration Descriptor)----这是额外的ATT。描述符有一个句柄和UUID。
struct gatts_profile_inst {
esp_gatts_cb_t gatts_cb;
dd_uint16_t gatts_if;
dd_uint16_t app_id;
dd_uint16_t conn_id;
dd_uint16_t service_handle;
esp_gatt_srvc_id_t service_id;
dd_uint16_t char_handle;
esp_bt_uuid_t char_uuid;
esp_gatt_perm_t perm;
esp_gatt_char_prop_t property;
dd_uint16_t descr_handle;
esp_bt_uuid_t descr_uuid;
};
Application Profile存储在数组中,并分配相应的回调函数gatts_profile_a_event_handler() 和 gatts_profile_b_event_handler()。
在GATT客户机上的不同的应用程序使用不同的接口,用gatts_if参数来表示。在初始化时,gatts-if参数初始化为ESP_GATT_IF_NONE,这意味着Application Profile还没有连接任何客户端。
/* 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 gl_profile_tab[PROFILE_NUM] = {
[PROFILE_A_APP_ID] = {
.gatts_cb = gatts_profile_a_event_handler,
.gatts_if = ESP_GATT_IF_NONE, /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},
[PROFILE_B_APP_ID] = {
.gatts_cb = gatts_profile_b_event_handler,/* This demo does not implement, similar as profile A */
.gatts_if = ESP_GATT_IF_NONE, /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},
};
这是两个元素的数组。可以用Application ID来注册Application Profiles,Application ID是由应用程序分配的用来标识每个Profile。 通过这种方法,可以在一个Server中run多个Application Profile。
esp_ble_gatts_app_register(PROFILE_A_APP_ID);
GATT回调函数的注册以及分析
下面是GATT回调函数的注册函数esp_ble_gatts_register_callback:
esp_err_t esp_ble_gatts_register_callback(esp_gatts_cb_t callback);
对于GATT server回调函数类型分析
typedef void (* esp_gatts_cb_t)(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
event: esp_gatts_cb_event_t 这是一个枚举类型,表示调用该回调函数时的事件(或蓝牙的状态)
/// GAP BLE callback event type
typedef enum {
ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT = 0, /*!< When advertising data set complete, the event comes */
ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT, /*!< When scan response data set complete, the event comes */
ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT, /*!< When scan parameters set complete, the event comes */
ESP_GAP_BLE_SCAN_RESULT_EVT, /*!< When one scan result ready, the event comes each time */
ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT, /*!< When raw advertising data set complete, the event comes */
ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT, /*!< When raw advertising data set complete, the event comes */
ESP_GAP_BLE_ADV_START_COMPLETE_EVT, /*!< When start advertising complete, the event comes */
ESP_GAP_BLE_SCAN_START_COMPLETE_EVT, /*!< When start scan complete, the event comes */
ESP_GAP_BLE_AUTH_CMPL_EVT, /* Authentication complete indication. */
ESP_GAP_BLE_KEY_EVT, /* BLE key event for peer device keys */
ESP_GAP_BLE_SEC_REQ_EVT, /* BLE security request */
ESP_GAP_BLE_PASSKEY_NOTIF_EVT, /* passkey notification event */
ESP_GAP_BLE_PASSKEY_REQ_EVT, /* passkey request event */
ESP_GAP_BLE_OOB_REQ_EVT, /* OOB request event */
ESP_GAP_BLE_LOCAL_IR_EVT, /* BLE local IR event */
ESP_GAP_BLE_LOCAL_ER_EVT, /* BLE local ER event */
ESP_GAP_BLE_NC_REQ_EVT, /* Numeric Comparison request event */
ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT, /*!< When stop adv complete, the event comes */
ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT, /*!< When stop scan complete, the event comes */
ESP_GAP_BLE_SET_STATIC_RAND_ADDR_EVT, /*!< When set the static rand address complete, the event comes */
ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT, /*!< When update connection parameters complete, the event comes */
ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT, /*!< When set pkt length complete, the event comes */
ESP_GAP_BLE_SET_LOCAL_PRIVACY_COMPLETE_EVT, /*!< When Enable/disable privacy on the local device complete, the event comes */
ESP_GAP_BLE_REMOVE_BOND_DEV_COMPLETE_EVT, /*!< When remove the bond device complete, the event comes */
ESP_GAP_BLE_CLEAR_BOND_DEV_COMPLETE_EVT, /*!< When clear the bond device clear complete, the event comes */
ESP_GAP_BLE_GET_BOND_DEV_COMPLETE_EVT, /*!< When get the bond device list complete, the event comes */
ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT, /*!< When read the rssi complete, the event comes */
ESP_GAP_BLE_UPDATE_WHITELIST_COMPLETE_EVT, /*!< When add or remove whitelist complete, the event comes */
ESP_GAP_BLE_UPDATE_DUPLICATE_EXCEPTIONAL_LIST_COMPLETE_EVT, /*!< When update duplicate exceptional list complete, the event comes */
ESP_GAP_BLE_EVT_MAX,
} esp_gap_ble_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 指向回调参数,是个联合体类型,不同的事件类型采用联合体内不同的成员结构体。
调用esp_ble_gatts_app_register(1),触发回调函数的ESP_GATTS_REG_EVT时,在其中完成以下操作:
if (event == ESP_GATTS_REG_EVT) {
if (param->reg.status == ESP_GATT_OK) {
gl_profile_tab[param->reg.app_id].gatts_if = gatts_if;
} else {
ddlog_warn("Reg app failed, app_id %04x, status %d\n",
param->reg.app_id,
param->reg.status);
return;
}
}
GATT Server状态机的一般过程为:
没有client连接之前:
REGISTER_APP_EVT---->CREATE_SERVICE_EVT---->SERVICE_START_EVT---->ADD_CHAR_EVT--->ADD_DESCR_EVT
有client连接之后:
CONNECT_EVT---->ESP_GATTS_MTU_EVT--->GATT_WRITE_EVT--->ESP_GATTS_CONF_EVT-->GATT_READ_EVT
esp_err_t esp_ble_gatts_create_attr_tab(const esp_gatts_attr_db_t *gatts_attr_db,
esp_gatt_if_t gatts_if,
uint8_t max_nb_attr,
uint8_t srvc_inst_id);
作用:创建一个服务Attribute表。
参数
gatts_attr_db :指向加入profile的服务 attr 表 (从Service 到 Characteristic....)
gatts_if: GATT服务器的访问接口
max_nb_attr: 加入服务数据库的attr的数目
srvc_inst_id: 服务instance
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;
对于结构体esp_gatts_attr_db_t的成员attr_control的可取值
#define ESP_GATT_RSP_BY_APP 0 //由应用程序回复写入\读取操作应答
#define ESP_GATT_AUTO_RSP 1 //由GATT堆栈自动回复吸入\读取操作应答
/**
* @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*/
uint16_t length; /*!< Current length of the element*/
uint8_t *value; /*!< Element value array*/
} esp_attr_desc_t;
建立连接之前的GATT状态机
在触发ESP_GATTS_REG_EVT时,除了创建表还可以创建服务S,调用esp_ble_gatts_create_service来创建服务S。
esp_err_t esp_ble_gatts_create_service(esp_gatt_if_t gatts_if,
esp_gatt_srvc_id_t *service_id, uint16_t num_handle);
作用:创建一个service。当一个service创建成功(done)后,ESP_CREATE_SERVICE_EVT事件触发回调函数被调用,该回调函数报告了profile的stauts和service ID。当要添加include service和 characteristics/descriptors入服务service,Service ID在回调函数中用到。
参数:gatts_if——GATT 服务器访问接口
service_id: 服务UUID相关信息
num_handle:该服务所需的句柄数 service handle、characteristic declaration handle、 characteristic value handle、characteristic description handle 的句柄数总和
/// 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;
uint32_t uuid32;
uint8_t uuid128[ESP_UUID_LEN_128];
} uuid; /*!< UUID */
} __attribute__((packed)) esp_bt_uuid_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;
/**
* @brief Gatt service id, include id
* (uuid and instance id) and primary flag
*/
typedef struct {
esp_gatt_id_t id; /*!< Gatt id, include uuid and instance */
bool is_primary; /*!< This service is primary or not */
} __attribute__((packed)) esp_gatt_srvc_id_t;
服务ID包括
is_primary参数当前服务是否是首要的;---------服务声明中的Attribute Type 0x2800---Primary/0x2801---Secondary
id参数UUID的信息(包括uuid 和实例instance) ---------uuid是UUID的信息包括UUID的长度(16bit/32bit/128bit)及UUID具体值。
例如:服务被定义为16bit的UUID的主服务。服务ID以实例ID为0,UUID为0x00FF来初始化。
服务实例ID是用来区分在同一个Profile中具有相同UUID的多个服务。Application Profile中拥有相同UUID的两个服务,需要用不同的实例ID来引用。
switch (event) {
case ESP_GATTS_REG_EVT:
ESP_LOGI(GATTS_TAG, "REGISTER_APP_EVT, status %d, app_id %d\n", param->reg.status, param->reg.app_id);
gl_profile_tab[PROFILE_B_APP_ID].service_id.is_primary = true;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.inst_id = 0x00;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_B;
esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_B_APP_ID].service_id, GATTS_NUM_HANDLE_TEST_B);
break;
…
}
启动服务并创建Characteristics
当一个服务service创建成功(done)后,由该profile GATT handler 管理的 ESP_GATTS_CREATE_EVT事件被触发,在这个事件可以启动服务和添加characteristics到服务中。调用esp_ble_gatts_start_service来启动指定服务。
Characteristic是在GATT规范中最小的逻辑数据单元,由一个Value和多个描述特性的Desciptior组成。实际上,在与蓝牙设备打交道,主要就是读写Characteristic的value来完成。同样的,Characteristic也是通过16bit或128bit的UUID唯一标识。
我们根据蓝牙设备的协议用对应的Characteristci进行读写即可达到与其通信的目的。
ESP_GATTS_CREATE_EVT事件中回调函数参数的类型为gatts_create_evt_param(包括操作函数、servic的句柄、服务的id
/**
* @brief ESP_GATTS_CREATE_EVT
*/
struct gatts_create_evt_param {
esp_gatt_status_t status; /*!< Operation status */
uint16_t service_handle; /*!< Service attribute handle */
esp_gatt_srvc_id_t service_id; /*!< Service id, include service uuid and other information */
} create; /*!< Gatt server callback param of ESP_GATTS_CREATE_EVT */
esp_err_t esp_ble_gatts_start_service(uint16_t service_handle)
esp_ble_gatts_start_service()这个函数启动一个服务。
参数: service_handle-------要被启动的服务句柄。
首先,由BLE堆栈生成生成的服务句柄(service handle)存储在Profile表中,应用层将用服务句柄来引用这个服务。调用esp_ble_gatts_start_service()和先前产生服务句柄来启动服务。
这样ESP_ATTS_START_EVT事件触发时,将打印输出信息启动的Service Handle之类的信息。
添加特征到service中,调用esp_ble_gatts_add_char()来添加characteristics连同characteristic 权限和property到服务service中。
有必要再次解释一下property
Characteristic Properties这个域(bit控制)决定了Characteristic Value如何使用、Characteristic descriptors 如何访问。只要下标中对应bit被设备,那么对应描述的action就被允许
添加Characteristic Value declaration ATT
esp_err_t esp_ble_gatts_add_char(uint16_t service_handle, esp_bt_uuid_t *char_uuid,
esp_gatt_perm_t perm, esp_gatt_char_prop_t property, esp_attr_value_t *char_val, esp_attr_control_t *control)
参数:service_handle-------Characteristic要添加到的服务的Service handler服务句柄,一个Characteristic至少包括2个属性ATT,一个属性用于characteristic declaration/另一个用于存放特征值(characteristic value declaration).
char_uuid-------Characteristic 的UUID; 属于Characteristic declaration 这个ATT
perm------特征值声明(Characteristic value declaration) 属性(Attribute)访问权限;
ATT具有一组与其关联的权限值,权限值指定了读/写权限、认证权限、授权许可
permission权限的可取值{可读、可加密读、可加密MITM读、可写、可加密写、可加密MITM写}
/**
* @brief Attribute permissions
*/
#define ESP_GATT_PERM_READ (1 << 0) /* bit 0 - 0x0001 */ /* relate to BTA_GATT_PERM_READ in bta/bta_gatt_api.h */
#define ESP_GATT_PERM_READ_ENCRYPTED (1 << 1) /* bit 1 - 0x0002 */ /* relate to BTA_GATT_PERM_READ_ENCRYPTED in bta/bta_gatt_api.h */
#define ESP_GATT_PERM_READ_ENC_MITM (1 << 2) /* bit 2 - 0x0004 */ /* relate to BTA_GATT_PERM_READ_ENC_MITM in bta/bta_gatt_api.h */
#define ESP_GATT_PERM_WRITE (1 << 4) /* bit 4 - 0x0010 */ /* relate to BTA_GATT_PERM_WRITE in bta/bta_gatt_api.h */
#define ESP_GATT_PERM_WRITE_ENCRYPTED (1 << 5) /* bit 5 - 0x0020 */ /* relate to BTA_GATT_PERM_WRITE_ENCRYPTED in bta/bta_gatt_api.h */
#define ESP_GATT_PERM_WRITE_ENC_MITM (1 << 6) /* bit 6 - 0x0040 */ /* relate to BTA_GATT_PERM_WRITE_ENC_MITM in bta/bta_gatt_api.h */
#define ESP_GATT_PERM_WRITE_SIGNED (1 << 7) /* bit 7 - 0x0080 */ /* relate to BTA_GATT_PERM_WRITE_SIGNED in bta/bta_gatt_api.h */
#define ESP_GATT_PERM_WRITE_SIGNED_MITM (1 << 8) /* bit 8 - 0x0100 */ /* relate to BTA_GATT_PERM_WRITE_SIGNED_MITM in bta/bta_gatt_api.h */
property-----Characteristic Properties (特征值声明属性的Properties)
char_val------属性值 Characteristic Value
control-------属性响应控制字节
characteristic declaration的Attribute Value 包括 property、characteristic Value declaration handle、char_uuid 三个段;其中property、char_uuid在添加Characteristic调用的函数的参数中已经指明,只有characteristic Value declaration handle尚未明确指出。
gl_profile_tab[PROFILE_A_APP_ID].service_handle = param->create.service_handle;
gl_profile_tab[PROFILE_A_APP_ID].char_uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_A_APP_ID].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_TEST_A;
esp_ble_gatts_add_char(gl_profile_tab[PROFILE_A_APP_ID].service_handle,
&gl_profile_tab[PROFILE_A_APP_ID].char_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
a_property,
&gatts_demo_char1_val,
NULL);
/**
* @brief set the attribute value type
*/
typedef struct
{
uint16_t attr_max_len; /*!< attribute max value length */
uint16_t attr_len; /*!< attribute current value length */
uint8_t *attr_value; /*!< the pointer to attribute value */
} esp_attr_value_t;
uint8_t char1_str[] = {0x11,0x22,0x33};
esp_attr_value_t gatts_demo_char1_val =
{
.attr_max_len = GATTS_DEMO_CHAR_VAL_LEN_MAX,
.attr_len = sizeof(char1_str),
.attr_value = char1_str,
};
在这个结构体里面包括了属性值最大长度、当前长度、当前值。由这个来指定 characteristic value declaration的。有这些值就足以构成一个 characteristic value declaration ATT了。
添加Characteristic descriptor描述符
当特征添加到service中成功(done)时,触发ESP_GATTS_ADD_CHAR_EVT事件。触发ESP_GATTS_ADD_CHAR_EVT事件时,回调函数参数param的结构体为gatts_add_char_evt_param,包括操作状态、特征ATT的handle()、service_handle(服务句柄)、characteristc uuid(服务的UUID)。
/**
* @brief ESP_GATTS_ADD_CHAR_EVT
*/
struct gatts_add_char_evt_param {
esp_gatt_status_t status; /*!< Operation status */
uint16_t attr_handle; /*!< Characteristic attribute handle */
uint16_t service_handle; /*!< Service attribute handle */
esp_bt_uuid_t char_uuid; /*!< Characteristic uuid */
} add_char; /*!< Gatt server callback param of ESP_GATTS_ADD_CHAR_EVT */
还可以通过调用esp_ble_gatts_get_attr_value()来获取跟具体的Characteristic Value declartation 属性的具体信息。
下面是调用的例子,输入参数是特征句柄;输出参数是length和prf_char
esp_err_t esp_ble_gatts_get_attr_value(uint16_t attr_handle, uint16_t *length, const uint8_t **value)
{
if (attr_handle == ESP_GATT_ILLEGAL_HANDLE) {
return ESP_FAIL;
}
btc_gatts_get_attr_value(attr_handle, length, (uint8_t **)value);
return ESP_OK;
}
void btc_gatts_get_attr_value(uint16_t attr_handle, uint16_t *length, uint8_t **value)
{
BTA_GetAttributeValue(attr_handle, length, value);
}
void BTA_GetAttributeValue(UINT16 attr_handle, UINT16 *length, UINT8 **value)
{
bta_gatts_get_attr_value(attr_handle, length, value);
}
void bta_gatts_get_attr_value(UINT16 attr_handle, UINT16 *length, UINT8 **value)
{
GATTS_GetAttributeValue(attr_handle, length, value);
}
/*******************************************************************************
**
** Function GATTS_GetAttributeValue
**
** Description This function sends to set the attribute value .
**
** Parameter attr_handle: the attribute handle
** length:the attribute value length in the database
** value: the attribute value out put
**
** Returns GATT_SUCCESS if successfully sent; otherwise error code.
**
*******************************************************************************/
tGATT_STATUS GATTS_GetAttributeValue(UINT16 attr_handle, UINT16 *length, UINT8 **value)
{
tGATT_STATUS status;
tGATT_HDL_LIST_ELEM *p_decl;
GATT_TRACE_DEBUG("GATTS_GetAttributeValue: attr_handle: %u\n",
attr_handle);
if ((p_decl = gatt_find_hdl_buffer_by_attr_handle(attr_handle)) == NULL) {
GATT_TRACE_ERROR("Service not created\n");
return GATT_INVALID_HANDLE;
}
status = gatts_get_attribute_value(&p_decl->svc_db, attr_handle, length, value);
return status;
}
还可在ESP_GATTS_ADD_CHAR_EVT事件的回调函数中,给characteristic添加characteristic description ATT。
下面是添加char_desc的例子
gl_profile_tab[PROFILE_A_APP_ID].char_handle = param->add_char.attr_handle;
gl_profile_tab[PROFILE_A_APP_ID].descr_uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_A_APP_ID].descr_uuid.uuid.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
esp_err_t add_descr_ret = esp_ble_gatts_add_char_descr(
gl_profile_tab[PROFILE_A_APP_ID].service_handle, &gl_profile_tab[PROFILE_A_APP_ID].descr_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, NULL, NULL);
case ESP_GATTS_ADD_CHAR_DESCR_EVT:
gl_profile_tab[PROFILE_A_APP_ID].descr_handle = param->add_char_descr.attr_handle;
ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d\n",
param->add_char_descr.status, param->add_char_descr.attr_handle, param->add_char_descr.service_handle);
break;
esp_ble_gatts_add_char_descr()这个函数的原型如下:
esp_err_t esp_ble_gatts_add_char_descr (uint16_t service_handle,
esp_bt_uuid_t *descr_uuid,
esp_gatt_perm_t perm, esp_attr_value_t *char_descr_val,
esp_attr_control_t *control);
service_handle:这个characteristic descriptor要添加的service handle。
perm: 描述符访问权限
descr_uuid:描述符UUID
char_descr_val:描述符值
control:ATT 应答控制字节
这个函数被用来添加Characteristic descriptor。当添加完成时,BTA_GATTS_ADD_DESCR_EVT 回调函数被调用去报告它的状态和ID。
下面看是看开始建立连接后,GATT的状态机转变过程:
手机client连接到server,触发ESP_GATTS_CONNECT_EVT事件。
/**
* @brief ESP_GATTS_CONNECT_EVT
*/
struct gatts_connect_evt_param {
uint16_t conn_id; /*!< Connection id */
esp_bd_addr_t remote_bda; /*!< Remote bluetooth device address */
} connect; /*!< Gatt server callback param of ESP_GATTS_CONNECT_EVT */
/// Bluetooth address length
#define ESP_BD_ADDR_LEN 6
/// Bluetooth device address
typedef uint8_t esp_bd_addr_t[ESP_BD_ADDR_LEN];
ESP_GATTS_CONNECT_EVT事件回调函数param参数包括连接ID以及远端蓝牙设备地址。调用esp_ble_update_conn_params(&conn_params)来更新连接参数。这个函数只有在连接上以后才可以调用。
esp_ble_conn_update_params_t conn_params = {0};
memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
/* For the IOS system, please reference the apple official documents about the ble connection parameters restrictions. */
conn_params.latency = 0;
conn_params.max_int = 0x20; // max_int = 0x20*1.25ms = 40ms
conn_params.min_int = 0x10; // min_int = 0x10*1.25ms = 20ms
conn_params.timeout = 400; // timeout = 400*10ms = 4000ms
esp_ble_gap_update_conn_params(&conn_params);
参数连接更新参数(蓝牙设备地址、最小连接间隔、最大连接间隔、连接数量、LE LINK超时)。
/// Connection update parameters
typedef struct {
esp_bd_addr_t bda; /*!< Bluetooth device address */
uint16_t min_int; /*!< Min connection interval */
uint16_t max_int; /*!< Max connection interval */
uint16_t latency; /*!< Slave latency for the connection in number of connection events. Range: 0x0000 to 0x01F3 */
uint16_t timeout; /*!< Supervision timeout for the LE Link. Range: 0x000A to 0x0C80.
Mandatory Range: 0x000A to 0x0C80 Time = N * 10 msec
Time Range: 100 msec to 32 seconds */
} esp_ble_conn_update_params_t;
建立连接时GATT的状态机研究
设备设备连上外围蓝牙时,打印如下所示:第一个触发了CONNECT回调函数;接着触发了MTU尺寸的大小确认事件。
/**
* @brief ESP_GATTS_CONNECT_EVT
*/
struct gatts_connect_evt_param {
uint16_t conn_id; /*!< Connection id */
esp_bd_addr_t remote_bda; /*!< Remote bluetooth device address */
} connect;
上面是连接回调函数param的参数结构体,包括连接id和远端(对端)蓝牙设备地址(bda)。
在一般回调函数处理中,记录对端的信息,且发送更新后的连接参数到对端设备 。
//start sent the update connection parameters to the peer device.
esp_ble_gap_update_conn_params(&conn_params);
深入到 esp_ble_gap_update_conn_params(&conn_params)。这个函数只能在连接上时使用,用于更新连接参数。
/**
* @brief Update connection parameters, can only be used when connection is up.
*
* @param[in] params - connection update parameters
*
* @return
* - ESP_OK : success
* - other : failed
*
*/
/// Connection update parameters
typedef struct {
esp_bd_addr_t bda; /*!< Bluetooth device address */
uint16_t min_int; /*!< Min connection interval */
uint16_t max_int; /*!< Max connection interval */
uint16_t latency; /*!< Slave latency for the connection in number of connection events. Range: 0x0000 to 0x01F3 */
uint16_t timeout; /*!< Supervision timeout for the LE Link. Range: 0x000A to 0x0C80.
Mandatory Range: 0x000A to 0x0C80 Time = N * 10 msec
Time Range: 100 msec to 32 seconds */
} esp_ble_conn_update_params_t;
esp_err_t esp_ble_gap_update_conn_params(esp_ble_conn_update_params_t *params)
{
btc_msg_t msg;
btc_ble_gap_args_t arg;
ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED);
msg.sig = BTC_SIG_API_CALL;
msg.pid = BTC_PID_GAP_BLE;
msg.act = BTC_GAP_BLE_ACT_UPDATE_CONN_PARAM;
memcpy(&arg.conn_update_params.conn_params, params, sizeof(esp_ble_conn_update_params_t));
return (btc_transfer_context(&msg, &arg, sizeof(btc_ble_gap_args_t), NULL) == BT_STATUS_SUCCESS ? ESP_OK : ESP_FAIL);
}
继续深入bt_status_t btc_transfer_context(btc_msg_t *msg, void *arg, int arg_len, btc_arg_deep_copy_t copy_func)
bt_status_t btc_transfer_context(btc_msg_t *msg, void *arg, int arg_len, btc_arg_deep_copy_t copy_func)
{
btc_msg_t lmsg;
if (msg == NULL) {
return BT_STATUS_PARM_INVALID;
}
BTC_TRACE_DEBUG("%s msg %u %u %u %p\n", __func__, msg->sig, msg->pid, msg->act, arg);
memcpy(&lmsg, msg, sizeof(btc_msg_t));
if (arg) {
lmsg.arg = (void *)osi_malloc(arg_len);
if (lmsg.arg == NULL) {
return BT_STATUS_NOMEM;
}
memset(lmsg.arg, 0x00, arg_len); //important, avoid arg which have no length
memcpy(lmsg.arg, arg, arg_len);
if (copy_func) {
copy_func(&lmsg, lmsg.arg, arg);
}
} else {
lmsg.arg = NULL;
}
return btc_task_post(&lmsg, TASK_POST_BLOCKING);
}
ESP_GATTS_MTU_EVT事件对应的回调函数中参数param的结构体为gatts_mtu_evt_param(包括连接id和MTU大小)
/**
* @brief ESP_GATTS_MTU_EVT
*/
struct gatts_mtu_evt_param {
uint16_t conn_id; /*!< Connection id */
uint16_t mtu; /*!< MTU size */
} mtu; /*!< Gatt server callback param of ESP_GATTS_MTU_EVT */
在例子中设置本地的MTU大小为500,代码如下所示:
esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);
如上所述,设置了MTU的值(经过MTU交换,从而设置一个PDU中最大能够交换的数据量)。例如:主设备发出一个150字节的MTU请求,但是从设备回应的MTU是23字节,那么今后双方要以较小的值23字节作为以后的MTU。即主从双方每次在做数据传输时不超过这个最大数据单元。 MTU交换通常发生在主从双方建立连接后。MTU比较小,就是为什么BLE不能传输大数据的原因所在。
MTU交换请求用于client通知server关于client最大接收MTU大小并请求server响应它的最大接收MTU大小。
Client的接收MTU 应该大于或等于默认ATT_MTU(23).这个请求已建立连接就由client发出。这个Client Rx MTU参数应该设置为client可以接收的attribute protocol PDU最大尺寸。
MTU交换应答发送用于接收到一个Exchange MTU请求
Server和Client应该设置ATT_MTU为Client Rx MTU和Server Rx MTU两者的较小值。
这个ATT_MTU在server在发出这个应答后,在发其他属性协议PDU之前生效;在client收到这个应答并在发其他属性协议PDU之前生效。
发送应答数据
#if (GATTS_INCLUDED == TRUE)
esp_err_t esp_ble_gatts_send_response(esp_gatt_if_t gatts_if, uint16_t conn_id, uint32_t trans_id,
esp_gatt_status_t status, esp_gatt_rsp_t *rsp);
btc_msg_t msg;
btc_ble_gatts_args_t arg;
ESP_BLUEDROID_STATUS_CHECK(ESP_BLUEDROID_STATUS_ENABLED);
msg.sig = BTC_SIG_API_CALL;
msg.pid = BTC_PID_GATTS;
msg.act = BTC_GATTS_ACT_SEND_RESPONSE;
arg.send_rsp.conn_id = BTC_GATT_CREATE_CONN_ID(gatts_if, conn_id);
arg.send_rsp.trans_id = trans_id;
arg.send_rsp.status = status;
arg.send_rsp.rsp = rsp;
return (btc_transfer_context(&msg, &arg, sizeof(btc_ble_gatts_args_t),
btc_gatts_arg_deep_copy) == BT_STATUS_SUCCESS ? ESP_OK : ESP_FAIL);
}
#endif
这个函数用于发送应答给对应请求。
参数: gatts_if-------GATT server 访问接口
conn_id-----连接ID
trans_id-----传输ID
status----应答状态
rsp-----应答数据 gatt attribute value
#define ESP_GATT_MAX_ATTR_LEN 600 //as same as GATT_MAX_ATTR_LEN
/// Gatt attribute value
typedef struct {
uint8_t value[ESP_GATT_MAX_ATTR_LEN]; /*!< Gatt attribute value */
uint16_t handle; /*!< Gatt attribute handle */
uint16_t offset; /*!< Gatt attribute value offset */
uint16_t len; /*!< Gatt attribute value length */
uint8_t auth_req; /*!< Gatt authentication request */
} esp_gatt_value_t;
/// GATT remote read request response type
typedef union {
esp_gatt_value_t attr_value; /*!< Gatt attribute structure */
uint16_t handle; /*!< Gatt attribute handle */
} esp_gatt_rsp_t;
客户端和服务器端收发数据
首先了解一下手机端在蓝牙连接、数据交互过程中的操作。
第3步扫描到设备会触发回调,在回调中,通常根据设备名称找到想要连接的设备,完成连接;连接后,获取到BluetoothGATT 对象,再从BluetoothGatt.discoverServices中获取各个Services,根据硬件工程师提供的UUID连接你需要的Service。最后,根据硬件工程师提供的UUID找到读、写和通知的UUID,然后再进行读写操作。 读写和设置通知均为单步操作,执行完一个才能执行下一个。
下面从小程序的低功耗蓝牙接口出发
wx.writeBLECharacteristicValue(Object object)
Object{
deviceId::string //必填,蓝牙设备id
serviceId::string //必填,蓝牙特征值Characteristic对应服务的UUID
characteristicId::string //必填,蓝牙特征值的UUID
value::ArrayBuffer //必填,蓝牙设备特征值对应的二进制值
success::function //非必填,接口调用成功后的回调函数
fail::function //非必填,接口调用失败后的回调函数
complete::function //非必填,接口调用结束后的回调函数(调用成功、失败都会执行)
}
向低功耗蓝牙设备特征值中写入二进制数据。 必须设备的特征值支持write权限才可以成功调用。
wx.writeBLECharacteristicValue({
deviceId: that.data.deviceId,//设备deviceId
serviceId: that.data.service_id,//设备service_id
characteristicId: that.data.write_id,//设备write特征值
value: buffer,//写入数据
success: function (res) {
console.log('发送数据:', res.errMsg)
}
});
wx.readBLECharacteristicValue(Object object)
Objcet{
deviceId::string //必填,蓝牙设备id
serviceId::string //必填,蓝牙特征值对应服务的UUID
characteristic::string //必填,蓝牙特征值UUID
success::function //非必填,接口调用成功后回调函数
fail::function //非必填,接口调用失败后回调函数
complete::function //非必填,接口调用结束后回调函数(调用成功、失败都会执行)
}
wx.readBLECharacteristicValue({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId,
success(res) {
console.log('readBLECharacteristicValue:', res.errCode)
}
})
目前,微信小程序上也应该是无应答写
设备端的操作
将ESP_GATTS_READ_EVT、ESP_GATTS_WRITE_EVT和ESP_GATTS_EXEC_WRITE_EVT三类事件param参数的类型如下:
/**
* @brief ESP_GATTS_READ_EVT
*/
struct gatts_read_evt_param {
uint16_t conn_id; /*!< Connection id */
uint32_t trans_id; /*!< Transfer id */
esp_bd_addr_t bda; /*!< The bluetooth device address which been read */
uint16_t handle; /*!< The attribute handle */
uint16_t offset; /*!< Offset of the value, if the value is too long */
bool is_long; /*!< The value is too long or not */
bool need_rsp; /*!< The read operation need to do response */
} read; /*!< Gatt server callback param of ESP_GATTS_READ_EVT */
/**
* @brief ESP_GATTS_WRITE_EVT
*/
struct gatts_write_evt_param {
uint16_t conn_id; /*!< Connection id */
uint32_t trans_id; /*!< Transfer id */
esp_bd_addr_t bda; /*!< The bluetooth device address which been written */
uint16_t handle; /*!< The attribute handle */
uint16_t offset; /*!< Offset of the value, if the value is too long */
bool need_rsp; /*!< The write operation need to do response */
bool is_prep; /*!< This write operation is prepare write */
uint16_t len; /*!< The write attribute value length */
uint8_t *value; /*!< The write attribute value */
} write; /*!< Gatt server callback param of ESP_GATTS_WRITE_EVT */
/**
* @brief ESP_GATTS_EXEC_WRITE_EVT
*/
struct gatts_exec_write_evt_param {
uint16_t conn_id; /*!< Connection id */
uint32_t trans_id; /*!< Transfer id */
esp_bd_addr_t bda; /*!< The bluetooth device address which been written */
#define ESP_GATT_PREP_WRITE_CANCEL 0x00 /*!< Prepare write flag to indicate cancel prepare write */
#define ESP_GATT_PREP_WRITE_EXEC 0x01 /*!< Prepare write flag to indicate execute prepare write */
uint8_t exec_write_flag; /*!< Execute write flag */
} exec_write;
使能通知
使能notify并读取蓝牙发过来的数据,开启这个后我们就能实时获取蓝牙发过来的值了。
使能通知(notify enable)的打印如下所示,打开通知实际上的一个WRITE。对应于手机端的mBluetoothGatt.setCharacteristicNotification(characteristic,true).
【2018-12-25 16:01:24:307】_[0;32mI (27345) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 3, handle 43_[0m
_[0;32mI (27345) GATTS_DEMO: GATT_WRITE_EVT, value len 2, value :_[0m
_[0;32mI (27345) GATTS_DEMO: 01 00 _[0m
_[0;32mI (27355) GATTS_DEMO: notify enable_[0m
_[0;33mW (27355) BT_BTC: btc_gatts_arg_deep_copy 11, NULL response_[0m
_[0;32mI (27365) GATTS_DEMO: ESP_GATTS_CONF_EVT, status 0 attr_handle 42_[0m
【2018-12-25 16:01:24:610】_[0;32mI (27645) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 4, handle 47
_[0m
_[0;32mI (27645) GATTS_DEMO: GATT_WRITE_EVT, value len 2, value :_[0m
_[0;32mI (27645) GATTS_DEMO: 01 00 _[0m
_[0;32mI (27655) GATTS_DEMO: notify enable_[0m
_[0;33mW (27655) BT_BTC: btc_gatts_arg_deep_copy 11, NULL response_[0m
_[0;32mI (27665) GATTS_DEMO: ESP_GATTS_CONF_EVT status 0 attr_handle 46_[0m
_[0;32mI (27725) GATTS_DEMO: GATT_READ_EVT, conn_id 0, trans_id 5, handle 42
_[0m
如果write.handle和descr_handle相同,且长度==2,确定descr_value描述值,根据描述值开启/关闭 通知notify/indicate。
//the size of notify_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_A_APP_ID].char_handle,
sizeof(notify_data), notify_data, false);
深入看看esp_ble_gatts_send_indicate
/**
* @brief Send indicate or notify to GATT client.
* Set param need_confirm as false will send notification, otherwise indication.
*
* @param[in] gatts_if: GATT server access interface
* @param[in] conn_id - connection id to indicate.
* @param[in] attr_handle - attribute handle to indicate.
* @param[in] value_len - indicate value length.
* @param[in] value: value to indicate.
* @param[in] need_confirm - Whether a confirmation is required.
* false sends a GATT notification, true sends a GATT indication.
*
* @return
* - ESP_OK : success
* - other : failed
*
*/
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);
该函数将notify或indicate发给GATT的客户端;
need_confirm = false,则发送的是notification通知;
==true,发送的是指示indication。
其他参数: 服务端访问接口;连接id; 属性句柄,value_len; 值
读写数据
看看Write 和Read 触发的回调函数;读写打印如下所示:
【2019-01-02 10:37:09:539】[0;32mI (4697198) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 11, handle 42[0m
[0;32mI (4697198) GATTS_DEMO: GATT_WRITE_EVT, value len 3, value :[0m
[0;32mI (4697208) GATTS_DEMO: 31 32 33 [0m
[0;33mW (4697208) BT_BTC: btc_gatts_arg_deep_copy 11, NULL response[0m
[0;32mI (4697278) GATTS_DEMO: GATT_READ_EVT, conn_id 0, trans_id 12, handle 42
[0m
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %d, handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);
if (!param->write.is_prep){
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, value len %d, value :", param->write.len);
esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
example_write_event_env(gatts_if, &a_prepare_write_env, param);
break;
}
深入example_write_event_env(),其核心代码如下 esp_ble_gatts_send_response函数
if (param->write.need_rsp){
.....
esp_err_t esp_ble_gatts_send_response(esp_gatt_if_t gatts_if, uint16_t conn_id, uint32_t trans_id,
esp_gatt_status_t status, esp_gatt_rsp_t *rsp);
}
根据param->write.need_rsp是否需要应答来决定,是否调用esp_ble_gatts_send_response来应答。
而esp_ble_gatts_send_response()函数实际调用了btc_transfer_context,将信息发送出去。
先把Write搞清楚,先看看写的事件参数
/**
* @brief ESP_GATTS_WRITE_EVT
*/
struct gatts_write_evt_param {
uint16_t conn_id; /*!< Connection id */
uint32_t trans_id; /*!< Transfer id */
esp_bd_addr_t bda; /*!< The bluetooth device address which been written */
uint16_t handle; /*!< The attribute handle */
uint16_t offset; /*!< Offset of the value, if the value is too long */
bool need_rsp; /*!< The write operation need to do response */
bool is_prep; /*!< This write operation is prepare write */
uint16_t len; /*!< The write attribute value length */
uint8_t *value; /*!< The write attribute value */
} write;
这里的is_prep 是针对单独一个Write Request Attribute Protocol 信息放不下的情况,即Write Long Characteristic;一般先prepare在excute; 由这个参数确定参数的是长包还是短包。
offset 是这包数据相对长数据的偏移,第一包是0x0000;
need_rsp是针对该写操作是否需要应答response。
value就是待写入的Characteristic Value的值。
接下来看看Read,看看读的事件参数
/**
* @brief ESP_GATTS_READ_EVT
*/
struct gatts_read_evt_param {
uint16_t conn_id; /*!< Connection id */
uint32_t trans_id; /*!< Transfer id */
esp_bd_addr_t bda; /*!< The bluetooth device address which been read */
uint16_t handle; /*!< The attribute handle */
uint16_t offset; /*!< Offset of the value, if the value is too long */
bool is_long; /*!< The value is too long or not */
bool need_rsp; /*!< The read operation need to do response */
} read; /*!< Gatt server callback param of ESP_GATTS_READ_EVT */
is_long针对客户端知道特征值句柄和特征值长度大于单独一个Read Response ATT 信息大小时,表示传输的是长数据,就用Read Blob Request。
handle 就是要读的Characteristic Value的句柄;
offset 就是要读的Characteristic Value偏移位置,第一包Read Blob Request时,offset为0x00;
对应的是Read Blob Response ATT 以一部分的Characteristic Value作为ATT Value 参数。
下面是ESP-IDF中关于read Response的结构体
/// Gatt attribute value
typedef struct {
uint8_t value[ESP_GATT_MAX_ATTR_LEN]; /*!< Gatt attribute value */
uint16_t handle; /*!< Gatt attribute handle */
uint16_t offset; /*!< Gatt attribute value offset */
uint16_t len; /*!< Gatt attribute value length */
uint8_t auth_req; /*!< Gatt authentication request */
} esp_gatt_value_t;
/// GATT remote read request response type
typedef union {
esp_gatt_value_t attr_value; /*!< Gatt attribute structure */
uint16_t handle; /*!< Gatt attribute handle */
} esp_gatt_rsp_t;
Characteristic结构
Characteristic 声明、Characteistic Value 声明、Characteristic Descriptor 声明。
其他注意事项
权限对于数据交换,通常设置有读Characteristic和写Characteristic
Read/Write读/写-----一次会同时出发读和写两个回调事件;
Read Only只读-------只能触发读回调事件;
Write Only只写------只能触发写回调事件;
参考文献:
https://blog.csdn.net/zhejfl/article/details/85136102
https://www.cnblogs.com/smart-mutouren/p/5937990.html
一分钟读懂低功耗蓝牙(BLE)MTU交换数据包:https://blog.csdn.net/viewtoolsz/article/details/76177465
BLE低功耗蓝牙详解,解决读写、通知失败问题:https://www.jianshu.com/p/f8130a0bfd94
https://blog.csdn.net/NBDR_YL/article/details/99546718