蓝牙应用层协议GATT
文章目录
- 一、GATT 是什么?
- 二、GATT 在 BLE 通信中的位置
- 三、GATT 的关键角色:客户端 vs. 服务器
- 四、GATT 的数据结构:层层嵌套
- 1. **Profile(配置文件)**
- 2. **Service(服务)**
- 3. **Characteristic(特征值)**
- 五、一个实际的通信流程(以心率手环为例)
- 总结
- 图表关键点解读
- 总结:Handle vs UUID
- 核心要点
- 详细解释与类比
- 标准 UUID vs. 自定义 UUID
- 总结
一、GATT 是什么?
GATT的全称是Generic Attribute Profile,即通用属性协议。
简单来说,它是低功耗蓝牙(BLE)设备之间进行通信的核心数据交换协议。它定义了两个 BLE 设备在建立连接后,如何组织和传输数据。
一个非常形象的比喻是:
GATT 就像是我们去图书馆借书所遵循的一整套规则。
- 图书馆本身就是你的 BLE 设备。
- 书架的分类(比如“计算机类”、“文学类”)就是Service(服务)。
- 具体的每一本书(比如《蓝牙开发入门》)就是Characteristic(特征值),书里的内容就是特征值所包含的数据。
- 图书馆的目录检索系统和借阅流程就是GATT协议本身。
这套规则(GATT)规定了书如何摆放、如何查找、如何借阅和归还。没有这套规则,你就无法有效地从图书馆获取信息。
二、GATT 在 BLE 通信中的位置
为了理解 GATT 的角色,我们看一下简化的 BLE 通信栈:
+------------------------------------+ | 你的手机 App | <-- 你交互的应用层 +------------------------------------+ | GATT 客户端 | <-- 发起请求的一方(如手机) +------------------------------------+ | ATT 协议层 | <-- 负责数据封装的底层协议 +------------------------------------+ | L2CAP 逻辑层 | +------------------------------------+ | PHY 物理层 (无线电波) | +------------------------------------+- ATT(Attribute Protocol): 是 GATT 的基础,它定义了数据的格式(即“属性”)。GATT 是在 ATT 之上构建的高级别规范,它赋予了这些属性具体的含义和组织结构。
- GATT 依赖于连接: GATT 通信必须在两个设备建立 BLE 连接之后才能进行。它不像广播那样可以一对多。
三、GATT 的关键角色:客户端 vs. 服务器
在 GATT 通信中,有两个明确的角色,这与我们熟悉的客户端-服务器模型(如网页浏览)非常相似:
| 角色 | GATT 服务器 | GATT 客户端 |
|---|---|---|
| 比喻 | 图书馆 | 读者 |
| 描述 | 数据的持有者和提供者 | 数据的请求者和使用者 |
| 典型设备 | 心率手环、智能门锁、传感器 | 手机、平板、电脑 |
| 职责 | 存储数据,并响应客户端的请求 | 发现服务器上的数据,并读写它们 |
| 主动/被动 | 被动(等待命令) | 主动(发起命令) |
- 一个 BLE 连接中,一方是 GATT 服务器,另一方是 GATT 客户端。
- 手机 App 通常作为GATT 客户端,而去读写手环等外设,外设就是GATT 服务器。
四、GATT 的数据结构:层层嵌套
这是 GATT 最核心的概念。数据是以一种层级化、嵌套的方式组织的,如下图所示,它清晰地展示了从配置文件(Profile)到特征值(Characteristic)的包含关系:
下面我们来详细解释图中的每一层:
1.Profile(配置文件)
- 这不是一个实际在总线上传输的数据实体,而是一个功能规范文档。
- 它定义了一个具体的应用场景需要哪些Service的组合。
- 例如,心率 Profile规定:一个设备必须包含心率 Service,并且该 Service 必须包含心率测量 Characteristic。
2.Service(服务)
- 服务是一个相关功能的数据集合。它像一个容器,里面装着多个实现某种功能的 Characteristic。
- 每个 Service 都有一个唯一的 128 位 UUID来标识。
- 标准 Service(如电池服务、设备信息服务)使用蓝牙技术联盟定义的16位短UUID(如
0x180F代表电池服务)。 - 设备制造商也可以定义自己的自定义 Service,使用 128 位的长 UUID。
- 例子:电池服务(
0x180F)里包含了“电池电量”这个特征值。
3.Characteristic(特征值)
- 特征值是 GATT 数据库中的最小数据单元,是你实际读写数据的地方。
- 一个 Characteristic 包含三个部分:
- Value(值):实际的数据本身(例如,心率数值、电池电量百分比)。这就是你读取和写入的对象。
- Properties(属性):定义了可以对这个 Characteristic 进行什么操作。它是一个位掩码,常见的有:
- Broadcast:广播
- Read:可读
- Write Without Response:无响应写
- Write:带响应写
- Notify:通知(不需要客户端 ACK,主动上报) Indicate:指示(需要客户端 ACK 确认)
- Authenticated Signed Writes:签名写
- Descriptors(描述符):描述 Characteristic 的元信息。最最重要的是:
- CCCD(客户端特征配置描述符):当 Characteristic 的属性包含
Notify或Indicate时,必须存在。开启 Notify/Indicate 就是往这个描述符写值。- 0x0000:关闭上报
- 0x0001:开启 Notify
- 0x0002:开启 Indicate
- Characteristic User Description(0x2901):可读字符串,特征的文字说明
- Characteristic Format(0x2904):说明 value 数据类型、单位
- CCCD(客户端特征配置描述符):当 Characteristic 的属性包含
五、一个实际的通信流程(以心率手环为例)
- 手机(客户端)与手环(服务器)建立 BLE 连接。
- 手机发起:发现服务(“嘿,你有什么功能?”)。
- 手环回复:我这里有
心率服务(0x180D)和电池服务(0x180F)。 - 手机针对心率服务:发现特征(“你的心率服务里有什么数据?”)。
- 手环回复:我有
心率测量特征(0x2A37),它的属性是Notify。 - 手机向
心率测量特征下面的CCCD 描述符写入0x0001(“请在有新心率数据时通知我”)。 - 手环的心率数据发生变化时,就会主动向手机发送一个包含新数据的“通知”包,而无需手机再次询问。
- 手机的 GATT 客户端回调函数(如
onCharacteristicChanged)会收到这个数据并更新界面。
具体层级关系如下:
Service: Heart Rate (0x180D) └─ Characteristic: Heart Rate Measurement (0x2A37) ├─ Value: [0x00, 0x4B] # 心率 75 bpm ├─ Properties: Notify └─ Descriptor: CCCD └─ 用于开启/关闭心率通知总结
- GATT是 BLE 通信的数据交互规则。
- 它基于客户端-服务器模型。
- 数据采用Profile -> Service -> Characteristic的层级结构组织。
- Characteristic是数据操作的基本单元,通过Notify/Indicate属性实现服务器主动推送,这是 BLE 应用中最常见的模式。
理解了 GATT,你就掌握了 BLE 应用开发的精髓。
这张图清晰地展示了层级关系中每一个关键元素的标识符:
图表关键点解读
服务层 (Service Level)
- 服务声明 (Service Declaration):
- Handle
0x0001: 这是整个服务的"门牌号"。 - UUID
0x2800: 这是一个固定的类型,表示"这是一个主服务声明"。 - Value UUID
0x180D: 这是服务声明所包含的值,它定义了这是一个什么样的服务。0x180D代表"心率服务"。
- Handle
- 服务声明 (Service Declaration):
特征层 (Characteristic Level)
- 特征声明 (Characteristic Declaration):
- Handle
0x0002: 特征声明的门牌号。 - UUID
0x2803: 固定的类型,表示"这是一个特征声明"。 - Value: 这个声明包含的值比较复杂,它打包了三个信息:该特征的操作属性、该特征值的句柄、该特征的UUID。
- Handle
- 特征值 (Characteristic Value):
- Handle
0x0003:这是核心!读/写/通知操作的实际数据对象。 - UUID
0x2A37: 定义了这是什么数据,这里是"心率测量值"。 - Properties: 定义了允许的操作,这里是
Notify和Read。
- Handle
- 特征声明 (Characteristic Declaration):
描述符层 (Descriptor Level)
- CCCD 描述符:
- Handle
0x0004: 描述符的门牌号。 - UUID
0x2902: 固定的类型,表示"这是一个客户端特征配置描述符"。
- Handle
- CCCD 描述符:
总结:Handle vs UUID
通过这张图,我们可以清晰地总结:
| 元素 | Handle (句柄) | UUID (通用唯一识别码) |
|---|---|---|
| 是什么 | 数据的内存地址 | 数据的类型说明 |
| 特点 | 简短的数字,在设备连接后动态发现 | 较长的字符串,用于静态定义功能 |
| 作用 | 定位: “数据在哪里?” | 识别: “这是什么数据?” |
| 类比 | 图书馆的索书号 | 书的ISBN号或分类 |
通信过程:
当手机App想要读取身体传感器位置时,底层发生的其实是:
- 应用层通过
UUID 0x2A38请求。 - 蓝牙栈查找之前发现的"地图",找到
UUID 0x2A38对应的Handle是0x0006。 - 蓝牙栈向设备发送一个ATT协议命令:
READ_REQ(handle=0x0006)。 - 设备回复:
READ_RSP(handle=0x0006, value=0x03)(0x03代表"胸部")。
希望这张综合了handle和UUID的图,能让您对GATT数据库的结构有更透彻的理解!
是的,特征值(Characteristic)绝对有 UUID,而且这是它的核心身份标识!
在上面的图表中,0x2A37和0x2A38就是特征值的 UUID。可能之前的解释有些绕,让我们抛开复杂的结构,直接聚焦于特征值本身。
核心要点
每一个特征值都有两个非常重要的 UUID:
用于“特征声明”的 UUID:
- 它的值是固定的
0x2803。这个 UUID 就像一个大声的宣告:“大家好,这里是一个特征值的开始!”。 - 它所在的这个“声明”本身,有一个句柄(比如
0x0002)。
- 它的值是固定的
特征值本身的 UUID:
- 这是真正定义这个特征值功能的 UUID。它告诉我们这个特征值是什么,能干什么。
- 它存储在特征声明所指向的特征值对象中,这个对象有自己的句柄(比如
0x0003)。
为了更清晰地理解这一点,下图直观地展示了这两个 UUID 如何共同定义一个特征值:
详细解释与类比
让我们用公司职位来类比,这更容易理解:
- 服务(Service)=部门(如:销售部
0x180D) - 特征声明(Declaration)=职位公告牌(上面写着“这里是销售部的一个职位”,牌子编号是
0x0002) - 特征值的 UUID=具体的职位名称(如:“销售总监”
0x2A37) - 特征值的句柄=该职位人员的工位位置(如:3号工位
0x0003)
当你需要和“销售总监”沟通(读写数据)时,你最终需要找到的是他的工位(句柄),而“销售总监”这个职位名称(特征值UUID)是帮助你找到正确工位的依据。
标准 UUID vs. 自定义 UUID
和服務一样,特征值的 UUID 也分为标准值和自定义值:
| 类型 | 示例 UUID | 示例功能 | 说明 |
|---|---|---|---|
| 标准特征值 | 0x2A37 | 心率测量 | 由蓝牙技术联盟定义,确保不同厂商设备互通。 |
| 标准特征值 | 0x2A19 | 电池电量 | 一个百分比(0-100%)的数字。 |
| 自定义特征值 | XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX | 厂商特定功能 | 由设备制造商自定义,用于实现特殊功能。 |
总结
是的,特征值有UUID,而且至关重要:
- 特征声明 UUID (
0x2803):是一个固定的信号,标志着“这里有一个特征”。 - 特征值 UUID (如
0x2A37):是特征的核心身份,定义了它的数据类型和功能。 - 句柄 (如
0x0003):是特征值在设备上的实际地址,用于高效通信。
在应用开发中,你通过特征值的 UUID(如0x2A37) 来寻找和操作它,底层蓝牙栈会帮你找到对应的句柄来完成实际的读写操作。
