外设交互:服务发现与特征读写
本文档深入解析 CBClassic 示例工程中外设交互层的完整实现——从连接建立后触发服务发现(Service Discovery)、特征发现(Characteristic Discovery),到通过特征进行数据写入(Write)、读取与通知订阅(Read/Notify)的端到端流程。核心实现位于 PeripheralViewController.swift,UUID 协议常量定义于 CentralViewController.swift 中的 BTConstants。
Purpose and Scope
本页面向需要理解或扩展"连接之后发生了什么"的开发者,覆盖:
PeripheralViewController的初始化与生命周期(入口、委托绑定、UI 构建)- 基于
CBPeripheralDelegate的服务发现与特征发现流程 - 特征写入(
.withResponse类型)与通知订阅(setNotifyValue)的数据通路 BTConstants协议常量(服务 UUIDAE00、写特征AE01、读特征AE02)的设计含义- 错误处理、并发/主线程边界与失败模式
以下主题属于相邻目录页,本文不做展开:
- 连接事件监听与连接/断开管理(
registerForConnectionEvents、centralManager(_:connectionEventDidOccur:for:))——见"中心设备管理"相关页面 - 扫描流程与系统蓝牙状态管理——见"中心设备管理"相关页面
- 构建配置(
.xcconfig)、权限声明(Info.plist)——见"工程配置"相关页面
Overview
CBClassic 是珠海杰理科技提供的基于 Apple CoreBluetooth 框架的 GATT over BR/EDR(经典蓝牙)通讯示例,源自 Apple 官方示例 Using Core Bluetooth Classic。与 BLE 相比,GATT over BR/EDR 底层走经典蓝牙协议,传输速率更高、单次数据量更大,适用于音频设备、穿戴设备等场景。
整个示例的职责划分非常清晰,分为两个视图控制器:
| 控制器 | 职责 | 对应目录页 |
|---|---|---|
CentralViewController | 中心设备侧:蓝牙状态管理、连接事件注册、外设选择与跳转 | 中心设备管理 |
PeripheralViewController | 外设交互侧:服务发现、特征发现、数据收发 | 本文(外设交互) |
外设交互层解决的核心问题是:连接建立后,如何找到协议约定的服务与特征,并安全地进行双向数据通讯。iOS 的 CoreBluetooth 采用异步委托回调模型——你发起一个请求(如 discoverServices),系统在完成时回调对应的 delegate 方法。PeripheralViewController 的全部逻辑就是围绕这一"请求-回调"模型组织起来的。
关键设计意图:
- 按需发现:
discoverServices只传入BTConstants.sampleServiceUUID(AE00),discoverCharacteristics只传入AE01/AE02,避免扫描外设上无关的服务与特征,缩短发现时间并节省电量。 - 读写分离:写特征
AE01与读特征AE02分开定义,写入使用.withResponse保证可靠交付,读取通过开启通知(Notify)实现被动接收——设备主动推送数据,而非客户端轮询。 - 主线程 UI 更新:所有 UI 刷新通过
DispatchQueue.main.async回到主线程,符合 UIKit 线程约束。
Architecture
下图展示了外设交互层的组件结构与数据流向(节点均对应实际代码中的类型/方法):
flowchart TD
subgraph sg_App["应用层 (UIViewController)"]
VC["PeripheralViewController"]
UI["输入框 / 发送按钮 / 接收标签"]
end
subgraph sg_CB["CoreBluetooth 框架层"]
PERIPH["CBPeripheral (selectedPeripheral)"]
DELEGATE["CBPeripheralDelegate 回调"]
end
subgraph sg_CONST["协议常量层"]
CONST["BTConstants"]
end
subgraph sg_DEVICE["远端 BR/EDR 设备"]
SVC["服务 AE00"]
WRITE_CHAR["写特征 AE01"]
READ_CHAR["读特征 AE02 (Notify)"]
end
VC -->|"viewDidLoad: 绑定 delegate"| PERIPH
VC -->|"discoverServices([AE00])"| PERIPH
VC -->|"sendData: writeValue(.withResponse)"| PERIPH
VC -->|"读取常量定义"| CONST
PERIPH -->|"异步回调"| DELEGATE
DELEGATE -->|"didDiscoverServices"| VC
DELEGATE -->|"didDiscoverCharacteristicsFor"| VC
DELEGATE -->|"didUpdateValueFor"| VC
DELEGATE -->|"更新文本"| UI
PERIPH <-->|"GATT over BR/EDR"| SVC
SVC --- WRITE_CHAR
SVC --- READ_CHAR
架构说明:
PeripheralViewController是外设交互的唯一门面:它既是 UI 控制器,也是CBPeripheralDelegate的实现者(通过 extension 拆分职责)。CBPeripheral是 CoreBluetooth 提供的远端外设抽象,所有发现与读写请求都发往它,结果通过 delegate 回调返回——这是典型的"命令-回调"异步模型。BTConstants是协议契约层,集中定义服务/特征 UUID。它被CentralViewController(连接事件匹配)与PeripheralViewController(发现与读写)共同引用,是"同一个协议两端一致"的保证。- 远端设备暴露的服务
AE00下挂两个特征:AE01接收写入(中心→外设),AE02推送数据(外设→中心,Notify)。
注意:本示例中 iOS 应用扮演 Central(中心) 角色,
PeripheralViewController的命名指的是"与远端外设交互的页面",而非外设角色本身。
核心实现解析
1. 入口与初始化:viewDidLoad
外设交互页面的入口是 viewDidLoad。此时 selectedPeripheral 已由上层(CentralViewController 在收到连接事件后跳转时)注入,页面要做三件事:构建 UI、绑定 delegate、发起服务发现:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupUI()
selectedPeripheral.delegate = self
selectedPeripheral.discoverServices([BTConstants.sampleServiceUUID])
}
Source: PeripheralViewController.swift
设计要点:
- delegate 绑定必须在发起发现之前。
CBPeripheral的所有结果都通过 delegate 回调返回,若顺序颠倒(先discoverServices再设 delegate),回调会丢失,服务发现将永远没有结果。 - 发现范围收窄到单个服务 UUID。传入
[BTConstants.sampleServiceUUID]而非nil,指示系统只查找AE00服务。这既减少了发现时间,也避免处理无关服务。 cbManager属性在此页面虽被声明(var cbManager: CBCentralManager!),但实际外设交互不依赖它——连接事件已经由上层完成,这里只需操作CBPeripheral。
2. UI 构建:setupUI
setupUI() 使用纯代码 + Auto Layout 构建三个控件:输入框(UITextField)、发送按钮(UIButton)、接收数据标签(UILabel)。布局约束使用 NSLayoutConstraint.activate 批量激活:输入框水平居中、宽度为屏幕 80%、顶部距安全区 20pt;发送按钮位于输入框下方 20pt、宽 100 高 44;接收标签位于按钮下方 30pt、宽度 90%。
sendButton = UIButton(type: .system)
sendButton.setTitle("发送", for: .normal)
sendButton.setTitleColor(.white, for: .normal)
sendButton.backgroundColor = .blue
sendButton.layer.cornerRadius = 8
sendButton.addTarget(self, action: #selector(sendData), for: .touchUpInside)
Source: PeripheralViewController.swift
sendButton 通过 addTarget(_:action:for:) 把点击事件绑定到 #selector(sendData)——这是 UIKit 的 Target-Action 模式,是数据发送链路的起点。
3. 协议常量:BTConstants
协议常量定义在 CentralViewController.swift 顶部的结构体中,两个控制器共享同一份契约:
struct BTConstants {
static let sampleServiceUUID = CBUUID(string: "AE00")
static let writeCharacteristicUUID = CBUUID(string: "AE01") // 用于写入数据
static let readCharacteristicUUID = CBUUID(string: "AE02") // 读取和通知数据
}
Source: CentralViewController.swift
UUID 契约的设计含义:
| UUID | 角色 | 方向 | 用途 |
|---|---|---|---|
AE00 | 服务 | — | 示例服务标识,同时用作连接事件匹配(CBConnectionEventMatchingOption.serviceUUIDs) |
AE01 | 写特征 | 中心 → 外设 | 客户端下发数据 |
AE02 | 读特征 | 外设 → 中心 | 读取数据 + Notify 通知 |
AE00 被 registerForConnectionEvents(options:) 用作连接事件过滤条件(见 CentralViewController.swift 的 matchingOptions)。这意味着:只有广播/暴露 AE00 服务的设备才会触发连接事件,从而进入外设交互流程——协议常量在连接层与交互层之间形成了闭环。
4. 服务发现:didDiscoverServices
didDiscoverServices 是发现流程的第一阶段回调。若出错则记录日志并直接返回;成功后遍历 peripheral.services,对每个服务发起特征发现:
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if let error = error {
os_log("Error discovering services: %@", error.localizedDescription)
return
}
for service in peripheral.services ?? [] {
peripheral.discoverCharacteristics([BTConstants.writeCharacteristicUUID, BTConstants.readCharacteristicUUID], for: service)
}
}
Source: PeripheralViewController.swift
实现细节:
os_log统一日志:所有失败路径都先os_log输出,这是调试 GATT 交互问题的主要手段(可在 Xcode 控制台按 subsystem 过滤)。- 遍历而非取第一个服务:虽然只请求了
AE00,代码仍遍历peripheral.services ?? [],容忍系统返回多个服务的情况。 - 特征发现也按需过滤:
discoverCharacteristics只查询AE01/AE02,避免发现服务下的其他无关特征。
5. 特征发现与通知订阅:didDiscoverCharacteristicsFor
第二阶段回调。核心逻辑是:按 UUID 匹配特征,把 AE01 保存为写特征、AE02 保存为读特征并立即开启通知:
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let error = error {
os_log("Error discovering characteristics: %@", error.localizedDescription)
return
}
for characteristic in service.characteristics ?? [] {
if characteristic.uuid == BTConstants.writeCharacteristicUUID {
writeCharacteristic = characteristic
os_log("Found write characteristic AE01")
}
if characteristic.uuid == BTConstants.readCharacteristicUUID {
readCharacteristic = characteristic
os_log("Found read characteristic AE02, subscribing...")
peripheral.setNotifyValue(true, for: characteristic)
}
}
}
Source: PeripheralViewController.swift
为什么在发现阶段就订阅通知? 因为外设侧数据(如设备状态、测量结果)是主动推送的。提前 setNotifyValue(true, for:) 后,远端设备一旦更新 AE02,系统就会回调 didUpdateValueFor,应用无需轮询。这是 GATT 中典型的"观察者"模式——先订阅、后收事件,与 iOS 通知中心的行为一致。
注意一个边界:writeCharacteristic 与 readCharacteristic 都是 CBCharacteristic? 可选值。如果远端设备不符合协议(缺 AE01 或 AE02),对应变量保持 nil,sendData() 中的 guard 会拦截写入请求并仅记录日志——这是一个静默降级设计,不会崩溃,但也不会提醒用户设备不兼容。
6. 数据发送:sendData
点击"发送"按钮触发。先 guard 检查写特征是否存在、输入是否为空,再将文本以 UTF-8 编码后通过 .withResponse 类型写入:
@objc private func sendData() {
guard let characteristic = writeCharacteristic else {
os_log("Write characteristic not found")
return
}
guard let text = inputTextField.text, !text.isEmpty else { return }
let dataToSend = text.data(using: .utf8)!
selectedPeripheral.writeValue(dataToSend, for: characteristic, type: .withResponse)
os_log("Sent data: %@", text)
}
Source: PeripheralViewController.swift
为什么选择 .withResponse? 该写入类型要求远端设备返回确认(ATT Write Response),系统会保证写入送达。对命令型交互(如控制指令)来说,可靠性优先于吞吐量。代价是单次写入有更长的往返时间;若需高吞吐流式传输,可改用 .withoutResponse 配合 MTU 分片(本示例未涉及,属于扩展方向)。
健壮性细节:guard let text = inputTextField.text, !text.isEmpty 用逗号连接两个条件,同时完成"非空"与"去空串"校验;data(using: .utf8)! 强制解包在 UTF-8 编码场景下是安全的(Swift 字符串总能编码为 UTF-8)。
7. 数据接收:didUpdateValueFor
写入的对面是接收。无论是一次性读取(readValue)还是通知推送,数据都汇聚到 didUpdateValueFor。本示例只处理 AE02 的通知数据:
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if let error = error {
os_log("Error reading characteristic: %@", error.localizedDescription)
return
}
if characteristic.uuid == BTConstants.readCharacteristicUUID, let value = characteristic.value {
let receivedText = String(data: value, encoding: .utf8) ?? "Invalid Data"
os_log("Received data: %@", receivedText)
DispatchQueue.main.async {
self.receivedDataLabel.text = "接收数据:\(receivedText)"
}
}
}
Source: PeripheralViewController.swift
三个值得注意的设计点:
- 回调线程与 UI 线程分离:CoreBluetooth 的回调可能发生在任意后台队列,直接更新
UILabel会触发 UIKit 线程检查崩溃。代码用DispatchQueue.main.async将 UI 刷新切回主线程——这是 iOS 蓝牙开发的必守纪律。 - UTF-8 解码的容错:
String(data:encoding:)返回可选值,解码失败时用?? "Invalid Data"兜底,避免nil传播到 UI。 - 按 UUID 分流:回调会覆盖所有已订阅/已读的特征,按
characteristic.uuid == BTConstants.readCharacteristicUUID过滤可避免把其他特征的数据误显示。
核心流程
从连接事件触发到双向数据通讯的完整时序如下(参与者均为实际代码中的类型/方法):
sequenceDiagram
participant App as AppDelegate / CentralVC
participant CB as CoreBluetooth 系统
participant DEV as 远端 BR/EDR 设备
participant PVC as PeripheralViewController
participant UI as 界面控件
App->>CB: registerForConnectionEvents(AE00)
CB-->>App: connectionEventDidOccur (连接事件)
App->>PVC: 注入 selectedPeripheral 并 present
PVC->>PVC: viewDidLoad: delegate 绑定
PVC->>CB: discoverServices([AE00])
CB->>DEV: 发起服务发现 (ATT)
DEV-->>CB: 返回服务列表
CB-->>PVC: didDiscoverServices
PVC->>CB: discoverCharacteristics([AE01, AE02])
CB->>DEV: 发起特征发现 (ATT)
DEV-->>CB: 返回特征列表
CB-->>PVC: didDiscoverCharacteristicsFor
PVC->>CB: setNotifyValue(true, AE02)
CB->>DEV: 开启 CCCD 通知 (ATT)
Note over UI,PVC: 用户输入文本并点击"发送"
UI->>PVC: sendData (Target-Action)
PVC->>CB: writeValue(data, AE01, .withResponse)
CB->>DEV: ATT Write Request
DEV-->>CB: ATT Write Response
CB-->>PVC: didWriteValueFor (隐式完成)
Note over DEV,PVC: 设备主动推送数据
DEV-->>CB: 通知 (AE02 值更新)
CB-->>PVC: didUpdateValueFor
PVC->>UI: DispatchQueue.main.async 更新接收标签
流程要点回顾:
- 连接事件是前置条件:
CentralViewController通过registerForConnectionEvents监听AE00服务的连接事件,事件到达后才创建并跳转到外设交互页面(selectedPeripheral随页面注入)。 - 两级发现串行推进:服务发现 → 特征发现 → 通知订阅,每一级都依赖上一级的回调结果,形成严格的依赖链。任一级出错(回调带
error)都会中断后续流程。 - 写入与接收是两条独立通路:上行走
writeValue(.withResponse)(请求-确认),下行走 Notify(订阅-推送),二者互不阻塞,符合全双工通讯模型。 - UI 更新始终在主线程:接收回调先解码、再
DispatchQueue.main.async切主线程更新标签。
使用示例
示例 1:发现服务并绑定委托(初始化阶段)
selectedPeripheral.delegate = self
selectedPeripheral.discoverServices([BTConstants.sampleServiceUUID])
Source: PeripheralViewController.swift
这是外设交互的起点:先设置 CBPeripheralDelegate,再发起对 AE00 服务的发现。顺序不可颠倒,否则发现结果无法回调。
示例 2:按 UUID 匹配特征并开启通知
for characteristic in service.characteristics ?? [] {
if characteristic.uuid == BTConstants.writeCharacteristicUUID {
writeCharacteristic = characteristic
os_log("Found write characteristic AE01")
}
if characteristic.uuid == BTConstants.readCharacteristicUUID {
readCharacteristic = characteristic
os_log("Found read characteristic AE02, subscribing...")
peripheral.setNotifyValue(true, for: characteristic)
}
}
Source: PeripheralViewController.swift
特征发现回调的标准写法:遍历特征列表、按 UUID 分流、写特征仅保存引用、读特征额外开启 Notify。
示例 3:带响应的数据写入
let dataToSend = text.data(using: .utf8)!
selectedPeripheral.writeValue(dataToSend, for: characteristic, type: .withResponse)
os_log("Sent data: %@", text)
Source: PeripheralViewController.swift
type: .withResponse 保证写入被远端确认;数据以 UTF-8 编码。若想确认写入成功,可再实现 didWriteValueFor 回调(本示例未实现,仅依赖 .withResponse 的系统语义)。
示例 4:接收数据并回到主线程更新 UI
if characteristic.uuid == BTConstants.readCharacteristicUUID, let value = characteristic.value {
let receivedText = String(data: value, encoding: .utf8) ?? "Invalid Data"
os_log("Received data: %@", receivedText)
DispatchQueue.main.async {
self.receivedDataLabel.text = "接收数据:\(receivedText)"
}
}
Source: PeripheralViewController.swift
接收侧的完整模式:按 UUID 过滤 → 取出 Data → UTF-8 解码(失败兜底)→ 主线程刷新 UI。这一模式可复用到任何"设备上报"场景。
示例 5:协议常量契约(连接层与交互层共享)
struct BTConstants {
static let sampleServiceUUID = CBUUID(string: "AE00")
static let writeCharacteristicUUID = CBUUID(string: "AE01") // 用于写入数据
static let readCharacteristicUUID = CBUUID(string: "AE02") // 读取和通知数据
}
Source: CentralViewController.swift
同一份常量同时用于连接事件匹配与特征发现/读写,保证整个链路使用一致的协议定义。新增协议特征时应在此扩展,而不是在控制器里硬编码 UUID。
配置说明
协议常量配置
外设交互行为由 BTConstants 中的三个 UUID 决定,它们是唯一需要与设备固件对齐的"配置":
| 常量 | 类型 | 值 | 作用 |
|---|---|---|---|
sampleServiceUUID | CBUUID | "AE00" | 服务标识;连接事件匹配 + 服务发现目标 |
writeCharacteristicUUID | CBUUID | "AE01" | 写特征;writeValue 目标 |
readCharacteristicUUID | CBUUID | "AE02" | 读特征;setNotifyValue 订阅目标 |
Source: CentralViewController.swift
修改建议:若对接不同协议版本的设备,只需更新这三个 UUID;但必须同步修改 CentralViewController 中 registerForConnectionEvents 的 matchingOptions,否则连接事件不会触发,外设交互页面永远不会被打开。
系统权限配置(Info.plist)
外设交互依赖蓝牙权限,工程已在 Info.plist 中声明(详见 Info.plist):
| Key | 值 | 说明 |
|---|---|---|
NSBluetoothAlwaysUsageDescription | 蓝牙使用用途描述 | 首次使用时系统弹窗文案 |
UIBackgroundModes | bluetooth-central | 可选;支持后台蓝牙事件 |
运行环境要求
| 项目 | 要求 |
|---|---|
| iOS | 13.0+(registerForConnectionEvents 的最低可用版本) |
| Xcode | 14.0+ |
| 语言 | Swift(工程为纯 Swift) |
API 参考
PeripheralViewController 类
UIViewController 子类,外设交互页面的唯一控制器。持有中心管理器引用(本页面不使用)与已选外设引用。
属性:
| 属性 | 类型 | 说明 |
|---|---|---|
cbManager | CBCentralManager! | 中心管理器(由上层注入,交互阶段不使用) |
selectedPeripheral | CBPeripheral! | 已连接的外设(由上层注入,交互阶段的操作对象) |
writeCharacteristic | CBCharacteristic? | 写特征 AE01,特征发现后赋值 |
readCharacteristic | CBCharacteristic? | 读特征 AE02,特征发现后赋值 |
sendButton / inputTextField / receivedDataLabel | UI 控件 | 页面 UI 元素 |
方法:
viewDidLoad()— 初始化 UI、绑定 delegate、发起服务发现setupUI()— 纯代码创建控件并激活 Auto Layout 约束sendData()(@objc private)— 读取输入框文本、UTF-8 编码、以.withResponse写入写特征
CBPeripheralDelegate 扩展(核心 API)
| 方法 | 触发时机 | 参数要点 | 行为 |
|---|---|---|---|
peripheral(_:didDiscoverServices:error:) | discoverServices 完成后 | peripheral.services、error | 出错记日志并返回;成功则遍历服务发起特征发现 |
peripheral(_:didDiscoverCharacteristicsFor:error:) | discoverCharacteristics 完成后 | service.characteristics、error | 按 UUID 匹配 AE01/AE02;对 AE02 调用 setNotifyValue(true, for:) |
peripheral(_:didUpdateValueFor:error:) | 读取完成或收到通知时 | characteristic.value、error | 过滤 AE02、UTF-8 解码、主线程更新标签 |
Throws / 错误处理:三个回调均接收 Error? 参数。出错时统一 os_log 输出 error.localizedDescription 并 return,不抛出异常、不崩溃;典型错误包括设备断开(CBErrorDomain)与特征不可用。
底层调用 API(CBPeripheral)
外设交互实际使用的 CoreBluetooth 调用(由系统提供,签名依据代码使用方式列出):
| 方法 | 参数 | 说明 |
|---|---|---|
discoverServices(_:) | [CBUUID]? | 按 UUID 发现服务;nil 表示发现全部 |
discoverCharacteristics(_:for:) | [CBUUID]?, CBService | 发现指定服务下的特征 |
writeValue(_:for:type:) | Data, CBCharacteristic, CBCharacteristicWriteType | 写入特征值;.withResponse 需要设备确认 |
setNotifyValue(_:for:) | Bool, CBCharacteristic | 订阅/取消订阅特征通知(写 CCCD) |
失败模式、边界情况与并发
失败模式
| 失败场景 | 表现 | 处理方式(源码依据) |
|---|---|---|
| 服务发现失败 | didDiscoverServices 收到 error | os_log 记录后 return,流程终止(L86-L90) |
| 特征发现失败 | didDiscoverCharacteristicsFor 收到 error | os_log 记录后 return,不订阅、不写入 |
设备不暴露 AE01 | writeCharacteristic 保持 nil | sendData 的 guard 拦截,仅记录 "Write characteristic not found"(L73-L76) |
设备不暴露 AE02 | readCharacteristic 保持 nil | 无通知数据,接收标签保持初始文案;无崩溃 |
| 接收数据非 UTF-8 | String(data:encoding:) 返回 nil | ?? "Invalid Data" 兜底显示 |
| 输入为空 | inputTextField.text 为空/空白 | guard 静默返回,不发写入 |
| 写入失败(设备断开等) | CoreBluetooth 内部错误 | 本示例未实现 didWriteValueFor,错误由系统日志呈现——这是已知的增强点 |
边界情况
- 多服务返回:
didDiscoverServices遍历peripheral.services ?? []而非取第一个,即使系统返回多个服务也能逐一尝试发现特征(L92)。 - 多特征匹配:
didDiscoverCharacteristicsFor用两个独立if(而非else if)分别匹配AE01与AE02,允许同一服务下两个特征同时存在。 - 可选值安全:
cbManager、selectedPeripheral为隐式解包可选,依赖上层注入;若上层未注入,viewDidLoad访问selectedPeripheral会崩溃——这是"入口契约",文档已在 Purpose 中说明依赖关系。
并发与线程
- 回调线程:CoreBluetooth delegate 回调不一定在主线程。所有 UI 修改都通过
DispatchQueue.main.async切回主线程(L125-L127),避免 UIKit 线程检查崩溃。 - 写入并发:
sendData由 UI 事件串行触发,本示例无并发写入问题。若扩展为多线程频繁写入,需自行串行化或使用.withoutResponse+ MTU 分片。 - 请求-回调顺序:服务发现 → 特征发现 → 通知订阅严格串行,每一级回调完成后才发起下一级,天然避免了竞态。
性能与运维注意事项
- 按需发现是主要性能手段:
discoverServices/discoverCharacteristics均传入明确 UUID 列表,避免全量发现带来的延迟与电量开销。 .withResponse的吞吐边界:每次写入等待 ATT Write Response,单次往返延迟决定了写入吞吐上限。高吞吐场景应改用.withoutResponse并配合maximumWriteValueLength(for:)获取 MTU 分片大小——本示例未实现,属扩展方向。- 通知机制避免轮询:
setNotifyValue(true)让设备主动推送,省去周期性readValue的功耗与流量。 - 调试入口:所有关键路径(发现成功/失败、发送、接收)均有
os_log输出,可在 Xcode 控制台启用 OSLog 过滤快速定位 GATT 交互问题。
扩展点
- 写入确认:实现
peripheral(_:didWriteValueFor:error:)以感知.withResponse的写入结果,可据此做重试或 UI 反馈(当前代码未实现,写入错误只能依赖系统日志)。 - 主动读取:在特征发现后调用
peripheral.readValue(for: readCharacteristic)可获取设备当前值(一次性读取),与 Notify 互补。 - 取消订阅与断开清理:在页面
dealloc/viewWillDisappear中调用setNotifyValue(false, for:)与cancelPeripheralConnection,避免后台残留订阅。 - 新增特征/服务:在
BTConstants扩展常量,并在didDiscoverCharacteristicsFor中增加匹配分支即可支持更多协议通道(如带分片的大数据通道)。 - 输入内容类型扩展:当前固定 UTF-8 文本;如需传输二进制协议数据,将
text.data(using: .utf8)!替换为协议编码函数即可。
测试覆盖说明
本示例工程未包含单元测试/UI 测试目标(工程结构仅含应用主模块,见 README.md 工程结构)。PeripheralViewController 的验证依赖真机 + 支持 AE00/AE01/AE02 协议的 BR/EDR 设备联调。建议的验证路径:连接设备 → 确认日志出现 "Found write characteristic AE01" 与 "Found read characteristic AE02, subscribing..." → 发送文本 → 设备侧收到 → 设备推送 → 界面标签更新。
Related Links
- PeripheralViewController.swift(本页核心实现)
- CentralViewController.swift(BTConstants 定义与连接事件注册)
- README.md(工程总览与快速开始)
- 相邻目录页:中心设备管理(连接事件监听、连接/断开管理、扫描)——本文档的外设交互入口依赖其连接事件
- Apple 官方文档:Using Core Bluetooth Classic、CBPeripheralDelegate