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

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

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

外设交互:服务发现与特征读写

本文档深入解析 CBClassic 示例工程中外设交互层的完整实现——从连接建立后触发服务发现(Service Discovery)、特征发现(Characteristic Discovery),到通过特征进行数据写入(Write)、读取与通知订阅(Read/Notify)的端到端流程。核心实现位于 PeripheralViewController.swift,UUID 协议常量定义于 CentralViewController.swift 中的 BTConstants。

Purpose and Scope

本页面向需要理解或扩展"连接之后发生了什么"的开发者,覆盖:

  • PeripheralViewController 的初始化与生命周期(入口、委托绑定、UI 构建)
  • 基于 CBPeripheralDelegate 的服务发现与特征发现流程
  • 特征写入(.withResponse 类型)与通知订阅(setNotifyValue)的数据通路
  • BTConstants 协议常量(服务 UUID AE00、写特征 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 的全部逻辑就是围绕这一"请求-回调"模型组织起来的。

关键设计意图:

  1. 按需发现:discoverServices 只传入 BTConstants.sampleServiceUUID(AE00),discoverCharacteristics 只传入 AE01/AE02,避免扫描外设上无关的服务与特征,缩短发现时间并节省电量。
  2. 读写分离:写特征 AE01 与读特征 AE02 分开定义,写入使用 .withResponse 保证可靠交付,读取通过开启通知(Notify)实现被动接收——设备主动推送数据,而非客户端轮询。
  3. 主线程 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

三个值得注意的设计点:

  1. 回调线程与 UI 线程分离:CoreBluetooth 的回调可能发生在任意后台队列,直接更新 UILabel 会触发 UIKit 线程检查崩溃。代码用 DispatchQueue.main.async 将 UI 刷新切回主线程——这是 iOS 蓝牙开发的必守纪律。
  2. UTF-8 解码的容错:String(data:encoding:) 返回可选值,解码失败时用 ?? "Invalid Data" 兜底,避免 nil 传播到 UI。
  3. 按 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 更新接收标签

流程要点回顾:

  1. 连接事件是前置条件:CentralViewController 通过 registerForConnectionEvents 监听 AE00 服务的连接事件,事件到达后才创建并跳转到外设交互页面(selectedPeripheral 随页面注入)。
  2. 两级发现串行推进:服务发现 → 特征发现 → 通知订阅,每一级都依赖上一级的回调结果,形成严格的依赖链。任一级出错(回调带 error)都会中断后续流程。
  3. 写入与接收是两条独立通路:上行走 writeValue(.withResponse)(请求-确认),下行走 Notify(订阅-推送),二者互不阻塞,符合全双工通讯模型。
  4. 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 决定,它们是唯一需要与设备固件对齐的"配置":

常量类型值作用
sampleServiceUUIDCBUUID"AE00"服务标识;连接事件匹配 + 服务发现目标
writeCharacteristicUUIDCBUUID"AE01"写特征;writeValue 目标
readCharacteristicUUIDCBUUID"AE02"读特征;setNotifyValue 订阅目标

Source: CentralViewController.swift

修改建议:若对接不同协议版本的设备,只需更新这三个 UUID;但必须同步修改 CentralViewController 中 registerForConnectionEvents 的 matchingOptions,否则连接事件不会触发,外设交互页面永远不会被打开。

系统权限配置(Info.plist)

外设交互依赖蓝牙权限,工程已在 Info.plist 中声明(详见 Info.plist):

Key值说明
NSBluetoothAlwaysUsageDescription蓝牙使用用途描述首次使用时系统弹窗文案
UIBackgroundModesbluetooth-central可选;支持后台蓝牙事件

运行环境要求

项目要求
iOS13.0+(registerForConnectionEvents 的最低可用版本)
Xcode14.0+
语言Swift(工程为纯 Swift)

API 参考

PeripheralViewController 类

UIViewController 子类,外设交互页面的唯一控制器。持有中心管理器引用(本页面不使用)与已选外设引用。

属性:

属性类型说明
cbManagerCBCentralManager!中心管理器(由上层注入,交互阶段不使用)
selectedPeripheralCBPeripheral!已连接的外设(由上层注入,交互阶段的操作对象)
writeCharacteristicCBCharacteristic?写特征 AE01,特征发现后赋值
readCharacteristicCBCharacteristic?读特征 AE02,特征发现后赋值
sendButton / inputTextField / receivedDataLabelUI 控件页面 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 收到 erroros_log 记录后 return,流程终止(L86-L90)
特征发现失败didDiscoverCharacteristicsFor 收到 erroros_log 记录后 return,不订阅、不写入
设备不暴露 AE01writeCharacteristic 保持 nilsendData 的 guard 拦截,仅记录 "Write characteristic not found"(L73-L76)
设备不暴露 AE02readCharacteristic 保持 nil无通知数据,接收标签保持初始文案;无崩溃
接收数据非 UTF-8String(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 交互问题。

扩展点

  1. 写入确认:实现 peripheral(_:didWriteValueFor:error:) 以感知 .withResponse 的写入结果,可据此做重试或 UI 反馈(当前代码未实现,写入错误只能依赖系统日志)。
  2. 主动读取:在特征发现后调用 peripheral.readValue(for: readCharacteristic) 可获取设备当前值(一次性读取),与 Notify 互补。
  3. 取消订阅与断开清理:在页面 dealloc/viewWillDisappear 中调用 setNotifyValue(false, for:) 与 cancelPeripheralConnection,避免后台残留订阅。
  4. 新增特征/服务:在 BTConstants 扩展常量,并在 didDiscoverCharacteristicsFor 中增加匹配分支即可支持更多协议通道(如带分片的大数据通道)。
  5. 输入内容类型扩展:当前固定 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
Prev
中心设备:连接事件监听与设备列表