工程结构与核心组件
ios-bt-demo 是珠海杰理科技(Jieli-Tech)面向蓝牙产品的 iOS 端测试示例代码集合仓库。本文档聚焦其中的 UsingCoreBluetoothClassic 示例工程,系统性地讲解该工程的目录组织、入口、核心视图控制器、蓝牙常量契约、构建配置,以及以 GATT over BR/EDR 为通讯模型的数据流与控制流。
Purpose and Scope
本文档覆盖以下内容:
- 仓库整体结构与
UsingCoreBluetoothClassic示例工程内部的目录组织; - 应用入口
AppDelegate与构建配置SampleCode.xcconfig的作用; - 核心组件
CentralViewController(中央设备角色:扫描、连接、连接事件管理)与PeripheralViewController(外围交互角色:服务发现、特征订阅、数据收发)的实现细节; - 蓝牙 GATT 服务/特征 UUID 契约(
BTConstants)与端到端数据流、控制流; - 失败模式、并发与边界情况、扩展点。
不属于本文档范围、由其他页面承载的主题包括:GATT over BR/EDR 协议原理与特性说明(参见文档中心 GATT Over EDR 描述)、特定杰理芯片的固件端行为、以及仓库中未来新增的其他示例工程。如需部署与运行步骤,请参见 README.md。
Overview
仓库定位
根据 README.md,iOS-BT-Demo 是"杰理蓝牙产品 iOS 端测试示例代码集合,方便客户快速测试蓝牙功能",当前包含一个用例:
GATT Over BR/EDR 连接示例,演示了如何在 iOS 端通过 GATT 协议与杰理蓝牙产品进行数据通讯。
GATT over BR/EDR 是在经典蓝牙(BR/EDR)链路上承载 GATT(Generic Attribute Profile)的通讯方式,相比传统 SPP 串口模拟具有更高速率与更标准的属性抽象。iOS 自 13.0 起支持该能力,示例要求 iOS 13.0+、Xcode 14.0+,开发语言为 Swift。
核心概念
| 概念 | 说明 | 本工程中的体现 |
|---|---|---|
| 中央设备(Central) | 发起扫描与连接的设备端角色 | CentralViewController + CBCentralManager |
| 外围设备(Peripheral) | 提供 GATT 服务的对端 | 杰理蓝牙设备(含 GATT Server) |
| GATT 服务(Service) | 一组特征的集合,用 UUID 标识 | AE00(BTConstants.sampleServiceUUID) |
| 特征(Characteristic) | 数据读写/通知的最小单元,用 UUID 标识 | 写特征 AE01、读/通知特征 AE02 |
| 连接事件(Connection Event) | iOS 系统级的外围设备连接/断开事件 | registerForConnectionEvents + connectionEventDidOccur |
设计意图
该示例刻意保持最小化:UI 层只有两个视图控制器,业务层直接使用系统 CoreBluetooth 框架,没有引入第三方依赖。其设计意图是作为"最小可运行参考实现",让客户在最短时间内看到"扫描 → 连接 → 发现服务 → 读写数据"的完整闭环,同时以 os_log 输出每个关键节点,便于真机调试。
Architecture
flowchart TD
subgraph sg_App["iOS 应用(UsingCoreBluetoothClassic)"]
AppDelegate["AppDelegate<br/>应用入口(@UIApplicationMain)"]
CentralVC["CentralViewController<br/>中央设备角色:扫描与连接"]
PeripheralVC["PeripheralViewController<br/>外围交互角色:数据收发"]
end
subgraph sg_CoreBluetooth["系统 CoreBluetooth 框架"]
CBM["CBCentralManager<br/>蓝牙中心管理器"]
PB["CBPeripheral<br/>远端设备句柄"]
end
subgraph sg_Device["杰理蓝牙设备(GATT Server)"]
Service["Service AE00"]
WriteChar["Write Characteristic AE01"]
ReadChar["Read / Notify Characteristic AE02"]
end
AppDelegate --> CentralVC
CentralVC -->|"初始化 + delegate"| CBM
CBM -->|"connectionEventDidOccur / didConnect"| PB
CentralVC -->|"present 跳转"| PeripheralVC
PeripheralVC -->|"discoverServices / discoverCharacteristics / writeValue / setNotifyValue"| PB
PB -->|"GATT over BR/EDR 空中链路"| Service
Service --> WriteChar
Service --> ReadChar
架构说明
- 入口层:
AppDelegate仅承载UIWindow与@UIApplicationMain注解,无额外初始化逻辑,保证示例的纯净性(见 AppDelegate.swift)。 - UI 层:两个视图控制器职责分离——
CentralViewController负责系统蓝牙状态与外围设备列表;PeripheralViewController负责与选中设备的数据交互。二者通过present衔接,并共享同一个CBCentralManager实例与CBPeripheral句柄。 - 框架层:直接使用系统
CoreBluetooth。CBCentralManager作为系统服务代理,负责状态上报、连接事件与连接管理;CBPeripheral是远端设备的本地句柄,所有 GATT 操作(发现服务/特征、读写、订阅通知)都通过它发起。 - 设备层:杰理蓝牙设备扮演 GATT Server,暴露
AE00服务下的AE01(写)与AE02(读/通知)两个特征,构成客户端的通讯契约。
为什么选择"连接事件 + 手动连接"双通道? 示例在 centralManagerDidUpdateState 的 .poweredOn 分支中调用 registerForConnectionEvents 注册系统级连接事件(见 CentralViewController.swift),同时保留 didSelectRowAt 中的主动 connect 路径。这样既能在系统层感知"用户在其他界面建立的连接",也能在应用内主动发起连接,覆盖了 iOS 13 连接事件 API 的典型使用场景。
核心组件详解
BTConstants —— GATT 通讯契约
BTConstants 是一个轻量 struct,集中定义客户端与杰理设备之间的 UUID 契约。所有 UUID 都以 16 位短格式字符串表示,CBUUID(string:) 会将其规范化为完整的 128 位 UUID:
struct BTConstants {
static let sampleServiceUUID = CBUUID(string: "AE00")
static let writeCharacteristicUUID = CBUUID(string: "AE01") // 用于写入数据
static let readCharacteristicUUID = CBUUID(string: "AE02") // 读取和通知数据
}
Source: CentralViewController.swift
设计意图:将 UUID 收敛到单一常量类型,避免在多个文件中硬编码字符串导致契约漂移。AE00 服务下的 AE01/AE02 特征命名遵循"写-读"对称约定:AE01 用于主机向设备写入指令/数据(.withResponse 写入),AE02 用于接收设备上报的读取/通知数据。这是 GATT over BR/EDR 通讯中最常见的"一写一读一通知"三件套结构,客户更换自有协议时只需修改此处常量。
AppDelegate —— 应用入口
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
}
Source: AppDelegate.swift
入口极简:没有 App Transport Security 例外、没有第三方 SDK 初始化。整个蓝牙生命周期完全由 CentralViewController 在 viewDidLoad 中创建 CBCentralManager 时启动。设计意图:将示例的注意力集中在 CoreBluetooth 用法本身,避免入口代码干扰读者理解。
CentralViewController —— 中央设备角色
CentralViewController 承担三个职责:展示外围设备列表、管理系统蓝牙状态、发起与跟踪连接。其关键状态包括:
class CentralViewController: UIViewController {
private var tableView: UITableView!
private var cbManager: CBCentralManager!
private var cbState = CBManagerState.unknown
private var cbPeripherals = [CBPeripheral]()
...
}
Source: CentralViewController.swift
初始化与列表构建
在 viewDidLoad 中完成三件事:创建 UITableView 并施加全屏 Auto Layout 约束、注册复用 cell、初始化 CBCentralManager(delegate 为 self、queue 为 nil 表示使用主队列回调):
override func viewDidLoad() {
super.viewDidLoad()
title = "Peripherals"
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "peripheralCell")
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
cbManager = CBCentralManager(delegate: self, queue: nil)
}
Source: CentralViewController.swift
值得注意的细节:tableView(_:cellForRowAt:) 中列表是倒序展示的(let index = cbPeripherals.count - (indexPath.row + 1)),最新发现/连接的外围设备显示在最顶部,便于在设备众多时快速看到新设备(见 CentralViewController.swift)。
系统蓝牙状态机处理
centralManagerDidUpdateState 是 CoreBluetooth 编程的"第一道门槛"——只有 .poweredOn 之后才能进行任何蓝牙操作。示例对每个状态都做了 os_log 日志输出:
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .resetting:
os_log("与系统服务的连接暂时丢失,即将更新")
case .unsupported:
os_log("平台不支持蓝牙低能耗中心/客户端角色")
case .unauthorized:
switch central.authorization {
case .restricted:
os_log("此设备的蓝牙被限制使用")
case .denied:
os_log("该应用没有授权使用蓝牙低能耗角色")
default:
os_log("发生了未知错误,正在清理 cbManager")
}
case .poweredOff:
os_log("蓝牙当前处于关闭状态")
case .poweredOn:
os_log("启动 cbManager")
let matchingOptions = [CBConnectionEventMatchingOption.serviceUUIDs: [BTConstants.sampleServiceUUID]]
cbManager.registerForConnectionEvents(options: matchingOptions)
default:
os_log("清理 cbManager")
}
}
Source: CentralViewController.swift
设计意图:状态机的每个分支都是"面向用户可解释"的日志,而非静默失败——这既是排障指南,也示范了 central.authorization(iOS 13+ 新增)与 registerForConnectionEvents 的正确用法。.unauthorized 分支内嵌 switch 是为了区分系统级 restricted 与应用级 denied 两种不同的授权失败原因。
连接事件与连接管理
func centralManager(_ central: CBCentralManager, connectionEventDidOccur event: CBConnectionEvent, for peripheral: CBPeripheral) {
switch event {
case .peerConnected:
os_log("peerConnected for peripheral: %@", peripheral)
cbPeripherals.append(peripheral)
case .peerDisconnected:
os_log("peerDisconnected for peripheral:%@", peripheral)
default:
if let idx = cbPeripherals.firstIndex(where: { $0 === peripheral }) {
cbPeripherals.remove(at: idx)
}
}
tableView.reloadData()
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
os_log("peripheral: %@ connected", peripheral)
let peripheralVC = PeripheralViewController()
peripheralVC.cbManager = cbManager
peripheralVC.selectedPeripheral = peripheral
present(peripheralVC, animated: true, completion: nil)
}
Source: CentralViewController.swift
设计意图:connectionEventDidOccur 处理的是系统级连接事件(可能由本应用或系统其他入口触发),因此需要根据事件类型增删 cbPeripherals 数组并刷新列表;而 didConnect 是应用主动 connect 的回调,此时直接创建 PeripheralViewController 并注入 cbManager 与 selectedPeripheral 两个依赖后 present。两种路径互补:前者维护列表一致性,后者驱动 UI 跳转。firstIndex(where:) 使用 === 恒等比较确保删除的是同一 CBPeripheral 实例。
PeripheralViewController —— 外围交互角色
PeripheralViewController 是被 CentralViewController present 出来的数据交互页,持有注入的 cbManager 与 selectedPeripheral,并缓存发现到的写/读特征:
class PeripheralViewController: UIViewController {
private var sendButton: UIButton!
private var inputTextField: UITextField!
private var receivedDataLabel: UILabel!
var cbManager: CBCentralManager!
var selectedPeripheral: CBPeripheral!
private var writeCharacteristic: CBCharacteristic?
private var readCharacteristic: CBCharacteristic?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupUI()
selectedPeripheral.delegate = self
selectedPeripheral.discoverServices([BTConstants.sampleServiceUUID])
}
...
}
Source: PeripheralViewController.swift
设计意图:cbManager 与 selectedPeripheral 是强引用 var(非 private),因为由外部控制器注入——这是示例中组件解耦与依赖注入的最小实现。viewDidLoad 中在 setupUI() 之后立即发起 discoverServices,使 GATT 发现流程与 UI 构建并行推进。
GATT 发现链:服务 → 特征 → 订阅
CBPeripheralDelegate 的发现回调构成一条严格的链式流程:必须先发现服务,才能对服务内的特征发起发现;发现特征后才能订阅通知:
extension PeripheralViewController: CBPeripheralDelegate {
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)
}
}
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
边界情况:示例对 error 参数一律先判空再继续——若服务/特征发现失败(设备不支持、链路异常等),直接 return,避免对空值做 ?? 兜底掩盖真实错误。特征缓存采用"按 UUID 匹配赋值"而非"取第一个特征",保证 AE01/AE02 与用途严格对应。
数据发送与接收
@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 类型:iOS 会等待设备侧返回写入确认(Write Response),可靠性更高,适合指令型通讯。接收路径在 didUpdateValueFor 中解码 UTF-8,并通过 DispatchQueue.main.async 切回主线程更新 UI——因为 CoreBluetooth 回调可能发生在非主队列,直接操作 UILabel 有线程安全隐患:
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
核心通讯流程
从用户点击列表行到收到设备数据,完整链路如下:
sequenceDiagram
participant User as 用户
participant CV as CentralViewController
participant CBM as CBCentralManager
participant PB as CBPeripheral
participant PV as PeripheralViewController
User->>CV: 点击列表行(didSelectRowAt)
CV->>CBM: connect(peripheral, options: nil)
CBM-->>CV: didConnect 回调
CV->>PV: present(PeripheralViewController)
Note over CV,PV: 注入 cbManager 与 selectedPeripheral
PV->>PB: discoverServices([AE00])
PB-->>PV: didDiscoverServices
PV->>PB: discoverCharacteristics([AE01, AE02])
PB-->>PV: didDiscoverCharacteristicsFor
PV->>PB: setNotifyValue(true, for: AE02)
User->>PV: 输入文本,点击"发送"
PV->>PB: writeValue(data, AE01, .withResponse)
PB-->>PV: didUpdateValueFor(AE02 通知到达)
PV->>PV: DispatchQueue.main.async 更新 receivedDataLabel
流程要点
- 连接入口:
didSelectRowAt直接调用cbManager.connect(peripheral, options: nil),未传连接选项(如CBConnectPeripheralOptionNotifyOnDisconnectionKey)——这是刻意简化,客户可自行扩展断线重连逻辑。 - 控制器交接:
didConnect中构造PeripheralViewController并以属性注入方式传递cbManager与selectedPeripheral,随后present。注意此时CBPeripheral.delegate尚未设置,直到新控制器的viewDidLoad才赋值——中间无任何 GATT 操作,规避了"无 delegate 时的调用丢失"问题。 - 发现与订阅:服务发现成功 → 特征发现成功 → 对
AE02调用setNotifyValue(true)完成订阅。此后设备端任何AE02通知都会触发didUpdateValueFor。 - 写入应答:
.withResponse写入的完成也会触发didWriteValueFor回调(示例未实现该委托方法,属可接受的简化——写入错误可通过该方法补充监控)。 - UI 更新:所有数据回显统一走
DispatchQueue.main.async,保证线程安全。
配置选项
SampleCode.xcconfig
示例工程通过 Configuration/SampleCode.xcconfig 提供一项构建期配置:
| 配置键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
SAMPLE_CODE_DISAMBIGUATOR | string(构建期宏) | ${DEVELOPMENT_TEAM} | 用于派生唯一的 Bundle Identifier,避免多个示例工程共用同一 bundle id 导致真机安装冲突 |
SAMPLE_CODE_DISAMBIGUATOR=${DEVELOPMENT_TEAM}
Source: SampleCode.xcconfig
设计意图(引自文件头注释):示例代码常被反复下载、且通常未设置开发团队(Development Team),因此 Bundle Identifier 直接派生自 DEVELOPMENT_TEAM 值,保证每个开发者构建出的应用标识唯一。该方式仅适用于示例工程,生产项目不应照搬。
运行时配置
蓝牙能力相关的运行时配置位于工程内 Info.plist(应用级元数据、权限声明与启动故事板配置)。CoreBluetooth 的授权状态由系统根据权限声明与用户选择决定,示例在 centralManagerDidUpdateState 的 .unauthorized 分支中对其进行了分类日志输出(见 CentralViewController.swift)。
API 参考
BTConstants
| 成员 | 类型 | 值 | 说明 |
|---|---|---|---|
sampleServiceUUID | CBUUID | AE00 | 杰理设备 GATT 服务 UUID |
writeCharacteristicUUID | CBUUID | AE01 | 主机→设备写入特征 |
readCharacteristicUUID | CBUUID | AE02 | 设备→主机读取/通知特征 |
CentralViewController(CBCentralManagerDelegate 关键回调)
centralManagerDidUpdateState(_ central: CBCentralManager)
- 系统蓝牙状态变化回调。
.poweredOn时注册连接事件(按AE00服务 UUID 过滤);.unauthorized时进一步区分restricted与denied;其余状态输出日志。无返回值。
centralManager(_ central: CBCentralManager, connectionEventDidOccur event: CBConnectionEvent, for peripheral: CBPeripheral)
- 系统级连接事件回调。
peerConnected追加外围设备,peerDisconnected从列表移除,随后tableView.reloadData()。 - 参数:
event(CBConnectionEvent,.peerConnected/.peerDisconnected)、peripheral(关联的外围设备)。
centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
- 主动
connect成功回调。构造PeripheralViewController,注入cbManager与selectedPeripheral并present。 - 配套回调:
didFailToConnect(连接失败)、didDisconnectPeripheral(连接断开)——示例仅记录日志,未实现重连。
tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
- 用户点击列表行,对选中
CBPeripheral调用cbManager.connect(peripheral, options: nil)。
PeripheralViewController(CBPeripheralDelegate 关键回调)
peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?)
- 服务发现结果回调。遍历
peripheral.services,对每个服务调用discoverCharacteristics([AE01, AE02], for:)。 - 参数:
error——非空时记录日志并 return。
peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
- 特征发现结果回调。按 UUID 匹配缓存
writeCharacteristic/readCharacteristic;对AE02调用setNotifyValue(true, for:)建立通知订阅。 - 参数:
service——特征所属服务;error——发现失败原因。
peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)
- 特征值更新回调(读响应或通知到达)。
AE02的数据按 UTF-8 解码后经DispatchQueue.main.async更新receivedDataLabel。 - 参数:
characteristic——值变化的特征;error——读取/通知错误。
sendData()(@objc private)
- 发送按钮动作。从
inputTextField取文本,data(using: .utf8)!转 Data,调用selectedPeripheral.writeValue(_:for:type: .withResponse)。 - 前置守卫:
writeCharacteristic未发现或文本为空时直接返回,避免空写入。
失败模式、边界情况与并发
蓝牙不可用状态
centralManagerDidUpdateState 覆盖了除 .poweredOn 外的所有系统状态。这些状态是异步到达的——用户可能在运行中关闭蓝牙、拒绝权限或系统正在重置蓝牙栈,示例的应对策略是逐状态输出日志并在 .poweredOn 前不发起任何操作。注意 .unauthorized 下的 central.authorization 在部分系统版本/设备上可能返回 unknown(示例中的 default 分支即为此保留)。
特征未发现时的写入
sendData() 的第一重守卫是 guard let characteristic = writeCharacteristic:若用户抢在 didDiscoverCharacteristicsFor 完成之前点击发送,写入会被安全跳过并输出 Write characteristic not found 日志,而非崩溃。这是异步 GATT 流程中典型的"竞态窗口",示例以守卫方式优雅降级。
连接失败与断开
didFailToConnect:仅记录日志,不弹窗提示,也不自动重试——示例刻意将重连策略留给客户实现。didDisconnectPeripheral:仅记录日志。PeripheralViewController已持有selectedPeripheral强引用,断线后若用户继续点击发送,writeValue会因无连接而触发系统错误回调(示例未实现didWriteValueFor来观测该错误,属已知简化)。
线程与并发
CBCentralManager(queue: nil)表示委托回调派发到主队列,因此centralManagerDidUpdateState、connectionEventDidOccur、didConnect等均在主线程执行,可直接操作UITableView。CBPeripheralDelegate的发现/更新回调同样由系统派发;didUpdateValueFor中仍显式使用DispatchQueue.main.async更新UILabel——这是防御性写法,保证即便未来将回调队列改为后台队列也不会出现 UI 线程冲突。- 列表增删(
connectionEventDidOccur)与用户点击(didSelectRowAt)可能交错发生,tableView.reloadData()在主线程串行执行,未使用并发容器,符合"主队列回调 + 主队列 UI"的单线程模型。
数据边界
- 文本编码固定为 UTF-8;
String(data:encoding:)失败时回退为"Invalid Data"字符串而非崩溃。 data(using: .utf8)!使用强制解包——因String转 UTF-8 实际不会失败,属可接受写法。- 示例未处理超过 MTU 的长数据分片;GATT 单次写入长度受 MTU 约束,生产环境中
writeValue大数据需自行分包,这是本示例留给客户的重要扩展点之一。
性能与运维考量
- 日志可观测性:全工程统一使用
os_log(而非print),日志可被 Xcode Console /log stream按子系统过滤,且不阻塞主线程。调试 GATT over BR/EDR 时建议真机连接 Xcode,观察 "Found write characteristic AE01"、"subscribing..."、"Received data" 等关键日志定位断点。 - 写入可靠性:
.withResponse写入由系统保证应答与错误上报(错误需实现didWriteValueFor观察),适用于指令类小数据;高频大数据场景应评估.withoutResponse+ 应用层确认,以获得更高吞吐。 - 连接事件注册:
registerForConnectionEvents仅在.poweredOn时调用一次,系统会持续上报匹配AE00服务的外围连接事件,无需重复注册;.poweredOff/.resetting后系统可能清除注册,示例在状态恢复时重新注册,覆盖了该场景。
扩展点
| 扩展方向 | 现有基础 | 建议做法 |
|---|---|---|
| 自定义协议 UUID | BTConstants 集中定义 | 修改三个 UUID 常量即可适配自有 GATT 服务 |
| 主动扫描 | 当前仅靠连接事件被动收集设备 | 在 .poweredOn 中调用 cbManager.scanForPeripherals(withServices: [AE00]) 并实现 didDiscover 回调 |
| 断线重连 | didDisconnectPeripheral 仅有日志 | 记录上次 CBPeripheral 标识,延迟后重调 connect |
| 大包收发 | .withResponse 单次写入 | 按 MTU(maximumWriteValueLength(for:))分片写入,接收端按协议头重组 |
| 写入结果监控 | 未实现 didWriteValueFor | 实现该委托方法观测 .withResponse 错误并提示用户 |
| 多设备管理 | 单 selectedPeripheral | 以字典/数组管理多个 CBPeripheral 与其特征缓存 |
测试情况
本示例未包含 XCTest 单元测试或 UI 测试文件;其"测试"以真机手动验证为主——README 的快速开始章节(README.md)即描述了"打开 Xcode → 选择示例目录 → Run → 真机测试蓝牙功能"的手动测试路径。由于 CoreBluetooth 依赖真实系统蓝牙栈与真实杰理设备,单元测试通常需要抽象 CBCentralManager/CBPeripheral 协议层(示例未做此抽象,属刻意保持的最小化设计)。验证要点建议覆盖:蓝牙关闭/开启的状态迁移、首次权限弹窗的授权/拒绝、连接后服务与特征发现日志、写入后设备返回通知的 UI 回显。