原生 CoreBluetooth 连接
本页介绍 JL OTA SDK 的"自定义蓝牙连接"方式:宿主 App 直接使用 Apple CoreBluetooth 框架(CBCentralManager / CBPeripheral)完成扫描、连接、发现服务与特征、订阅通知与数据收发,并将这条原生 BLE 通道桥接给 JL_OTAManager 与 JLHashHandler 来执行设备认证与 OTA 升级。
Purpose and Scope
本页覆盖以下内容:
- 原生连接管理器
BleManager(CBCentralManagerDelegate/CBPeripheralDelegate)的职责:扫描、连接、断开、MTU 分片写入、重连与超时控制。 - 与 OTA SDK 的衔接点:连接就绪后初始化 OTA 上下文(
mBLE_UUID/mBLE_NAME)、noteEntityConnected()、cmdTargetFeature()、cmdOTAData()、cmdOtaDataReceive()。 - 认证数据与 OTA 数据的分流逻辑(
JLHashHandler.inputPairDatavscmdOtaDataReceive)。 - 升级失败场景下的重连与重试策略(UUID / MAC 重连、命令超时重试)。
以下主题属于兄弟页面,不在本页展开:
- SDK 内置蓝牙管理(
JL_BLEKit.xcframework的BleManager/JL_EntityM),请参见 bluetooth-connectivity.sdk-blekit。 - GATT Over EDR(经典蓝牙通道承载 GATT),请参见 bluetooth-connectivity.gatt-over-edr。
- OTA 协议命令(Opcode 0xe2–0xe8)与
JL_OTAResult枚举语义,请参见 ota-protocol。
Overview
JL OTA SDK 提供两种蓝牙接入方式:
- SDK 内置蓝牙管理:使用
JL_BLEKit.xcframework,由 SDK 内部封装扫描、连接与数据收发,App 只需调用JL_EntityM/BleManager的高层接口。 - 自定义蓝牙连接(本页主题):App 自己持有
CBCentralManager并实现CBCentralManagerDelegate/CBPeripheralDelegate,把CBPeripheral与特征通道交给JL_OTAManager使用。SDK 只负责 OTA 业务逻辑与数据解析,不感知蓝牙链路细节。
选择自定义连接方式的典型场景:App 已有成熟的蓝牙连接层、需要同时管理多台设备、或需要与既有配对/认证逻辑共存。此模式下 SDK 侧只要求三件事:告知设备标识(mBLE_UUID / mBLE_NAME)、通知连接状态(noteEntityConnected / noteEntityDisconnected)、提供数据收发回调(otaDataSend 写设备、cmdOtaDataReceive 收设备数据)。
Architecture
flowchart TD
subgraph sg_App["App 层(自定义蓝牙)"]
VC["ViewController"]
OTA["OTAActionManager"]
BM["BleManager<br/>CBCentralManagerDelegate<br/>CBPeripheralDelegate"]
end
subgraph sg_CB["CoreBluetooth 原生栈"]
CM["CBCentralManager"]
PERI["CBPeripheral"]
end
subgraph sg_SDK["JL OTA SDK"]
OM["JL_OTAManager"]
HH["JLHashHandler"]
LIB["JL_OTALib"]
end
subgraph sg_Dev["杰理设备"]
DEV["耳机 / 音箱<br/>(GATT Server)"]
end
VC -->|"选择设备"| BM
BM -->|"scan / connect / write"| CM
CM -->|"发现与连接"| PERI
PERI -->|"服务发现 / 通知 / 读写"| DEV
OM -->|"otaDataSend 数据下发"| BM
BM -->|"cmdOtaDataReceive / inputPairData 数据上送"| OM
OM -->|"认证数据"| HH
HH -->|"hashOnPairOutputData 写回设备"| BM
OM -->|"OTA 流程控制"| LIB
架构要点:
BleManager是原生桥:它是 App 与 CoreBluetooth 之间的唯一入口,同时承担连接生命周期管理与数据通道职责,是 SDK 回连、数据写入的落点。JL_OTAManager只面向业务:它不直接持有CBPeripheral,而是通过 delegate 回调(otaDataSend)向BleManager请求写数据,通过cmdOtaDataReceive接收设备上行数据,实现"蓝牙无关"的 OTA 引擎。- 认证与升级共用同一条原生通道:连接建立后,
JLHashHandler先完成配对认证(bluetoothPairingKey),认证通过后再由JL_OTAManager执行特性查询与 OTA,两条数据流在BleManager处汇合。
Core Flow
sequenceDiagram
participant VC as ViewController
participant BM as BleManager
participant CB as CoreBluetooth
participant OM as JL_OTAManager
participant HH as JLHashHandler
participant DEV as 杰理设备
VC->>BM: startScan()
BM->>CB: scanForPeripherals(withServices: nil)
CB-->>BM: didDiscover(peripheral, advertisementData, rssi)
VC->>BM: connect(peripheral:)
BM->>CB: connect(peripheral, options: nil)
CB-->>BM: didConnect(peripheral)
BM->>CB: discoverServices(nil)
CB-->>BM: didDiscoverServices
BM->>CB: discoverCharacteristics(for: service)
CB-->>BM: didDiscoverCharacteristicsFor
BM->>CB: setNotifyValue(true, for: characteristicWrite)
CB-->>BM: didUpdateNotificationStateFor
BM-->>OM: onPeripheralReady → mBLE_UUID / mBLE_NAME
OM->>OM: noteEntityConnected()
OM->>OM: cmdTargetFeature()
OM-->>HH: 认证触发(hashResetPair / bluetoothPairingKey)
HH-->>BM: hashOnPairOutputData(data)
BM->>DEV: writeValue(data, for: characteristic, .withoutResponse)
DEV-->>BM: didUpdateValueFor(characteristic)
BM-->>HH: inputPairData(data)(认证前)
OM-->>BM: otaDataSend(data)
BM->>DEV: writeValue(按 MTU 分片)
DEV-->>BM: didUpdateValueFor(characteristic)
BM-->>OM: cmdOtaDataReceive(data)(认证后)
流程说明:
BleManager.startScan()以withServices: nil全量扫描,扫描结果在didDiscover中回调。- 用户选中设备后
connect(peripheral:),成功后 SDK 需要先拿到设备标识(UUID 与名称),再调用noteEntityConnected()通知 OTA 引擎"上下文就绪"。 - 连接建立后,原生栈完成服务发现、特征发现与通知订阅;
didUpdateNotificationStateFor代表数据通道打通,此时才初始化 OTA 上下文(onPeripheralReady)。 - 认证阶段,
JLHashHandler产出的配对数据经hashOnPairOutputData写入设备,设备响应进入inputPairData。 - 认证完成后进入 OTA 阶段:
JL_OTAManager通过otaDataSend把待发送数据交给BleManager分片写入,设备回包统一经cmdOtaDataReceive交还 SDK 解析,驱动升级进度。
原生连接管理器(BleManager)
BleManager(单例 BleManager.shared)是自定义连接方式的核心,封装了全部 CoreBluetooth 原生调用。其公开行为可从开发示例文档的源码摘录中还原:
扫描与停止扫描
// 扫描与停止扫描
func startScan() {
centralManager.scanForPeripherals(withServices: nil, options: nil)
}
func stopScan() {
centralManager.stopScan()
}
Source: OTA 升级开发示例.md
设计意图:withServices: nil 表示不过滤服务 UUID,全量扫描。这是因为杰理设备的广播可能不包含目标 Service UUID,或 App 需要按广播包中的自定义字段(如厂商数据中的 MAC 地址)来筛选设备;过滤逻辑放在 didDiscover 回调中自行实现更灵活。
连接与断开
// 连接与断开
func connect(peripheral: CBPeripheral) {
centralManager.connect(peripheral, options: nil)
}
func disconnect(peripheral: CBPeripheral) {
centralManager.cancelPeripheralConnection(peripheral)
}
Source: OTA 升级开发示例.md
连接选项传 nil,表示不请求重连(CBConnectPeripheralOptionNotifyOnConnectionKey 等均为默认值)。断开统一走 cancelPeripheralConnection,SDK 侧会通过 JL_OTAResultDisconnect 感知并停止升级流程。
MTU 分片写入
// 数据写入(按设备 MTU 分片)
func write(data: Data) {
guard let characteristic = characteristicWrite, let peripheral = currentPeripheral else { return }
let mtu = peripheral.maximumWriteValueLength(for: .withoutResponse)
var len = 0
while len < data.count {
let end = min(len + mtu, data.count)
peripheral.writeValue(data.subdata(in: len ..< end), for: characteristic, type: .withoutResponse)
len = end
}
}
Source: OTA 升级开发示例.md
这是整个数据通道中最关键的实现:
- 写入类型固定为
.withoutResponse(无响应写),避免每包等待 ATT 应答,吞吐更高,适合 OTA 这种大数据量场景; - 每次写入前通过
maximumWriteValueLength(for:)查询当前连接协商出的 MTU(iOS 协商后通常为 185/247 字节),把一整帧 OTA 数据切成 MTU 大小的分片依次下发; characteristicWrite是 App 在didDiscoverCharacteristicsFor中保存的写特征(杰理协议中为 RCSP 写通道),currentPeripheral为当前连接的外设。
重连与超时控制
// 通过 UUID 重连设备并启动超时计时
func reConnectWithUUID(uuid: String) {
reconnectUUID = uuid
reconnectMac = nil
JLLogManager.logLevel(.DEBUG, content: "reConnectWithUUID: \(uuid)")
startScan()
startTimeout()
}
// 通过 MAC 地址重连设备并启动超时计时
func reConnectWithMac(mac: String) {
reconnectUUID = nil
reconnectMac = mac
JLLogManager.logLevel(.DEBUG, content: "reConnectWithMac: \(mac)")
startScan()
startTimeout()
}
Source: OTA 升级开发示例.md
设计意图:BLE 地址(reconnectMac)与系统分配的 UUID(reconnectUUID)是两套标识——iOS 对同一外设的 UUID 在系统层面基本稳定,但用户若"忽略此设备"或系统重置后会变化;MAC 地址则来自广播包中杰理厂商数据字段(kCBAdvDataManufacturerData),可跨系统稳定标识设备。两种重连入口对应 SDK 的 JL_OTAResultReconnect(UUID)与 JL_OTAResultReconnectWithMacAddr(MAC)两种回连诉求,startTimeout() 防止扫描阶段无限期挂起。
连接生命周期与 SDK 桥接
连接就绪的判定在 didUpdateNotificationStateFor 之后:只有通知订阅成功,设备上行数据才能到达,因此 OTA 上下文初始化(onPeripheralReady)必须发生在此之后。
// 3) 设备连接完成后初始化 OTA 上下文(接口规范)
private func onPeripheralReady(_ peripheral: CBPeripheral) {
otaManager.mBLE_UUID = peripheral.identifier.uuidString
otaManager.mBLE_NAME = peripheral.name ?? ""
otaManager.noteEntityConnected() // /doc/API 说明.md → 3.4.1 设备操作
otaManager.cmdTargetFeature() // /doc/API 说明.md → 3.3.3 状态查询
}
Source: OTA 升级开发示例.md
要点:
mBLE_UUID/mBLE_NAME是 SDK 识别设备、执行重连与日志排查的依据,必须在任何 OTA 命令前赋值;noteEntityConnected()通知 SDK 设备已连接,内部会复位命令序号(SN)等状态;cmdTargetFeature()查询设备特性(是否支持 OTA、TWS、EDR 等),其结果通过otaFeatureResult委托回调返回。
数据上行回调按认证状态分流(最小示例):
// 数据通道:认证数据与 OTA 数据分流
BleManager.shared.subNotifySubject
.subscribe(onNext: { [weak self] data in
guard let self = self else { return }
if !self.isAuthed {
self.auth.inputPairData(data)
return
}
self.otaManager.cmdOtaDataReceive(data)
})
.disposed(by: disposeBag)
Source: OTA 升级开发示例.md
设计意图:认证完成前后,同一根特征通知通道上的报文属于不同协议域。认证前的报文是 BLE 配对握手数据(交给 JLHashHandler.inputPairData),认证后的报文才是 RCSP/OTA 响应(交给 cmdOtaDataReceive)。用一个布尔状态(isAuthed)在入口处分流,避免认证报文被误解析为 OTA 命令导致协议错乱。
异常与重试策略
升级过程中断是 BLE 场景的高频故障,示例文档给出了三种典型处置(Objective-C 实现):
- (void)otaUpgradeResult:(JL_OTAResult)result Progress:(float)progress {
switch (result) {
case JL_OTAResultReconnect: {
NSString *uuid = self.otaManager.mBLE_UUID;
[[BleManager shared] reConnectWithUUID:uuid];
break;
}
case JL_OTAResultReconnectWithMacAddr: {
NSString *mac = self.otaManager.bleAddr;
[[BleManager shared] reConnectWithMac:mac];
break;
}
case JL_OTAResultFailCmdTimeout: {
if (self.retryCount < 3) {
self.retryCount += 1;
NSTimeInterval delay = (NSTimeInterval)self.retryCount;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.otaManager cmdTargetFeature];
});
}
break;
}
case JL_OTAResultLowPower:
case JL_OTAResultDisconnect:
default:
break;
}
}
Source: OTA 升级开发示例.md
策略解读:
JL_OTAResultReconnect:SDK 期望按 UUID 回连(常见于升级第一阶段设备重启后广播变化,但系统 UUID 未变);JL_OTAResultReconnectWithMacAddr:SDK 期望按 MAC 回连(常见于设备重启后广播名/UUID 变化,只能靠广播包厂商数据中的 MAC 定位);JL_OTAResultFailCmdTimeout:命令超时,采用"最多重试 3 次、延时按次数递增(1s/2s/3s)"的退避策略重新查询特性,避免高频重试加剧链路拥塞;JL_OTAResultLowPower/JL_OTAResultDisconnect:低电与断连属不可恢复或需人工介入的场景,示例中直接放弃重试。
数据模型与关键状态
原生连接层不需要持久化存储,但存在一组运行时状态,驱动重连与写入逻辑:
| 状态字段 | 用途 | 来源 |
|---|---|---|
currentPeripheral | 当前连接的 CBPeripheral,写入目标 | didConnect 时保存 |
characteristicWrite | 已发现的写特征(RCSP 写通道) | didDiscoverCharacteristicsFor 时保存 |
reconnectUUID | 待重连设备的系统 UUID | reConnectWithUUID(uuid:) 设置 |
reconnectMac | 待重连设备的 MAC 地址 | reConnectWithMac(mac:) 设置 |
isAuthed | 认证状态,决定上行数据分流方向 | 认证回调 bluetoothPairingKey 置位 |
subNotifyInitSubject | 通知订阅完成事件(RxSwift) | didUpdateNotificationStateFor 触发 |
subNotifySubject | 设备上行数据流(RxSwift) | didUpdateValueFor 触发 |
这些状态只存活于内存中;跨启动恢复设备连接依赖系统蓝牙持久化(CoreBluetooth 会按 CBPeripheral.identifier 保留已知外设)。
使用示例
基础示例:完整 OTA 调用链(Swift)
import UIKit
import CoreBluetooth
import JL_OTALib
/// OTA 示例控制器:演示 JL_OTAManager 标准调用流程
final class OTAExampleViewController: UIViewController, JL_OTAManagerDelegate {
private let otaManager = JL_OTAManager.getOTAManager() // 接口文档:getOTAManager
override func viewDidLoad() {
super.viewDidLoad()
// 1) 绑定 delegate(接口规范)
otaManager.delegate = self // /doc/API 说明.md → 3.5 回调协议
// 2) 扫描并连接设备
BleManager.shared.startScan()
// 选择设备后:BleManager.shared.connect(peripheral: sel)
}
// 3) 设备连接完成后初始化 OTA 上下文(接口规范)
private func onPeripheralReady(_ peripheral: CBPeripheral) {
otaManager.mBLE_UUID = peripheral.identifier.uuidString
otaManager.mBLE_NAME = peripheral.name ?? ""
otaManager.noteEntityConnected() // /doc/API 说明.md → 3.4.1 设备操作
otaManager.cmdTargetFeature() // /doc/API 说明.md → 3.3.3 状态查询
}
// 4) 发起 OTA 升级(委托驱动进度,接口文档:cmdOTAData)
private func startUpgrade(with data: Data) {
otaManager.cmdOTAData(data) // /doc/API 说明.md → 3.4.2 OTA 操作
}
// 5) 取消升级(接口文档:cmdOTACancelResult)
private func cancelUpgrade() {
otaManager.cmdOTACancelResult { result in
print("OTA 取消:\(result)")
}
otaManager.noteEntityDisconnected() // /doc/API 说明.md → 3.4.1 设备操作
}
// 6) 委托回调(接口文档:JL_OTAManagerDelegate)
func otaUpgradeResult(_ result: JL_OTAResult, progress: Float) {
print("升级状态:\(result) 进度:\(progress)") // /doc/API 说明.md → 3.5 回调协议
}
func otaDataSend(_ data: Data) {
BleManager.shared.write(data: data)
}
func otaCancel() {}
func otaFeatureResult(_ manager: JL_OTAManager) {}
}
Source: OTA 升级开发示例.md
高级示例:订阅完成后自动认证 + 查询特性
import RxSwift
import JL_OTALib
import JL_HashPair
import CoreBluetooth
/// 订阅完成后自动执行设备认证并查询特性的最小示例
final class OTAAutoAuthMini: JLHashHandlerDelegate {
private let otaManager = JL_OTAManager.getOTAManager()
private let auth = JLHashHandler()
private let disposeBag = DisposeBag()
private var isAuthed = false
init() {
auth.delegate = self
// 订阅完成:设置设备标识并触发认证与特性查询
BleManager.shared.subNotifyInitSubject
.subscribe(onNext: { [weak self] peripheral in
guard let self = self else { return }
self.otaManager.mBLE_UUID = peripheral.identifier.uuidString
self.otaManager.mBLE_NAME = peripheral.name ?? ""
self.otaManager.noteEntityConnected()
self.auth.hashResetPair()
self.auth.bluetoothPairingKey(nil) { status in
if status {
self.isAuthed = true
self.otaManager.cmdTargetFeature()
}
}
})
.disposed(by: disposeBag)
// 数据通道:认证数据与 OTA 数据分流
BleManager.shared.subNotifySubject
.subscribe(onNext: { [weak self] data in
guard let self = self else { return }
if !self.isAuthed {
self.auth.inputPairData(data)
return
}
self.otaManager.cmdOtaDataReceive(data)
})
.disposed(by: disposeBag)
}
// 认证输出:写入设备
func hash(onPairOutputData data: Data) {
BleManager.shared.write(data: data)
}
}
Source: OTA 升级开发示例.md
配置选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
scanForPeripherals(withServices:) | [CBUUID]? | nil(全量扫描) | 扫描过滤;示例不传服务 UUID,靠 didDiscover 回调自行筛选 |
connect(peripheral:options:) | [String: Any]? | nil | 连接选项;示例未启用系统级重连提示 |
| 写入类型 | CBCharacteristicWriteType | .withoutResponse | OTA 数据全部使用无响应写以提升吞吐 |
| 分片大小 | Int(由系统返回) | peripheral.maximumWriteValueLength(for: .withoutResponse) | 随连接协商 MTU 动态变化,每次写入前查询 |
| 重连方式 | String? | 二选一(UUID / MAC) | reConnectWithUUID 或 reConnectWithMac,由 JL_OTAResult 决定 |
| 命令超时重试 | Int | 最多 3 次,延时 1s/2s/3s 递增 | 应对 JL_OTAResultFailCmdTimeout |
| 日志等级 | JLLogManager.logLevel | .DEBUG | 重连入口打印 UUID/MAC,便于联调定位 |
上述默认值均取自 OTA 升级开发示例.md 的源码摘录;实际工程中这些参数可随业务需要调整。
API Reference(原生连接层)
以下方法为 BleManager 暴露给上层与 SDK 的关键接口(签名依据示例源码摘录整理):
startScan()
启动 CoreBluetooth 全量扫描。扫描回调 centralManager(_:didDiscover:advertisementData:rssi:) 中可结合 peripheral.name、广播包厂商数据(MAC)与目标 UUID 做重连匹配。
stopScan()
停止扫描。进入连接阶段或重连成功后应调用,以节省功耗。
connect(peripheral: CBPeripheral)
发起连接。参数: peripheral — 待连接外设(来自扫描结果)。连接结果在 centralManager(_:didConnect:) 回调。
disconnect(peripheral: CBPeripheral)
调用 cancelPeripheralConnection 断开。SDK 侧表现为 JL_OTAResultDisconnect。
write(data: Data)
把一帧数据按当前 MTU 分片后写入写特征。 参数: data — 完整帧(认证数据或 OTA 数据)。 前置条件: characteristicWrite 与 currentPeripheral 已就绪;无响应写不产生逐包应答,写入即返回。
reConnectWithUUID(uuid: String)
按系统 UUID 重连:清空 reconnectMac、设置 reconnectUUID、启动扫描与超时计时。
reConnectWithMac(mac: String)
按 MAC 重连:清空 reconnectUUID、设置 reconnectMac、启动扫描与超时计时。MAC 通常由广播包厂商数据解析(kCBAdvDataManufacturerData)。
失败模式、边界情况与并发
- 升级中断开连接:
didDisconnectPeripheral后 SDK 返回JL_OTAResultDisconnect,示例策略为不自动重连、交由用户处理;JL_OTAResultReconnect/JL_OTAResultReconnectWithMacAddr才触发自动回连。 - 设备重启后广播变化:OTA 第一阶段升级后设备会重启并可能更换广播名,系统 UUID 可能保留也可能失效,因此 SDK 提供两种回连标识(UUID 与 MAC),App 需同时实现两条路径。
- 命令超时:
JL_OTAResultFailCmdTimeout采用退避重试(最多 3 次、延时递增),防止高频重试加剧链路拥塞;重试仍失败则停止。 - 低电量:
JL_OTAResultLowPower直接放弃,避免升级中途断电变砖。 - 并发写入:示例采用
.withoutResponse串行分片写入,无显式发送队列;在升级大流量场景下,建议由JL_OTAManager的流控(等待cmdOtaDataReceive回包再发下一帧)天然限速,App 侧不应自行并发写入多条 OTA 数据。 - 空特征/空外设:
write(data:)以guard let兜底,characteristicWrite或currentPeripheral缺失时静默返回,避免崩溃;调试时应通过JLLogManager确认服务发现时序。
性能与运维注意事项
- 吞吐关键在分片:无响应写 + 按
maximumWriteValueLength分片是 OTA 速度的决定因素;若自行实现,勿用.withResponse或固定 20 字节 MTU 硬分片。 - 日志可观测性:重连入口(
reConnectWithUUID/reConnectWithMac)均打印.DEBUG日志,包含 UUID/MAC,配合 测试调试 文档可快速定位断连原因。 - 扫描功耗:
startScan()后务必在连接成功或超时后stopScan();重连路径用startTimeout()兜底,防止扫描无限期运行。
扩展点
JL_OTAManagerDelegate:otaUpgradeResult(_:progress:)、otaDataSend(_:)、otaCancel()、otaFeatureResult(_:)是 App 与 OTA 引擎的全部交互面,可在此接入 UI 进度条、重连弹窗等。JLHashHandlerDelegate:hash(onPairOutputData:)提供认证输出回调,App 可替换写入实现(例如写入前加密、日志记录)。- RxSwift 数据流:
subNotifyInitSubject(订阅完成事件)与subNotifySubject(上行数据流)是 BleManager 暴露的响应式扩展点,便于与既有 Rx 架构整合;非 Rx 工程可直接在 delegate 回调中实现等价逻辑。