BLE基础:GATT服务、连接建立与数据流
做BLE开发,如果你不理解GATT,那就像写Android不知道四大组件一样。我个人习惯先梳理清楚GATT这个“抽象层”,再动手写代码。今天我们就来彻底搞懂BLE的通信模型。
GATT服务模型:Server与Client
GATT的全称是Generic Attribute Profile。它定义了BLE设备之间如何交换数据。说白了,GATT就是一套规则:谁是数据提供者,谁是数据消费者。
- GATT Server:持有数据的一方,通常是被连接的设备(如传感器、心率带)。
- GATT Client:发起读写请求的一方,通常是手机或平板。
你可能要问:那一个设备能不能同时做Server和Client?可以。比如手机既接收心率数据(Client),又向外发送控制指令(Server)。但在大多数场景下,手机是Client,外设是Server。
讲个我自己的教训。有一次项目里,外设同时提供了多个Service,我忘了区分哪个Service对应哪个功能,结果连上去后该读的数据一直读不到。后来把每个Service的UUID打印出来才定位到问题。从那时起,我写代码前一定会先画一张GATT层级图。
GATT层级结构
一个典型的GATT Server结构如下:
Server
└─ Service (UUID: 0x180D)
├─ Characteristic (UUID: 0x2A37) // 心率测量
│ ├─ Value
│ ├─ Properties: Notify
│ ├─ Client Characteristic Configuration Descriptor (CCCD)
│ └─ User Description Descriptor
└─ Characteristic (UUID: 0x2A38) // 身体传感器位置
├─ Value
└─ Properties: Read
每个Service包含一个或多个Characteristic。每个Characteristic又有Value和Descriptor。Descriptor用来描述这个特征的行为,比如是否启用通知。
关键点:Characteristic的Properties决定了你能对它做什么。Read、Write、Notify、Indicate是常见的四种。Notify不需要应用层确认,Indicate则需要。我在项目中遇到过一个设备,它的蓝牙芯片实现Indicate时丢包率很高,后来改成Notify就稳定了。
连接建立过程
BLE连接不是一步到位的。它分几个阶段:
- 扫描(Scanning):Client发现广告包,获取设备地址和名称。
- 连接(Connect):Client发起连接请求。连接后,设备进入连接状态。
- 服务发现(Service Discovery):Client调用API获取Server的Service、Characteristic列表。这一步很关键,因为只有服务发现完成后,你才知道可以操作哪些特征。
- MTU协商(MTU Exchange):双方协商数据包的最大长度。默认是23字节,协商后可以提升到512甚至更多。
- 数据交互:读写、通知、指示。
你想想看,如果跳过MTU协商,每次只能发20字节的有效数据(因为ATT层有3字节头部)。我曾经在一个固件升级项目里,因为没做MTU协商,几百KB的数据包被拆成上千个小包,传输时间长到用户直接拔设备。嗯,从那之后我再也不会忘记手动请求MTU了。
数据流:ATT层交互
GATT之上是ATT(Attribute Protocol)。所有的读写、通知最终都通过ATT PDU在连接上传输。一个典型的通知流程:
- Client收到通知后,会收到一个ATT_HANDLE_VALUE_NOTIFICATION PDU。
- 如果Characteristic的Properties是Indicate,Server会等Client返回确认(ATT_HANDLE_VALUE_CFM),否则直接发送。
代码层面,在Android上进行服务发现和启用通知的典型片段:
// 连接成功后,在onConnectionStateChange中调用
bluetoothGatt.discoverServices()
// 服务发现完成回调
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
val service = gatt.getService(UUID.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
val characteristic = service.getCharacteristic(UUID.fromString("00002A37-0000-1000-8000-00805F9B34FB"))
// 启用通知
gatt.setCharacteristicNotification(characteristic, true)
val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
常见坑:启用通知时,必须同时调用setCharacteristicNotification和writeDescriptor(写入CCCD)。只写其中一个的话,设备要么不给你发数据,要么系统回调收不到。我见过不少新人只写了前半句,然后跑来问为什么收不到通知。
GATT与数据流核心概念速查
为了方便记忆,我整理了一个速查表:
| 概念 | 说明 | 我的建议 |
|---|---|---|
| Service | 功能集合,如心率、电池 | 先罗列所有Service,用UUID字典 |
| Characteristic | 具体数据点,包含值和属性 | 注意区分Notify/Indicate |
| Descriptor | 特征描述,常用CCCD | 启用通知必写CCCD |
| MTU | 单包最大长度 | 连接完后立即请求协商 |
| 连接间隔 | 设备广播和连接参数 | 影响功耗和吞吐量 |
例如,当你需要写入大数据时,如果MTU没协商,就会拆成很多个小包,丢包概率大增。我建议在onConnectionStateChange后立即调用requestMtu(512),然后在onMtuChanged回调里继续下一步。
小技巧:如果不知道设备的MTU上限,可以先请求一个较大的值(如512),如果设备不支持,它会在onMtuChanged返回实际支持的值。我习惯用这个方法来探测设备能力。