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连接不是一步到位的。它分几个阶段:

  1. 扫描(Scanning):Client发现广告包,获取设备地址和名称。
  2. 连接(Connect):Client发起连接请求。连接后,设备进入连接状态。
  3. 服务发现(Service Discovery):Client调用API获取Server的Service、Characteristic列表。这一步很关键,因为只有服务发现完成后,你才知道可以操作哪些特征。
  4. MTU协商(MTU Exchange):双方协商数据包的最大长度。默认是23字节,协商后可以提升到512甚至更多。
  5. 数据交互:读写、通知、指示。

你想想看,如果跳过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返回实际支持的值。我习惯用这个方法来探测设备能力。

BLE连接建立与GATT数据流 BLE连接建立与数据流 ① 扫描(Scanning) ② 连接(Connect) ③ 服务发现 ④ MTU协商 ⑤ 数据交互 GATT Server 内部结构 Service (0x180D 心率) Characteristic (0x2A37) CCCD Descriptor 服务发现完成后,Client才能知道可以操作哪些特征;MTU每次连接都需要重新协商。 数据交互阶段使用ATT PDU传输,分为Read、Write、Notify、Indicate四种模式。