GATT专题4之句柄
GATT得句柄在GATT得属性数据结构中,是属性的一个元素。
handle是一个16位的数据,句柄是和服务绑定的,只能先有对应的服务后才会有对应的属性,
客户端通过句柄来引用特定的属性,GATT的所有操作都需要指定句柄。
读/写/通知/指示---在函数中传参使用
点击查看代码
int bt_gatt_write(FAR struct bt_conn_s *conn, uint16_t handle,
FAR const void *data, uint16_t length,
bt_gatt_rsp_func_t func);
void bt_gatt_notify(uint16_t handle, FAR const void *data, size_t len);
int bt_gatt_read(FAR struct bt_conn_s *conn, uint16_t handle,
uint16_t offset, bt_gatt_read_func_t func);
句柄的本质:GATT属性在服务端的内存地址编号,是全局唯一的。
点击查看代码
struct bt_gatt_attr_s
{
/* Attribute UUID */
FAR const struct bt_uuid_s *uuid;
/* Attribute read callback */
CODE int (*read)(FAR struct bt_conn_s *conn,
FAR const struct bt_gatt_attr_s *attr, FAR void *buf,
uint8_t len, uint16_t offset);
/* Attribute write callback */
CODE int (*write)(FAR struct bt_conn_s *conn,
FAR const struct bt_gatt_attr_s *attr,
FAR const void *buf, uint8_t len, uint16_t offset);
/* Attribute flush callback */
CODE int (*flush)(FAR struct bt_conn_s *conn,
FAR const struct bt_gatt_attr_s *attr,
uint8_t flags);
/* Attribute user data */
FAR void *user_data;
/* Attribute handle */
uint16_t handle;
/* Attribute permissions */
uint8_t perm;
};
一、先把句柄的本质讲透
句柄(Handle)= GATT 属性在服务端的「内存地址编号」,是客户端访问属性的唯一入口
- 16位无符号整数(0x0001 ~ 0xFFFF),GATT服务器初始化时按属性顺序自动分配,全局唯一
- 所有GATT操作(读、写、订阅通知、发现服务)都必须用句柄定位属性,UUID只是属性的「功能名」,句柄才是「门牌号」
二、以蓝牙手表心率服务为例,完整拆解句柄的分配与使用
1. 心率服务的属性结构(服务端视角)
心率服务(0x180D)在GATT服务器中,会按顺序生成3个核心属性,每个属性对应一个句柄:
| 属性类型 | UUID | 功能 | 句柄分配(示例) |
|---|---|---|---|
| 服务声明(Service Declaration) | 0x2800(GATT标准服务声明UUID) |
声明「心率服务」的存在 | 0x0010 |
| 心率测量特征值声明(Characteristic Declaration) | 0x2803(GATT标准特征声明UUID) |
声明「心率测量」特征值的属性 | 0x0011 |
| 心率测量特征值(Characteristic Value) | 0x2A37(心率测量值UUID) |
存储实时心率数据,用于通知 | 0x0012 |
| 客户端特征值配置描述符(CCCD) | 0x2902(GATT标准CCCD UUID) |
控制通知/指示的开关 | 0x0013 |
| 身体传感器位置特征值声明 | 0x2803 |
声明「传感器位置」特征值 | 0x0014 |
| 身体传感器位置特征值 | 0x2A38 |
存储佩戴位置(手腕) | 0x0015 |
| 心率控制点特征值声明 | 0x2803 |
声明「心率控制」特征值 | 0x0016 |
| 心率控制点特征值 | 0x2A39 |
接收手机写入的控制命令 | 0x0017 |
注:句柄是按属性在GATT数据库中的顺序连续分配,不同手表/协议栈的句柄数值不同,但结构完全一致。
2. 客户端(手机)如何用句柄操作心率服务?
场景1:订阅心率实时通知(最核心场景)
手机要接收手表的实时心率,必须通过句柄操作CCCD,步骤如下:
- 发现服务:手机通过「服务发现」流程,拿到心率服务的属性列表,获取到:
- 心率测量特征值句柄:
0x0012 - 对应CCCD句柄:
0x0013
- 心率测量特征值句柄:
- 写入CCCD开启通知:手机向CCCD句柄
0x0013写入0x0001(开启通知)// 手机端GATT写操作:指定CCCD句柄,写入开启通知命令 bt_gatt_write(conn, 0x0013, (uint8_t[]){0x01, 0x00}, 2); - 手表端响应:GATT服务器检查
0x0013句柄的权限,允许写入后,将CCCD值设为0x0001 - 发送通知:手表每次采集到心率数据,通过心率测量特征值句柄
0x0012向手机发送通知// 手表端GATT通知操作:指定心率特征值句柄,发送心率数据 bt_gatt_notify(conn, 0x0012, heart_rate_data, sizeof(heart_rate_data));
场景2:读取身体传感器位置
手机要读取手表的佩戴位置,直接通过特征值句柄0x0015发起读请求:
// 手机端GATT读操作:指定传感器位置特征值句柄
bt_gatt_read(conn, 0x0015);
手表GATT服务器收到读请求后,匹配句柄0x0015,返回存储的位置数据(如0x02代表手腕)。
场景3:写入心率控制命令
手机要重置心率能量消耗,向心率控制点特征值句柄0x0017写入命令:
// 手机端GATT写操作:指定控制点句柄,写入重置命令
bt_gatt_write(conn, 0x0017, (uint8_t[]){0x01}, 1);
3. 句柄 vs UUID:为什么必须同时存在?
| 对比项 | UUID | 句柄(Handle) |
|---|---|---|
| 本质 | 功能的「全球唯一身份证号」 | 属性的「本地门牌号」 |
| 作用 | 让双方识别「这是什么功能」 | 让客户端定位「这个功能在服务器的哪个位置」 |
| 生命周期 | 全球固定,所有设备一致 | 服务器初始化时分配,重启后可能变化 |
| 操作依赖 | 发现服务时用UUID匹配功能 | 所有读写/通知操作必须用句柄 |
举个生活例子:
- UUID = 「XX市XX路XX号 心率服务专卖店」(全球唯一地址)
- 句柄 = 「专卖店内 1号柜台(心率测量)、2号柜台(位置)」(店内唯一编号)
手机先通过UUID找到「心率服务专卖店」,再通过句柄找到「1号柜台」,才能买东西(读/写数据)。
4. 手表端(服务端)句柄的分配逻辑(NuttX/Zephyr协议栈)
以NuttX BLE协议栈为例,心率服务的属性定义代码如下,句柄由协议栈自动分配:
// 心率服务属性定义
static const struct bt_gatt_attr s heart_rate_attrs[] = {
// 1. 服务声明:句柄0x0010(协议栈自动分配)
BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_16(BT_UUID_HRS_VAL)),
// 2. 心率测量特征值声明:句柄0x0011
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_16(BT_UUID_HRS_MEASUREMENT_VAL),
BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ),
// 3. 心率测量特征值:句柄0x0012(核心数据句柄)
BT_GATT_CCC(NULL, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), // 4. CCCD:句柄0x0013
// 5. 身体传感器位置特征值:句柄0x0015
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_16(BT_UUID_HRS_BODY_SENSOR_VAL),
BT_GATT_CHRC_READ,
BT_GATT_PERM_READ),
// 6. 心率控制点特征值:句柄0x0017
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_16(BT_UUID_HRS_CNTL_POINT_VAL),
BT_GATT_CHRC_WRITE,
BT_GATT_PERM_WRITE),
};
// 注册服务:协议栈按数组顺序分配句柄
BT_GATT_SERVICE_DEFINE(heart_rate_svc, heart_rate_attrs);
- 协议栈初始化时,按属性在数组中的顺序,从0x0001开始连续分配句柄
- 服务端代码中不需要手动指定句柄,由协议栈自动管理,仅在发送通知时需要获取句柄
// 手表端获取心率测量特征值句柄,发送通知 uint16_t handle = bt_gatt_attr_handle(&heart_rate_attrs[2]); bt_gatt_notify(conn, handle, data, len);
三、核心总结(一句话记住)
在蓝牙手表心率服务中:
- 句柄是GATT服务器给每个属性分配的「唯一门牌号」,按属性顺序自动生成
- 手机(客户端)先通过UUID找到心率服务,再通过句柄定位具体特征值
- 订阅心率通知、读取位置、写入控制命令,所有操作都必须通过句柄完成
- UUID是「功能名」,句柄是「操作入口」,二者缺一不可
要不要我给你补一份完整的心率服务GATT交互时序图,从服务发现到通知开启的全流程,让句柄的流转更直观?
浙公网安备 33010602011771号