杰理 SDK 文档中心
首页
首页
  • 快速开始

    • 环境要求与工程导入
    • 蓝牙权限与后台模式配置
  • GATT over BR/EDR 示例

    • 工程结构与核心组件
    • 中心设备:连接事件监听与设备列表
    • 外设交互:服务发现与特征读写

工程结构与核心组件

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

架构说明

  1. 入口层:AppDelegate 仅承载 UIWindow 与 @UIApplicationMain 注解,无额外初始化逻辑,保证示例的纯净性(见 AppDelegate.swift)。
  2. UI 层:两个视图控制器职责分离——CentralViewController 负责系统蓝牙状态与外围设备列表;PeripheralViewController 负责与选中设备的数据交互。二者通过 present 衔接,并共享同一个 CBCentralManager 实例与 CBPeripheral 句柄。
  3. 框架层:直接使用系统 CoreBluetooth。CBCentralManager 作为系统服务代理,负责状态上报、连接事件与连接管理;CBPeripheral 是远端设备的本地句柄,所有 GATT 操作(发现服务/特征、读写、订阅通知)都通过它发起。
  4. 设备层:杰理蓝牙设备扮演 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

流程要点

  1. 连接入口:didSelectRowAt 直接调用 cbManager.connect(peripheral, options: nil),未传连接选项(如 CBConnectPeripheralOptionNotifyOnDisconnectionKey)——这是刻意简化,客户可自行扩展断线重连逻辑。
  2. 控制器交接:didConnect 中构造 PeripheralViewController 并以属性注入方式传递 cbManager 与 selectedPeripheral,随后 present。注意此时 CBPeripheral.delegate 尚未设置,直到新控制器的 viewDidLoad 才赋值——中间无任何 GATT 操作,规避了"无 delegate 时的调用丢失"问题。
  3. 发现与订阅:服务发现成功 → 特征发现成功 → 对 AE02 调用 setNotifyValue(true) 完成订阅。此后设备端任何 AE02 通知都会触发 didUpdateValueFor。
  4. 写入应答:.withResponse 写入的完成也会触发 didWriteValueFor 回调(示例未实现该委托方法,属可接受的简化——写入错误可通过该方法补充监控)。
  5. UI 更新:所有数据回显统一走 DispatchQueue.main.async,保证线程安全。

配置选项

SampleCode.xcconfig

示例工程通过 Configuration/SampleCode.xcconfig 提供一项构建期配置:

配置键类型默认值说明
SAMPLE_CODE_DISAMBIGUATORstring(构建期宏)${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

成员类型值说明
sampleServiceUUIDCBUUIDAE00杰理设备 GATT 服务 UUID
writeCharacteristicUUIDCBUUIDAE01主机→设备写入特征
readCharacteristicUUIDCBUUIDAE02设备→主机读取/通知特征

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 后系统可能清除注册,示例在状态恢复时重新注册,覆盖了该场景。

扩展点

扩展方向现有基础建议做法
自定义协议 UUIDBTConstants 集中定义修改三个 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 回显。

Related Links

  • README.md(仓库总览与快速开始)
  • CentralViewController.swift(中央设备角色实现)
  • PeripheralViewController.swift(外围交互角色实现)
  • AppDelegate.swift(应用入口)
  • SampleCode.xcconfig(构建配置)
  • UsingCoreBluetoothClassic/README.md(示例工程说明)
  • 文档中心:GATT Over EDR 描述
Next
中心设备:连接事件监听与设备列表