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,步骤如下:

  1. 发现服务:手机通过「服务发现」流程,拿到心率服务的属性列表,获取到:
    • 心率测量特征值句柄:0x0012
    • 对应CCCD句柄:0x0013
  2. 写入CCCD开启通知:手机向CCCD句柄0x0013写入0x0001(开启通知)
    // 手机端GATT写操作:指定CCCD句柄,写入开启通知命令
    bt_gatt_write(conn, 0x0013, (uint8_t[]){0x01, 0x00}, 2);
    
  3. 手表端响应:GATT服务器检查0x0013句柄的权限,允许写入后,将CCCD值设为0x0001
  4. 发送通知:手表每次采集到心率数据,通过心率测量特征值句柄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);
    

三、核心总结(一句话记住)

在蓝牙手表心率服务中:

  1. 句柄是GATT服务器给每个属性分配的「唯一门牌号」,按属性顺序自动生成
  2. 手机(客户端)先通过UUID找到心率服务,再通过句柄定位具体特征值
  3. 订阅心率通知、读取位置、写入控制命令,所有操作都必须通过句柄完成
  4. UUID是「功能名」,句柄是「操作入口」,二者缺一不可

要不要我给你补一份完整的心率服务GATT交互时序图,从服务发现到通知开启的全流程,让句柄的流转更直观?

posted @ 2026-04-09 00:37  wzm888  阅读(0)  评论(0)    收藏  举报