N年前在一个支付公司做过2.5年的蓝牙开发,当时的项目是通过蓝牙连接mpos机进行刷卡支付。
APP中业务交互逻辑是比较复杂,当时APP的核心难以解决的痛点:逻辑比较混乱,很难从头到尾排查清楚一个完整流程。同时也会出现偶现的不好解决的bug---当时项目的架构是将蓝牙与VC交互放到了基类VC中,机具蓝牙交互则使用了多个单例通过代理和通知监听实现,由于是单例所以会偶现VC未正常释放也会出现UI响应在不该出现的页面。
我经过持续大约半年的时间完成针对蓝牙交互模块重构成了一个组件库,APP中刷卡/查余额/各类交易类型的各个VC中关于蓝牙交互的逻辑也在这基础上完成重构,极大简化了逻辑提升排查效率,也降低了极端异常的功能展示问题。(不影响业务开发的情况下半年)
下面我针对过往的这次做个回顾总结。
一、重构的蓝牙SDK架构
核心诉求:基于6厂商蓝牙SDK封装蓝牙连接交互与业务逻辑完成机具扫描连接、激活绑定、N个刷卡交易场景等。
实现思路:
- 工厂方法模式封装各厂商SDK提供统一交互接口,统一管理单例类完成各业务发起后内部完成各操作并在所有流程节点回调给业务侧。
- 蓝牙基础操作抽取在基类,不同业务场景通过策略模式分发管理其发起及蓝牙回调业务逻辑处理。
- 单例类保存机具、交易等基本信息作为上下文,专门的网络层处理请求事宜。
1. 核心文件
| 文件 | 职责 |
|---|---|
BluetoothManager.h/.m |
对外门面。暴露蓝牙扫描、连接、断开、固件更新、刷卡交易等能力;内部编排 CoreBluetooth、机具请求层、网络请求和业务回调。 |
BLERequestManager.h/.m |
机具网络请求/厂商 SDK 适配层。根据设备名称选择不同厂商 SDK,负责打开设备、获取设备信息/SN/流水号、更新 WorkKey/IC 参数、刷卡、二次授权、取消交易。 |
BLEInfo.h/.m |
全局上下文单例。保存登录用户信息、HTTP key、用户机具列表、当前连接机具、最近厂商接口、最近操作状态、刷卡参数。 |
BLEOperatDelegate.h |
BluetoothManager中对业务层的回调协议,包含扫描、连接、参数更新、刷卡、签名、交易成功/失败等回调。 |
BLERequestManagerDelegate.h |
BLERequestManager 对上层的回调协议,包含打开机具、关闭机具、更新参数、刷卡结果、无键盘机具输密等回调。 |
SwipAPI.h/.m |
刷卡 SDK 静态入口。负责用户注册、网络初始化、连接状态查询、断开、清除数据、App 回前台连接状态校验。 |
2. 内部架构
SwipBaseDel 子类] --> BM[BluetoothManager
对外门面] BM --> CB[CoreBluetooth
CBCentralManager] BM --> RM[BLERequestManager
厂商 SDK 适配] BM --> HTTP[SwipHttpTool
AA008 / AA124 / AA006 / AA010 等] BM --> INFO[BLEInfo
全局状态 / 当前连接机具 / 交易参数] RM --> INFO RM --> SDK[厂商 SDK / IRequestPosInteface
新大陆 / 联迪 / 天谕 / 中磁 / 华智融等] RM --> SQL[SwipSQLUtils
IC 参数 / 签购单缓存] BM --> CALLBACK[BLEOperatDelegate
扫描 / 连接 / 更新 / 刷卡流程节点及结果回调] CALLBACK --> UI
3. 业务 Delegate 关系
通用扫描/连接/更新/弹窗逻辑] Base --> Pay[YSTSwipPaymentDel
收款] Base --> Web[YSTSwipForWebNeedDel
H5收款/订单] Base --> Recharge[YSTSwipRechargeDel
钱包充值] Base --> Order[YSTSwipOrderPayDel
订单支付] Base --> Credit[YSTSwipCreditCardRepaymentDel
信用卡还款] Base --> Balance[YSTSwipAccountBlanceDel
查余额] Pay --> BM[YSTBluetoothManager] Web --> BM Recharge --> BM Order --> BM Credit --> BM Balance --> BM
4. 业务复杂度举例-连接
代码中的"连接成功"不是蓝牙配对成功,而是以下链路全部完成:
requestOpenDevice打开设备。requestDeviceInfo获取设备信息和终端号。requestDeviceSN获取 SN、PN、APP 版本、厂商信息等。requestDeviceTrace获取交易流水号。YSTBluetoothManager再发AA008校验用户和机具关系。- 如需更新 WorkKey / IC 参数,则更新完成后才回调
didAllsetMpos。
二、系统蓝牙基本交互回顾
1、基本概念及系统架构
蓝牙是一种短距离无线通信技术 ,用于设备之间的小数据量、低功耗、近距离传输。在 iOS 开发中,主要指 BLE(蓝牙低功耗) ,通过 CoreBluetooth 框架实现。
1.1 核心角色
| 概念 | 解释 | 类比 |
|---|---|---|
| Central(中心设备) | 主动扫描、连接、读写数据的设备 | 手机 App(主动发起) |
| Peripheral(外设) | 广播自己的存在,响应中心设备 | 智能手表、心率带、打印机 |
| Service(服务) | 一组功能的集合,一个外设可以有多个 Service | 一个"功能模块" |
| Characteristic(特征) | 服务下的具体数据点,是读写操作的最小单位 | 一个"数据字段" |
| UUID | 标识 Service 和 Characteristic 的唯一 ID | 门牌号 |
| 广播包 | 外设定期发送的数据,包含设备名、服务 UUID 等 | "我在,来找我吧" |
1.2 层次结构
css
Peripheral(外设)
└── Service 1(服务)
├── Characteristic A(特征-可读)
├── Characteristic B(特征-可写)
└── Characteristic C(特征-可通知)
└── Service 2(服务)
└── Characteristic D(特征-可读)
css
[初始化] → [扫描] → [连接] → [发现服务] → [发现特征] → [读写/通知] → [断开]
1.3 大体API使用情况
扫描 用
scanForPeripherals,连接 用connectPeripheral,通信 通过CBPeripheral的discoverServices→discoverCharacteristics→writeValue/setNotifyValue实现。
2、详细代码与说明
2.1 初始化
swift
import CoreBluetooth
class BluetoothManager: NSObject, CBCentralManagerDelegate {
var centralManager: CBCentralManager!
var connectedPeripheral: CBPeripheral?
override init() {
super.init()
// 初始化中心设备,会触发 centralManagerDidUpdateState
centralManager = CBCentralManager(delegate: self, queue: nil)
}
// 蓝牙状态回调(必须实现)
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
print("蓝牙已开启,可以扫描")
case .poweredOff:
print("蓝牙已关闭")
case .unauthorized:
print("未授权")
case .unsupported:
print("设备不支持蓝牙")
default:
break
}
}
}
关键点:
queue: nil表示回调在主线程;传自定义 queue 则会在子线程- 必须等
.poweredOn才能开始扫描
2.2 扫描外设
swift
func startScan() {
guard centralManager.state == .poweredOn else { return }
// 方式1:扫描所有设备
centralManager.scanForPeripherals(withServices: nil, options: nil)
// 方式2:只扫描特定服务的设备(推荐,省电)
let serviceUUIDs = [CBUUID(string: "180D")] // 心率服务
centralManager.scanForPeripherals(withServices: serviceUUIDs, options: [
CBCentralManagerScanOptionAllowDuplicatesKey: false // 不重复上报
])
}
// 扫描到设备后的回调
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber) {
print("发现设备: \(peripheral.name ?? "无名"), RSSI: \(RSSI)")
// 根据名称或信号强度筛选
if peripheral.name == "MyDevice" {
// 保存引用,否则会被释放
self.connectedPeripheral = peripheral
// 停止扫描,省电
centralManager.stopScan()// 实际使用时建议等连接成功再停止。
// 发起连接
centralManager.connect(peripheral, options: nil)
}
}
关键点:
peripheral需要强引用保存,否则会释放导致连接失败- 扫描到目标后立即
stopScan(),省电 RSSI绝对值越小信号越强(-50 比 -80 强)
2.3 连接外设
swift
// 连接成功
func centralManager(_ central: CBCentralManager,
didConnect peripheral: CBPeripheral) {
print("连接成功")
peripheral.delegate = self // 设置代理,用于后续服务发现
// 开始发现服务
peripheral.discoverServices(nil) // nil = 所有服务
}
// 连接失败
func centralManager(_ central: CBCentralManager,
didFailToConnect peripheral: CBPeripheral,
error: Error?) {
print("连接失败: \(error?.localizedDescription ?? "")")
// 重试逻辑
}
// 连接断开
func centralManager(_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?) {
print("断开连接")
// 尝试重连
centralManager.connect(peripheral, options: nil)
}
关键点:
- 必须设置
peripheral.delegate = self - 连接成功后要调用
discoverServices,否则无法通信
2.4 发现服务与特征
swift
// 发现服务
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let services = peripheral.services else { return }
for service in services {
print("发现服务: \(service.uuid)")
// 发现服务下的特征
peripheral.discoverCharacteristics(nil, for: service)
}
}
// 发现特征
func peripheral(_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?) {
guard let characteristics = service.characteristics else { return }
for characteristic in characteristics {
print("发现特征: \(characteristic.uuid)")
// 根据 UUID 判断特征类型
switch characteristic.uuid.uuidString {
case "FFE1": // 写入特征(发送数据给设备)
self.writeCharacteristic = characteristic
case "FFE2": // 通知特征(接收设备数据)
self.notifyCharacteristic = characteristic
// 开启通知
peripheral.setNotifyValue(true, for: characteristic)
default:
break
}
}
}
关键点:
- 必须按层级:先发现服务 → 再发现特征
- 特征有不同属性:
.read、.write、.notify、.indicate - 开启通知前,确认特征有
.notify属性
2.5. 通信:写数据
swift
func sendDataToDevice(data: Data) {
guard let characteristic = writeCharacteristic else { return }
// 方式1:带响应写入(会回调 didWriteValueFor)
connectedPeripheral?.writeValue(data,
for: characteristic,
type: .withResponse)
// 方式2:无响应写入(不回调,速度快,不保证送达)
// connectedPeripheral?.writeValue(data,
// for: characteristic,
// type: .withoutResponse)
}
// 写入成功/失败的回调(只有 .withResponse 才会触发)
func peripheral(_ peripheral: CBPeripheral,
didWriteValueFor characteristic: CBCharacteristic,
error: Error?) {
if let error = error {
print("写入失败: \(error)")
} else {
print("写入成功")
}
}
关键点:
- 数据大小限制:20 字节(MTU 默认 23,减去 3 字节头部)
- 大于 20 字节需要分包发送
2.6 通信:接收数据(通知)
swift
// 收到设备主动推送的数据
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
guard let data = characteristic.value else { return }
// 解析数据
let string = String(data: data, encoding: .utf8)
print("收到: \(string ?? "")")
// 更新 UI(注意切主线程)
DispatchQueue.main.async {
self.updateUI(with: data)
}
}
关键点:
- 必须先调用
setNotifyValue(true, for:)才能收到通知 - 数据回调在蓝牙队列,更新 UI 需要切主线程
2.7 断开连接
swift
func disconnect() {
// 先关闭通知(可选)
if let characteristic = notifyCharacteristic {
connectedPeripheral?.setNotifyValue(false, for: characteristic)
}
// 断开连接
if let peripheral = connectedPeripheral {
centralManager.cancelPeripheralConnection(peripheral)
}
}
3、完整时序图
arduino
App 系统蓝牙 外设
│ │ │
│──scanForPeripherals───────→│ │
│ │←──────广播包────────────│
│←──didDiscover──────────────│ │
│──stopScan─────────────────→│ │
│──connect──────────────────→│ │
│ │─────连接请求───────────→│
│ │←─────连接响应───────────│
│←──didConnect───────────────│ │
│──discoverServices─────────→│ │
│←──didDiscoverServices──────│ │
│──discoverCharacteristics──→│ │
│←──didDiscoverChars─────────│ │
│──setNotifyValue(true)─────→│ │
│ │─────开启通知────────────→│
│──writeValue───────────────→│ │
│ │─────数据───────────────→│
│ │←─────处理结果───────────│
│←──didWriteValue────────────│ │
│ │←─────主动推送───────────│
│←──didUpdateValue───────────│ │
三、蓝牙MTU数据传输
MTU(Maximum Transmission Unit最大传输单元) 分包传输是 BLE 开发中决定传输效率和稳定性的核心底层机制。针对涉及固件升级、实时音频流、文件传输的 IoT 业务场景,掌握 MTU 的原理和 iOS 上的处理策略至关重要。
可以从以下三个递进层面来讲解:
1. 基础层:什么是 MTU?为什么要分包?
-
定义 :MTU 是指链路层单次能够承载的最大有效数据载荷(Payload)。在标准 BLE 4.0/4.1 规范中,ATT_MTU 默认值为 23 字节。
-
数学账:23 字节中,还要扣除 ATT 协议头(3 字节)。
-
结论 :iOS App 单次
writeValue指令,实际能传输给外设(Peripheral)的有效数据只有 20 字节。 -
分包的根本原因 :当你需要下发一个 500KB 的固件文件(.bin)或一张图片时,必须将数据流切分成 N 个 20 字节 的小包,依次发送。如果不分包直接塞给系统,系统会因为数据超长而直接丢弃该包且不报错(静默失败)。
2. 演进层:MTU 协商与扩展(解决分包慢的问题)
如果每次只能发 20 字节,传 500KB 文件需要 25,000 次握手,耗时极长且容易出错。现代 BLE 通过MTU 协商机制来放大单包容量。
-
iOS 端的主动协商 : 在连接外设成功后,App 应主动发起 MTU 请求,争取更大的通道。
objectivec// 连接成功后,尝试将 MTU 协商到 512 字节(iPhone 6以上支持更大值) [peripheral maximumWriteValueLengthForType:CBCharacteristicWriteWithResponse]; -
iOS 协商策略 :
- 旧设备(iPhone 4S/5):上限约 158 字节。
- 较新设备(iPhone 6 及以上,支持 BLE 4.2+) :上限可达 512 字节 ,甚至部分外设支持 158 字节(LE Data Length Extension)。
- 关键收益 :单包容量从 20B 提升至 512B ,传输同一文件的交互次数减少了 96%,带宽利用率提升,丢包率显著下降。
3. 实战层:iOS 端分包传输的代码实现与绿联场景题
"MTU 协商成功是 512 字节,但你发 600 字节怎么办?代码怎么写?"
核心逻辑:NSData 切片 + 递归/队列发送。
场景模拟:发送智能摄像头的配置 JSON(假设长度 1200 字节,协商 MTU=512)
objectivec
// 1. 获取当前连接的最大写入长度(系统已考虑 ATT 头开销)
NSInteger maxLength = [peripheral maximumWriteValueLengthForType:CBCharacteristicWriteWithoutResponse];
// 2. 分包切片逻辑
NSData *totalData = ...; // 1200 字节的配置数据
NSInteger offset = 0;
while (offset < totalData.length) {
// 计算本次切片的长度(剩余长度 vs 最大允许长度)
NSInteger chunkSize = MIN(maxLength, totalData.length - offset);
NSData *chunk = [totalData subdataWithRange:NSMakeRange(offset, chunkSize)];
// 3. 写入外设
[peripheral writeValue:chunk
forCharacteristic:characteristic
type:CBCharacteristicWriteWithoutResponse]; // 无响应模式追求速度
offset += chunkSize;
}
业务场景下的关键注意点
在分包传输时,不能像上面代码那样无脑连续循环发送 。因为 BLE 是有空中拥塞控制的。
- 问题 :如果
while循环一口气把 1200 字节切成 3 个包瞬间推给系统,会导致 缓冲区溢出(Buffer Full) ,第三包可能被系统直接丢弃,外设只收到了不完整的 JSON 导致解析失败。 - 正确做法(队列 + 回调确认) :
- 采用
CBCharacteristicWriteWithResponse模式(可靠模式)。 - 不依赖
while循环 ,而是在didWriteValueForCharacteristic回调中,判断上一包是否成功写入。 - 成功后,再从发送队列中取出下一包继续发送。
- 这是一个典型的**排队机(Queue State Machine)**设计。
- 采用
4. 怎么保证数据完整性
把一个大文件切成有序的小包,通过序列号和校验机制,确保接收方能完整、有序地重组。
4.1 具体措施
1. 包序号 (Sequence Number) ------ 防止乱序与重复
每个分片必须携带一个自增的序号,这是重组的基础。
| 字段 | 大小 | 说明 |
|---|---|---|
Seq |
2 字节 | 当前包的序号(从 0 开始) |
Total |
2 字节 | 总包数(便于接收方预分配缓冲区) |
Payload |
N 字节 | 实际数据切片 |
接收方维护一个 expectedSeq,如果收到的包序号不等于期望值:
- 大于期望值:说明中间有包丢了,立即请求重传缺失的包。
- 小于期望值:可能是重传的重复包,直接丢弃。
2. 校验和 (Checksum / CRC) ------ 防止数据损坏
即使蓝牙底层有 CRC,应用层也必须再加一层校验。因为数据可能在硬件接收后、写入 Flash 前,因内存位翻转而损坏。
- 简单场景 :对整个分片数据做 XOR 异或校验 或 累加和校验。
- 严格场景 (固件升级):使用 CRC16 或 CRC32,放在包尾。接收方校验不通过则丢弃该包并请求重传。
3. 确认与重传机制 (ACK & Retransmission) ------ 核心
这是保证完整性的核心策略,有两种模式可选:
模式 A:停等协议 (Stop-and-Wait) ------ 简单可靠
- 发送方发一包,等待接收方回复 ACK,收到后再发下一包。
- 超时未收到 ACK,则重传当前包。
- 优点:实现简单,适合低速传输。
- 缺点:效率低,BLE 本身延迟就高,停等会让传输更慢。
模式 B:滑动窗口 / 批量确认 (适用于 BLE 高速传输)
- 发送方连续发送 N 个包(窗口大小),接收方收到后回复一个 位图 ACK,标识哪些包收到了。
- 发送方只需重传位图中标记为 0 的包。
- 优点:充分利用 BLE 连接间隔,大幅提升吞吐量。
- 场景适配 :固件升级时,通常采用 窗口大小为 1 的停等协议,因为固件写入 Flash 本身有延迟,发太快反而容易溢出。
4. 整体完整性校验 (Global Checksum / Hash) ------ 收尾验证
所有包传输完毕后,接收方需要对整个完整数据进行一次校验,确保拼接后的文件和发送方完全一致。
-
常见做法 :在传输开始前,先发送一个起始包,里面包含:
- 文件总大小
- 整个文件的 MD5 或 SHA-256 哈希值
-
收尾流程 :接收方收完所有包、拼接完成后,计算本地文件的哈希,与起始包中的哈希比对。一致则回复 传输完成 ,不一致则整包重传。
5.2 这种验证跟HTTPS的Hmac校验的区别?
HMAC的标准流程如下:
-
准备密钥 :双方预先共享一把相同的
Secret Key。 -
双重哈希:
- 内层哈希 :
Hash( (Key ⊕ 内层填充) + 原始消息 ) - 外层哈希 :
Hash( (Key ⊕ 外层填充) + 内层哈希结果 )
- 内层哈希 :
-
传输 :发送方将
原始消息和计算出的HMAC 值一起发给接收方。
场景假设: 智能摄像头收到一条 App 发来的指令: "格式化存储卡" 。
- 场景 A(无 HMAC) :如果攻击者劫持了 Wi-Fi 数据包,把内容改成 "关闭报警" ,并重新算了一个 MD5 填进去。摄像头收到后一看 MD5 是对的,就执行了。
- 场景 B(有 HMAC) :App 发出 "格式化存储卡" 时,会配合配对时生成的会话密钥 计算一个 HMAC。攻击者改了内容,但没有会话密钥 ,算不出新的 HMAC。摄像头收到后校验 HMAC 失败,直接丢弃危险指令并报警。
四、蓝牙ATT、GATT协议
在我们的蓝牙交互过程中,API的层次结构是下面的,其实这个设计背后的原因是蓝牙ATT、GATT协议.
Swift
Peripheral(外设)
└── Service 1(服务)
├── Characteristic A(特征-可读)
├── Characteristic B(特征-可写)
└── Characteristic C(特征-可通知)
└── Service 2(服务)
└── Characteristic D(特征-可读)
特征里面包含的属性CBCharacteristicProperties很多是跟ATT协议对照的:
Objective-C
@interface CBCharacteristic : CBAttribute
@property(readonly, nonatomic) CBCharacteristicProperties properties;
...
@end
typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) {
CBCharacteristicPropertyBroadcast = 0x01,
CBCharacteristicPropertyRead = 0x02,
CBCharacteristicPropertyWriteWithoutResponse = 0x04,
CBCharacteristicPropertyWrite = 0x08,
CBCharacteristicPropertyNotify = 0x10,
CBCharacteristicPropertyIndicate = 0x20,
CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40,
CBCharacteristicPropertyExtendedProperties = 0x80,
CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(10_9, 6_0) = 0x100,
CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(10_9, 6_0) = 0x200
};
下面来具体了解下各个协议
五、蓝牙ATT
全称属性协议(Attribute Protocol),是低功耗蓝牙(BLE)设备间进行数据交换的基础协议。它好比一个精简的"数据库访问协议",提供了发现、读取、写入和修改数据的基本操作集。
ATT协议是基于客户端/服务器(C/S)模型设计的,一次通信必然由客户端发起:
- 客户端 (Client) :通常是智能手机、平板等中心设备。它主动向服务器发送请求或命令,并处理服务器的响应或更新。
- 服务器 (Server) :通常是传感器、手环等外围设备。它负责存储和组织数据(作为属性),响应客户端的请求,并可根据需要主动发送通知或指示。
1. 数据的基石:属性 (Attribute)
属性是ATT协议的数据基本单元,由以下四个部分组成:
- 句柄 (Handle) :由服务器分配的16位数字 (
0x0001-0xFFFF),用作属性的唯一地址,客户端通过它来访问特定数据。 - 类型 (Type) :一个UUID(通用唯一标识符),用于说明该属性代表的数据种类(如心率、温度、设备名等)。
- 值 (Value) :数据本身,是属性的实际内容,可以是心跳值、温度读数、字符串等。
- 权限 (Permissions) :一组由更高层协议(如GATT)定义的安全规则,用于控制客户端对该属性的访问(例如,是否可读、是否需要加密连接)。
2. 六种通信报文 (PDUs) 详解
ATT协议定义了六种报文(PDU)类型,支撑起所有的数据交互。这六种报文因其确认机制(是否需要回复)不同,应用场景也各异:
| 报文类型 | 发送方向 | 特点 |
|---|---|---|
| 请求 (Request) | 客户端 → 服务器 | 一条必须得到"响应"的报文。 |
| 响应 (Response) | 服务器 → 客户端 | 对客户端"请求"的回复。 |
| 命令 (Command) | 客户端 → 服务器 | 客户端主动发送,无需服务器回复。 |
| 通知 (Notification) | 服务器 → 客户端 | 服务器主动推送数据,无需客户端确认。 |
| 指示 (Indication) | 服务器 → 客户端 | 服务器主动推送数据,必须得到客户端的"确认"。 |
| 确认 (Confirmation) | 客户端 → 服务器 | 对服务器"指示"的确认 |
基于这些报文,ATT定义了一套操作集(Opcode),完整列表如下:
| 功能类别 | 操作名称及操作码 (Opcode) | 功能描述 |
|---|---|---|
| 错误处理 | Error Response (0x01) | 当请求无效时,服务器返回的错误响应。 |
| MTU交换 | Exchange MTU Request (0x02) / Response (0x03) | 客户端与服务器交换各自能处理的最大传输单元(MTU)值。 |
| 查找信息 | Find Information Request (0x04) / Response (0x05) | 用于发现指定句柄范围内的属性及其类型。 |
| Find By Type Value Request (0x06) / Response (0x07) | 用于查找具有特定类型和值的属性。 | |
| 读取属性 | Read By Type Request (0x08) / Response (0x09) | 根据属性类型UUID读取属性值和句柄。 |
| Read Request (0x0A) / Response (0x0B) | 根据属性句柄读取具体的属性值。 | |
| Read Blob Request (0x0C) / Response (0x0D) | 用于分片读取一个很长的属性值。 | |
| Read Multiple Request (0x0E) / Response (0x0F) | 一次性读取多个已知句柄的属性值。 | |
| Read by Group Type Request (0x10) / Response (0x11) | 用于读取特定组类型(如主服务)的属性。 | |
| 写入属性 | Write Request (0x12) / Response (0x13) | 写入一个属性值,服务器必须回复确认。 |
| Write Command (0x52) | 写入一个属性值,服务器无需回复,效率更高。 | |
| Signed Write Command (0xD2) | 带数字签名的写入命令,用于不需要配对但需验证的场景。 | |
| Prepare Write Request (0x16) / Response (0x17) | 为可靠写入长属性值做准备,可提交或取消。 | |
| Execute Write Request (0x18) / Response (0x19) | 执行或取消之前所有Prepare Write请求的最终操作。 | |
| 队列式写入 | Prepare Write Request (0x16) / Response (0x17) | 与写入属性操作共用,用于实现长属性值的可靠写入。 |
| Execute Write Request (0x18) / Response (0x19) | ||
| 服务器推送 | Handle Value Notification (0x1B) | 服务器主动向客户端发送属性值,无需确认。 |
| Handle Value Indication (0x1D) / Confirmation (0x1E) | 服务器主动发送属性值,并需要客户端确认。 |
3. 工作模型:停止-等待与顺序协议
ATT是一个有状态、顺序性的协议,其请求/响应和指示/确认遵循"停止-等待"工作模型。这意味着在一个事务未完成前,不能开始下一个同类型事务。例如,客户端必须等服务器对其"读请求"做出"读响应"后,才能发起下一个请求。
4. 协议演进:从ATT到EATT
随着应用场景的复杂化,原始的ATT协议因为一次只能处理一个事务,逐渐成为性能瓶颈。因此,蓝牙5.2核心规范引入了增强型属性协议(EATT, Enhanced Attribute Protocol) 。
EATT的核心改进在于:
- 并发支持:允许在多个L2CAP(逻辑链路控制与适配层协议)通道上并行处理多个ATT事务。
- 动态MTU调整:允许连接建立后变更ATT最大传输单元(MTU)。
- 可靠性增强 :通过基于信用的流量控制,解决原始ATT在无流量控制下可能丢包的问题。
六、 GATT协议
GATT(Generic Attribute Profile,通用属性规范) 是基于ATT这套语法写出的"数据组织字典和交互规范"。它规定了数据如何组织成有意义的"服务",以及应用程序该如何使用 ATT 来访问这些数据。
- GATT 是建立在 ATT 之上的高层次规范,赋予数据以结构和服务语义。
- 它用 Service 、Characteristic 、Descriptor 三个核心概念将 ATT 数据库组织成功能模块。
- 客户端通过标准的发现 → 访问 → 使能流程,实现对设备功能的读取、控制和数据订阅。
- 与 ATT 的"无状态"原子操作不同,GATT 定义了有状态的交互过程(如发现、配置 CCCD),但这状态由上层维护,ATT 本身依然是事务性的。
ATT 管传输,GATT 管组织。理解了 ATT 的六种报文和操作,再结合 GATT 的服务/特征体系,你就掌握了 BLE 数据通信的核心。
1. GATT 与 ATT 的关系
ATT 只定义了"属性"这个基本单元(句柄、UUID、值、权限),以及对这些属性进行读、写、通知的原子操作。但仅凭 ATT 无法知道:
- 哪些属性代表同一个功能(如心率测量)?
- 哪个属性是该功能的配置开关?
- 属性之间有什么逻辑关系?
GATT 在 ATT 之上建立了一个分层的抽象模型 ,把所有属性归类为 服务(Service) 和 特征(Characteristic),并约定了一套标准的发现与访问流程。
2. GATT 的核心概念
1. 服务(Service)
一个服务代表设备上的一项逻辑功能(如心率服务、电池服务、设备信息服务)。它是一组相关特征和其他服务的容器。每个服务都用一个 UUID 来标识:标准服务由蓝牙技术联盟(SIG)分配 16 位 UUID(如心率服务为 0x180D),厂商自定义服务使用 128 位 UUID。
服务有两种类型:
- 主要服务(Primary Service):代表设备的主要功能。
- 次要服务(Secondary Service):由其他服务引用,提供辅助功能。
2. 特征(Characteristic)
特征是服务的核心,它包含一个特征值(Value) 和可选的描述符(Descriptor) ,以及一组特征属性(Properties) 和权限。
- 特征值:实际的数据,如心率测量值、电池电量百分比。
- 特征属性 :规定该特征支持哪些 ATT 操作,例如:
- Read:允许客户端读取
- Write:允许客户端写入
- Notify:允许服务器发送通知
- Indicate:允许服务器发送指示 这些属性直接映射到底层 ATT 报文的权限。
- 特征声明(Characteristic Declaration) :存储在 ATT 表中的一种特殊属性,其值包含了该特征的属性 、特征值句柄 以及特征 UUID。客户端通过读取这些声明来发现特征。
3. 描述符(Descriptor)
描述符提供关于特征值或特征的附加信息。常见的有:
- CCCD(客户端特征配置描述符):启用/禁用通知或指示。只有在该描述符被客户端正确写入后,服务器才会开始推送数据。
- CCFD(服务器特征格式描述符):描述特征值的数据格式、单位等。
- RCD(特征用户描述符):一个可读的字符串,用来描述特征(如"心率测量值")。
3. GATT 的数据层次结构
一个典型的 GATT 数据库看起来像这样:
scss
Profile(未在表中直接体现,是应用的顶层约定)
└── Service 1 (UUID: Battery Service 0x180F)
│ ├── Characteristic (UUID: Battery Level 0x2A19)
│ │ ├── Value (电池电量,可读、可通知)
│ │ └── Descriptor (CCCD,用于开启通知)
│ └── Characteristic (UUID: ...)
└── Service 2 (UUID: Heart Rate Service 0x180D)
├── Characteristic (UUID: Heart Rate Measurement 0x2A37, 可通知)
│ └── Descriptor (CCCD)
├── Characteristic (UUID: Body Sensor Location 0x2A38, 可读)
└── Characteristic (UUID: Heart Rate Control Point 0x2A39, 可写)
4. GATT 的通用操作流程
GATT 客户端通过与服务器建立连接后,按以下典型步骤进行交互,每一步都对应着底层的 ATT 操作:
-
服务发现
客户端使用
Read by Group Type Request遍历所有主要服务,获取其 UUID 和句柄范围。 -
特征发现
在某个服务的句柄范围内,使用
Read by Type Request(类型为特征声明 UUID0x2803)找出所有特征声明,从中提取特征值句柄、属性、特征 UUID。 -
特征值访问
- 如果特征是可读 的,客户端用
Read Request读取特征值句柄。 - 如果特征是可写 的,客户端用
Write Request或Write Command向特征值句柄写入数据。 - 如果特征是可通知/指示 的,客户端先通过
Write Request向对应的 CCCD 描述符写入0x0001(通知)或0x0002(指示)来使能推送。
- 如果特征是可读 的,客户端用
-
数据推送
使能后,当特征值发生变化,服务器会主动发送
Handle Value Notification或Handle Value Indication给客户端。 -
多字节传输处理
GATT 定义了一个长特征值 的概念。当特征值或描述符超过 ATT 单次能承载的 MTU-3 字节时,使用
Read Blob Request分片读取,或使用Prepare Write Request+Execute Write Request可靠地分片写入。
5. GATT 客户端与服务器的角色
在 BLE 生态中,角色通常这样分配:
- GATT 服务器:存储数据,通常是外围设备(如心率带、温湿度传感器)。
- GATT 客户端:读取/写入数据,通常是中心设备(如智能手机)。
不过也有例外:一些设备可以同时担任 GATT 客户端和服务器。例如,一个智能手表可以既作为手机通知的 GATT 客户端,也作为心率数据向手机提供的 GATT 服务器。