设备扫描与广播发现
本文档介绍 iOS-JL_OTA SDK 中 BLE 设备扫描与广播(Advertisement)发现机制的完整实现,覆盖自定义蓝牙接入(JL_Assist)与 SDK 内置蓝牙接入(JL_BLEKit)两种模式下的扫描 API、发现回调、设备去重、广播包解析(含 MAC 提取)以及 OTA 回连场景下的 UUID/MAC 匹配逻辑。
Purpose and Scope
本页聚焦「如何发现周边杰理 BLE 设备」这一子系统能力,具体包括:
- 两种蓝牙接入模式下扫描的启动/停止(
startScan/stopScan/scanStart) - 设备发现回调(
centralManager(_:didDiscover:...)、kJL_BLE_M_FOUND通知)中的去重、过滤与列表发布 - 广播包字段解析(
kCBAdvDataLocalName、kCBAdvDataManufacturerData)与JLAdvParse的 MAC 提取比对 - OTA 升级中断后按 UUID/MAC 回连设备时依赖扫描进行的目标匹配
- 扫描超时控制与相关配置项
不属于本页范围(由同级目录页承担):GATT 服务/特征发现与数据收发、BLE 握手认证(Pair)、OTA 升级命令流程、设备固件信息查询。本页只在扫描发现完成后作为流程入口简要提及这些后续步骤,详细内容请参阅对应目录页(见 Related Links)。
Overview
BLE(Bluetooth Low Energy)场景下,手机作为 Central(中心设备)必须通过广播扫描才能感知周边作为 Peripheral(外围设备)的杰理耳机/音箱。扫描是 OTA 全流程的入口:用户先在设备列表中选择目标设备,SDK 或 App 再据此发起连接。
仓库中的 Demo 提供了两种等价的接入方式,其扫描实现路径不同:
- 自定义蓝牙连接(JL_Assist / JL_OTALib):App 自持
CBCentralManager,通过BleManager单例直接调用scanForPeripherals(withServices:options:)扫描,在didDiscover回调中自行维护设备列表(RxSwiftdiscoverPeripheralsSubject),并借助JLAdvParse解析广播包提取 MAC。 - SDK 内置蓝牙连接(JL_BLEKit):App 使用
JL_BleMultiple(Demo 中的bleMutipleManager)封装好的scanStart(),SDK 内部完成扫描,通过NotificationCenter发布kJL_BLE_M_FOUND,设备实体为JL_EntityM列表(blePeripheralArr)。
两种模式都会遇到两个关键工程问题,这也是本页要解释的核心设计意图:
- 去重与稳定标识:同一设备在扫描周期内会被系统重复上报(每次广播都会触发回调),必须用
peripheral.identifier(自定义模式)或mUUID(SDK 模式)去重,才能得到稳定的设备列表。 - OTA 回连的目标识别:设备进入升级模式(Bootloader)后可能更换广播名称甚至不广播名称,此时只能依赖广播包中的 Manufacturer Data 内嵌 MAC 地址来识别目标设备。这正是
JLAdvParse.otaBleMacAddress(_:isEqualToCBAdvDataManufacturerData:)存在的意义。
Architecture
下图展示两种接入模式下扫描与广播发现的完整组件关系:
flowchart TD
subgraph sg_App["App 层"]
UI["设备列表 UI"]
Rx["discoverPeripheralsSubject (RxSwift)"]
NC["NotificationCenter 观察者"]
end
subgraph sg_Custom["自定义蓝牙模式 (JL_Assist)"]
BM["BleManager (单例)"]
CM["CBCentralManager"]
AP["JLAdvParse (广播解析)"]
end
subgraph sg_SDK["SDK 蓝牙模式 (JL_BLEKit)"]
BMM["JL_BleMultiple (bleMutipleManager)"]
EA["blePeripheralArr [JL_EntityM]"]
end
subgraph sg_Dev["BLE 外设"]
DEV["杰理耳机/音箱 (AE00 服务)"]
ADV["广播包: LocalName / ManufacturerData"]
end
UI --> Rx
UI --> NC
Rx --> BM
BM --> CM
CM -->|"scanForPeripherals 扫描广播"| ADV
ADV -->|"didDiscover 回调"| BM
BM --> AP
AP -->|"MAC 提取与比对"| BM
BM -->|"accept(设备列表)"| Rx
BMM -->|"scanStart() 扫描广播"| ADV
ADV -->|"广播包"| BMM
BMM --> EA
EA -->|"kJL_BLE_M_FOUND 通知"| NC
ADV --> DEV
架构要点说明:
- 两条扫描路径最终都汇聚到同一目标——发现携带
AE00服务(jl_BLE_SERVICE)广播的杰理设备;区别只在于「谁在扫」:自定义模式由 App 的BleManager直接驱动CBCentralManager,SDK 模式由JL_BleMultiple内部驱动。 JLAdvParse是广播包解析的公共工具类,自定义模式下 App 显式调用它做 MAC 比对;SDK 模式下 SDK 内部亦使用jl_advpars能力完成相似工作(Demo 中的JLAdvParse.otaBleMacAddress即其公开接口)。- 设备列表的对外发布方式不同:自定义模式用 RxSwift
Subject推送数组;SDK 模式通过NotificationCenter广播kJL_BLE_M_FOUND,由 App 从blePeripheralArr读取并自行过滤。
自定义蓝牙模式(JL_Assist)的扫描实现
扫描的启动与停止
自定义模式下,BleManager 直接操作系统 CBCentralManager。扫描使用 scanForPeripherals(withServices:options:),Demo 中传入 nil 服务过滤,即全量扫描周边所有广播设备——设计意图是让 App 能够发现不广播服务 UUID 的早期固件设备,代价是需要在上层自行过滤:
// 扫描与停止扫描
func startScan() {
centralManager.scanForPeripherals(withServices: nil, options: nil)
}
func stopScan() {
centralManager.stopScan()
}
Source: OTA 升级开发示例.md
连接建立后必须调用 stopScan()——一方面节省系统资源与电量,另一方面避免扫描回调与已连接设备的事件互相干扰(见下文 didConnect 中的 stopScan())。
发现回调:去重、名称过滤与列表发布
centralManager(_:didDiscover:advertisementData:rssi:) 是扫描结果的核心入口。系统对每个正在广播的设备按广播周期重复回调,因此实现做了两件事:按 peripheral.identifier 去重与仅收录有广播名称的设备,然后通过 RxSwift discoverPeripheralsSubject 向 UI 发布最新列表:
// 扫描回调中的重连判断(UUID/MAC)
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if peripheral.name != nil {
JLLogManager.logLevel(.DEBUG, content: "发现设备: \(peripheral.name!)")
discoverPeripherals.removeAll(where: { $0.identifier == peripheral.identifier })
discoverPeripherals.append(peripheral)
discoverPeripheralsSubject.accept(discoverPeripherals)
}
if reconnectUUID != nil {
if peripheral.identifier.uuidString == reconnectUUID {
reconnectUUID = nil
stopScan()
connect(peripheral: peripheral)
return
}
}
if reconnectMac != nil {
guard let advData = advertisementData["kCBAdvDataManufacturerData"] as? Data else { return }
guard let mac = reconnectMac else { return }
if JLAdvParse.otaBleMacAddress(mac, isEqualToCBAdvDataManufacturerData: advData) {
stopScan()
reconnectMac = nil
connect(peripheral: peripheral)
return
}
}
}
Source: OTA 升级开发示例.md
该回调同时承担普通发现与OTA 回连匹配两个职责,设计要点:
peripheral.name != nil过滤:无名称设备(如刚进入 Bootloader 的升级模式设备)不会被加入列表,但仍然参与回连匹配——因为回连分支在名称判断之后执行,且 MAC 匹配不依赖名称。这保证了升级模式设备虽不显示在列表中,却能被静默找回。- 去重采用「先删后加」:
removeAll(where:)后append,保证列表顺序为最近一次发现的时间序,同时避免重复条目。 - 回连匹配命中即
stopScan()并立即connect(peripheral:),且将对应的reconnectUUID/reconnectMac置nil,防止后续重复匹配与重复连接。
OTA 回连:UUID 与 MAC 两条匹配路径
设备在升级第一/二阶段之间会重启进入 Bootloader,此时 App 需要重新扫描并连接同一设备。Demo 提供两种回连触发方式:
// 通过 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
两条路径共享同一套超时控制:Timer 每 1 秒触发一次 timeoutHandler,累计 10 次(maxCount = 10)即判定回连超时并输出错误日志:
// 超时控制
@objc private func timeoutHandler() {
timerCount += 1
if timerCount >= maxCount {
timer?.invalidate()
timer = nil
timerCount = 0
JLLogManager.logLevel(.ERROR, content: "连接超时")
}
}
private func startTimeout() {
maxCount = 10
timerCount = 0
timer?.invalidate()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timeoutHandler), userInfo: nil, repeats: true)
timer?.fire()
}
private func stopTimeout() {
timer?.invalidate()
timer = nil
timerCount = 0
}
Source: OTA 升级开发示例.md
设计意图:UUID 匹配依赖 iOS 为外设分配的稳定 identifier,绝大多数场景可用;MAC 匹配用于 iOS 重装系统/系统重置后 UUID 失效、或设备进入 Bootloader 后仍广播相同 MAC 的场景。MAC 匹配需要从广播包的 kCBAdvDataManufacturerData 中解析出设备真实 MAC,这正是 JLAdvParse 的职责。
连接成功后的扫描收尾
didConnect 中停止扫描与超时计时器,避免连接后系统仍持续回调:
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
JLLogManager.logLevel(.DEBUG, content: "连接设备成功")
currentPeripheral = peripheral
currentUUID = peripheral.identifier.uuidString
peripheral.delegate = self
peripheral.discoverServices(nil)
stopScan()
stopTimeout()
}
Source: OTA 升级开发示例.md
连接建立后即转入 GATT 服务发现(discoverServices(nil) 后按 SERVICE_UUID 过滤),该部分属于「连接与 GATT」目录页范畴,此处仅作流程衔接。
SDK 蓝牙模式(JL_BLEKit)的扫描与发现
扫描参数配置
使用 SDK 模式时,App 先配置 JL_BleMultiple(Demo 中 bleMutipleManager)的协议常量与行为开关,再注册发现通知:
bleMutipleManager.jl_BLE_SERVICE = SERVICE_UUID // "AE00"
bleMutipleManager.jl_BLE_RCSP_W = CHARACTERISTIC_WRITE // "AE01"
bleMutipleManager.jl_BLE_RCSP_R = CHARACTERISTIC_NOTIFY // "AE02"
bleMutipleManager.ble_FILTER_ENABLE = false
bleMutipleManager.ble_TIMEOUT = 10
// 监听 SDK 发布的通知
NotificationCenter.default.addObserver(self, selector: #selector(discoverEntity(_:)), name: NSNotification.Name(kJL_BLE_M_FOUND), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(currentEntityUpdate(_:)), name: NSNotification.Name(kJL_BLE_M_ENTITY_CONNECTED), object: nil)
Source: OTA升级开发示例(SDK蓝牙连接).md
配置项语义:
jl_BLE_SERVICE/jl_BLE_RCSP_W/jl_BLE_RCSP_R:杰理私有协议的服务 UUID(AE00)与读写/通知特征 UUID(AE01/AE02),SDK 扫描时据此识别杰理设备。ble_FILTER_ENABLE:是否启用 SDK 内置设备过滤(按名称前缀filterKey等条件);Demo 置false表示信任 App 侧过滤。ble_TIMEOUT:SDK 连接/扫描超时秒数,Demo 设为 10。
扫描启动与发现通知处理
func startScan() {
bleMutipleManager.scanStart()
}
// 发现设备回调处理
@objc private func discoverEntity(_ _: Notification) {
let entities = bleMutipleManager.blePeripheralArr as! [JL_EntityM]
var newEntitys: [JL_EntityM] = []
// 过滤去重
for entity in entities {
if entity.mItem.count > 0 {
newEntitys.removeAll(where: { $0.mUUID == entity.mUUID })
newEntitys.append(entity)
}
}
discoverPeripheralsSubject.accept(newEntitys)
}
Source: OTA升级开发示例(SDK蓝牙连接).md
与自定义模式对照:SDK 模式用 scanStart() 一键扫描,每次 kJL_BLE_M_FOUND 通知时从 blePeripheralArr 全量重建列表;过滤条件为 mItem.count > 0(实体已携带解析出的广播信息条目),并按 mUUID 去重。JL_EntityM 是 SDK 的设备实体模型,mUUID 对应 CoreBluetooth 的 identifier,mItem 为广播解析结果集合。
连接 / 断开 / 回连 API
// 发起连接
func connect(entity: JL_EntityM) {
bleMutipleManager.connectEntity(entity) { status in
JLLogManager.logLevel(.DEBUG, content: "connect status: \(status)")
if status == .paired {
self.subConnectInitSubject.onNext(entity)
}
}
}
func disconnect(entity: JL_EntityM) {
bleMutipleManager.disconnectEntity(entity) { status in
if status == .paired {
self.disconnectSubject.onNext(entity)
}
}
}
// 通过 UUID 重连(用于 OTA 过程中的回连)
func reConnectWithUUID(uuid: String) {
guard let entity = bleMutipleManager.makeEntity(withUUID: uuid) else {
JLLogManager.logLevel(.ERROR, content: "reConnectWithUUID: \(uuid) error")
return
}
connect(entity: entity)
}
// 通过 MAC 地址重连
func reConnectWithMac(mac: String) {
JLLogManager.logLevel(.DEBUG, content: "reConnectWithMac: \(mac)")
bleMutipleManager.connectEntity(forMac: mac) { status in
if status == .paired {
self.reconnectSubject.onNext(Void())
}
}
}
Source: OTA升级开发示例(SDK蓝牙连接).md
SDK 模式将回连封装为 makeEntity(withUUID:)(按 UUID 构造实体)与 connectEntity(forMac:)(按 MAC 直连,SDK 内部完成广播解析比对),App 侧无需接触 CBCentralManager 与广播字典——这是两种模式最大的分工差异。
广播包解析与设备识别(JLAdvParse)
广播包关键字段
CoreBluetooth 的 didDiscover 回调携带 advertisementData 字典,杰理设备识别依赖两个字段:
| 字段 | 用途 |
|---|---|
kCBAdvDataLocalName | 设备广播名称,用于列表显示与名称过滤 |
kCBAdvDataManufacturerData | 厂商自定义数据,内嵌设备真实 MAC 地址,用于回连匹配 |
JLAdvParse 的 MAC 提取与比对
JLAdvParse 提供静态方法 otaBleMacAddress(_:isEqualToCBAdvDataManufacturerData:):从 Manufacturer Data 中按杰理私有协议偏移提取 6 字节 MAC,再与传入的 mac 字符串比较。自定义模式回连时的典型用法(前文 didDiscover 中已见):
guard let advData = advertisementData["kCBAdvDataManufacturerData"] as? Data else { return }
guard let mac = reconnectMac else { return }
if JLAdvParse.otaBleMacAddress(mac, isEqualToCBAdvDataManufacturerData: advData) {
stopScan()
reconnectMac = nil
connect(peripheral: peripheral)
return
}
Source: OTA 升级开发示例.md
设计意图:iOS 会对外设 MAC 做随机化处理(identifier 与真实 MAC 无关),而杰理设备广播包中携带的 MAC 在普通模式与 Bootloader 模式间保持一致,因此 MAC 是 OTA 回连最可靠的稳定标识。若广播包不含 Manufacturer Data(as? Data 失败),回调直接 return,继续等待下一个广播包——这种「守护式解析」避免了对缺失字段的崩溃风险。
Core Flow
自定义模式:普通发现 + OTA 回连时序
sequenceDiagram
participant App as App / UI
participant BM as BleManager
participant CM as CBCentralManager
participant Dev as BLE 设备
participant AP as JLAdvParse
App->>BM: startScan()
BM->>CM: scanForPeripherals(withServices: nil, options: nil)
CM->>Dev: 扫描广播
Dev-->>CM: 广播包 (LocalName / ManufacturerData)
CM-->>BM: didDiscover(peripheral, advertisementData, rssi)
BM->>BM: 按 identifier 去重 + name 非空过滤
BM-->>App: discoverPeripheralsSubject.accept(设备列表)
Note over App,BM: OTA 回连场景(升级中断后)
App->>BM: reConnectWithMac(mac)
BM->>CM: startScan()(继续扫描)
CM-->>BM: didDiscover(peripheral, advertisementData, rssi)
BM->>AP: otaBleMacAddress(mac, isEqualToCBAdvDataManufacturerData: advData)
AP-->>BM: true(MAC 匹配)
BM->>CM: stopScan() + connect(peripheral)
CM-->>BM: didConnect
BM->>BM: stopScan() + stopTimeout()
BM->>CM: discoverServices(nil)(转入 GATT 流程)
SDK 模式:扫描发现流程图
flowchart TD
Start([App 调用 startScan]) --> Config["配置 jl_BLE_SERVICE=AE00<br/>jl_BLE_RCSP_W=AE01 / jl_BLE_RCSP_R=AE02"]
Config --> Scan["bleMutipleManager.scanStart()"]
Scan --> Notif["kJL_BLE_M_FOUND 通知触发"]
Notif --> Read["读取 blePeripheralArr (JL_EntityM 数组)"]
Read --> Filter{"mItem.count > 0?"}
Filter -->|"否"| Skip["丢弃该实体"]
Filter -->|"是"| Dedupe["按 mUUID 去重 (removeAll + append)"]
Dedupe --> Publish["discoverPeripheralsSubject.accept(newEntitys)"]
Publish --> UI[("设备列表 UI 刷新")]
UI --> Choice{"用户选择设备?"}
Choice -->|"connectEntity(entity)"| Paired{"回调 status == .paired?"}
Paired -->|"是"| Init["subConnectInitSubject.onNext(entity)<br/>进入初始化/认证流程"]
Paired -->|"否"| Fail["连接失败处理"]
Skip --> Read
回连目标匹配决策流
flowchart TD
Start([OTA 升级中断需要回连]) --> Decide{"回连依据?"}
Decide -->|"UUID"| UUID["reConnectWithUUID(uuid)<br/>reconnectUUID = uuid"]
Decide -->|"MAC"| Mac["reConnectWithMac(mac)<br/>reconnectMac = mac"]
UUID --> Scan["startScan() + startTimeout()<br/>(Timer 1s × 10 = 10s)"]
Mac --> Scan
Scan --> Did["didDiscover 回调"]
Did --> Check{"reconnectUUID 非空?"}
Check -->|"是"| M1{"identifier.uuidString == reconnectUUID?"}
M1 -->|"匹配"| Conn["stopScan() + connect(peripheral)<br/>reconnectUUID = nil"]
M1 -->|"否"| Continue["继续扫描"]
Check -->|"否"| Check2{"reconnectMac 非空?"}
Check2 -->|"是"| M2{"从 kCBAdvDataManufacturerData<br/>解析 MAC 并比对?"}
M2 -->|"匹配"| Conn
M2 -->|"否/无广播数据"| Continue
Check2 -->|"否"| Continue
Continue --> Did
Scan --> Timeout{"10 秒超时?"}
Timeout -->|"是"| TOut["timer 停止<br/>日志: 连接超时"]
Timeout -->|"否"| Scan
Usage Examples
示例 1:自定义模式一键扫描并展示设备列表
从 Demo 主流程摘录——App 页面启动后调用 BleManager.shared.startScan(),随后通过 RxSwift 订阅设备列表:
// 1) 扫描并连接设备
BleManager.shared.startScan()
// 选择设备后:BleManager.shared.connect(peripheral: sel)
Source: OTA 升级开发示例.md
示例 2:SDK 模式扫描、发现去重与连接
func startScan() {
bleMutipleManager.scanStart()
}
// 发现设备回调处理
@objc private func discoverEntity(_ _: Notification) {
let entities = bleMutipleManager.blePeripheralArr as! [JL_EntityM]
var newEntitys: [JL_EntityM] = []
// 过滤去重
for entity in entities {
if entity.mItem.count > 0 {
newEntitys.removeAll(where: { $0.mUUID == entity.mUUID })
newEntitys.append(entity)
}
}
discoverPeripheralsSubject.accept(newEntitys)
}
Source: OTA升级开发示例(SDK蓝牙连接).md
示例 3:扫描回调中按广播包 MAC 精确找回升级模式设备
if reconnectMac != nil {
guard let advData = advertisementData["kCBAdvDataManufacturerData"] as? Data else { return }
guard let mac = reconnectMac else { return }
if JLAdvParse.otaBleMacAddress(mac, isEqualToCBAdvDataManufacturerData: advData) {
stopScan()
reconnectMac = nil
connect(peripheral: peripheral)
return
}
}
Source: OTA 升级开发示例.md
示例 4:JL_Assist 模式下相同的广播比对逻辑
JL_Assist 自定义连接 Demo 与 MiniSingleDemo 的 didDiscover 实现一致,均通过 JLAdvParse 对 kCBAdvDataManufacturerData 做 MAC 比对后 stopScan() 并连接:
if reconnectMac != nil {
guard let advData = advertisementData["kCBAdvDataManufacturerData"] as? Data else { return }
guard let mac = reconnectMac else { return }
if JLAdvParse.otaBleMacAddress(mac, isEqualToCBAdvDataManufacturerData: advData) {
stopScan()
connect(peripheral: peripheral)
}
}
Source: OTA 升级开发示例(JL_Assist 自定义蓝牙连接).md
Configuration Options
SDK 模式(JL_BleMultiple / bleMutipleManager)
| 选项 | 类型 | 默认值(Demo) | 说明 |
|---|---|---|---|
jl_BLE_SERVICE | String | "AE00" | 杰理私有服务 UUID,SDK 扫描识别设备的关键过滤条件 |
jl_BLE_RCSP_W | String | "AE01" | 写入特征 UUID(RCSP Write) |
jl_BLE_RCSP_R | String | "AE02" | 通知特征 UUID(RCSP Read/Notify) |
ble_FILTER_ENABLE | Bool | false | 是否启用 SDK 内置设备过滤(结合名称前缀 filterKey),Demo 置 false 由 App 侧过滤 |
ble_TIMEOUT | Int | 10 | 连接/扫描超时秒数 |
自定义模式(BleManager)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
maxCount | Int | 10 | 回连超时计时器最大累计次数(配合 1 秒 Timer 即 10 秒) |
SERVICE_UUID | String | "AE00" | 服务发现时用于匹配杰理服务 UUID |
CHARACTERISTIC_WRITE | String | "AE01" | 写入特征 UUID |
CHARACTERISTIC_NOTIFY | String | "AE02" | 通知特征 UUID |
discoverPeripheralsSubject | RxSwift Subject | — | 设备列表发布通道,UI 订阅刷新 |
扫描行为参数(系统级)
| 参数 | 值 | 说明 |
|---|---|---|
scanForPeripherals(withServices:options:) | nil, nil | 全量扫描所有广播设备;若已知服务 UUID 可传入以提升扫描效率 |
kCBAdvDataManufacturerData | Data | 广播包厂商数据,内含设备真实 MAC |
kCBAdvDataLocalName | String | 广播本地名称,用于列表显示与名称过滤 |
API Reference
自定义模式:BleManager
startScan()
启动 BLE 扫描。内部调用 centralManager.scanForPeripherals(withServices: nil, options: nil),全量发现周边广播设备。扫描结果通过 didDiscover 回调去重后经 discoverPeripheralsSubject 发布。
- 参数:无
- 返回:无
stopScan()
停止扫描。调用 centralManager.stopScan()。连接成功后必须调用,以节省系统资源并避免扫描回调干扰连接事件。
connect(peripheral: CBPeripheral)
发起连接。内部调用 centralManager.connect(peripheral, options: nil)。
- 参数:
peripheral(CBPeripheral)— 待连接的目标外设(来自扫描列表或回连匹配) - 返回:无
disconnect(peripheral: CBPeripheral)
断开连接。内部调用 centralManager.cancelPeripheralConnection(peripheral)。
reConnectWithUUID(uuid: String)
按 UUID 回连。设置 reconnectUUID 并清空 reconnectMac,随后 startScan() + startTimeout()。didDiscover 中若 peripheral.identifier.uuidString == reconnectUUID 则自动停止扫描并连接。
- 参数:
uuid(String)— 目标设备identifier.uuidString(如 OTA 失败回调JL_OTAResultReconnectWithUUID提供) - 返回:无
reConnectWithMac(mac: String)
按 MAC 地址回连。设置 reconnectMac 并清空 reconnectUUID,随后 startScan() + startTimeout()。didDiscover 中解析广播包 kCBAdvDataManufacturerData 并与 mac 比对,命中即停止扫描并连接。
- 参数:
mac(String)— 目标设备 MAC 地址(如 OTA 失败回调JL_OTAResultReconnectWithMacAddr提供) - 返回:无
startTimeout() / stopTimeout() / timeoutHandler()
回连超时控制。startTimeout 创建 1 秒周期的 Timer,maxCount = 10;timeoutHandler 累计计数,达到上限后停止计时并输出 "连接超时" 错误日志。stopTimeout 在连接成功(didConnect)时调用。
write(data: Data)
按设备 MTU 分片写数据(OTA 数据下行通道)。内部按 peripheral.maximumWriteValueLength(for: .withoutResponse) 分片,逐片 writeValue(_:for:type: .withoutResponse)。
- 参数:
data(Data)— 待发送的 OTA 数据包 - 返回:无
SDK 模式:JL_BleMultiple(bleMutipleManager)
scanStart()
启动 SDK 内部扫描。扫描结果通过 kJL_BLE_M_FOUND 通知发布,App 从 blePeripheralArr 读取 JL_EntityM 列表。
- 参数:无
- 返回:无
blePeripheralArr
扫描到的设备实体数组([JL_EntityM])。每次 kJL_BLE_M_FOUND 通知触发时读取并做 mItem.count > 0 过滤与 mUUID 去重。
connectEntity(_ entity: JL_EntityM, callback: (JLConnectStatus) -> Void)
连接指定设备实体。回调状态 status == .paired 表示连接完成,可转入初始化流程(subConnectInitSubject)。
- 参数:
entity(JL_EntityM);callback(闭包,JLConnectStatus) - 返回:无
disconnectEntity(_ entity: JL_EntityM, callback: (JLConnectStatus) -> Void)
断开指定设备实体,status == .paired 时通过 disconnectSubject 通知上层。
makeEntity(withUUID uuid: String) -> JL_EntityM?
按 UUID 构造设备实体,用于 UUID 回连。构造失败(设备不存在)返回 nil,Demo 中记录 reConnectWithUUID: \(uuid) error。
- 参数:
uuid(String) - 返回:
JL_EntityM?
connectEntity(forMac mac: String, callback: (JLConnectStatus) -> Void)
按 MAC 地址直连设备,SDK 内部完成广播包 MAC 比对(对应自定义模式中 JLAdvParse.otaBleMacAddress 的职责)。
- 参数:
mac(String);callback(闭包) - 返回:无
广播解析:JLAdvParse
otaBleMacAddress(_ mac: String, isEqualToCBAdvDataManufacturerData advData: Data) -> Bool
从杰理设备广播包的 kCBAdvDataManufacturerData 中提取真实 MAC 并与 mac 比较。
- 参数:
mac(String)— 期望的目标 MAC;advData(Data)— 广播包厂商数据 - 返回:
Bool— MAC 匹配返回true - 典型场景:OTA 回连
didDiscover中的目标识别;advData缺失时由调用方guard提前退出
通知(SDK 模式)
| 通知名 | 触发时机 | 说明 |
|---|---|---|
kJL_BLE_M_FOUND | 扫描到设备时 | 从 blePeripheralArr 读取最新实体列表 |
kJL_BLE_M_FOUND_SINGLE | 单设备发现 | 单设备场景的发现通知 |
kJL_BLE_M_ENTITY_CONNECTED | 设备连接成功 | 触发 currentEntityUpdate(_:) 更新当前设备 |
kJL_BLE_M_ENTITY_DISCONNECTED | 设备断开 | 清理连接状态 |
kJL_BLE_M_ON / kJL_BLE_M_OFF | 系统蓝牙开关变化 | 蓝牙关闭时停止扫描并提示用户 |
Failure Modes, Edge Cases & Concurrency
扫描无结果 / 回连超时
- 现象:
Timer累计 10 秒后timeoutHandler输出"连接超时"错误日志。 - 根因:目标设备未在广播(进入休眠、离得太远)、设备广播名称被 iOS 随机化过滤、或广播包中无
kCBAdvDataManufacturerData。 - 处理建议:回连失败时按
JL_OTAResultFailCmdTimeout等 OTA 回调执行重试策略(Demo 中retryCount < 3时递增延迟重发cmdTargetFeature);确认设备处于可发现模式。
广播包字段缺失(nil 解包风险)
advertisementData["kCBAdvDataManufacturerData"] as? Data 使用 guard let 守护式解包——非杰理设备、非 OTA 广播包的设备会直接 return,不会崩溃,也不会误连。这是广播解析必须遵守的容错约定。
设备重复上报与并发更新
系统按广播周期重复触发 didDiscover,回调运行在 CBCentralManager 委托队列。两种模式均采用「先删后加」去重(removeAll(where:) + append),保证列表无重复条目;但若 UI 在主线程渲染而回调在其他队列,需注意线程切换(Demo 通过 RxSwift Subject 默认线程调度 + DispatchQueue.main.asyncAfter 处理通知时序)。discoverEntity 每次通知都重建 newEntitys 数组再发布,避免多个通知交错修改同一可变数组。
扫描与连接并发冲突
didConnect中显式stopScan()+stopTimeout():防止连接过程中扫描回调再次命中同一设备触发重复connect。- 回连匹配命中即
stopScan()并把reconnectUUID/reconnectMac置nil:防止同一个广播包被两条匹配分支重复处理。 - SDK 模式下
ble_TIMEOUT = 10提供连接级兜底;连接回调status == .paired才推进流程,避免状态机提前进入初始化。
蓝牙关闭 / 权限未授权
系统蓝牙关闭时扫描无任何回调;SDK 模式通过 kJL_BLE_M_OFF 通知告知 App。iOS 14+ 需在 Info.plist 配置 NSBluetoothAlwaysUsageDescription(后台扫描需 UIBackgroundModes 含 bluetooth-central),否则 CBCentralManager 无法初始化、didDiscover 永不触发。
升级模式设备识别边界
进入 Bootloader 的设备可能不广播名称(peripheral.name == nil),因此不会出现在设备列表中,但 MAC 回连分支位于名称过滤之后仍会执行——这是设计上有意为之:列表只展示可交互设备,回连匹配不受列表限制。
Performance & Operational Considerations
- 全量扫描的成本:
scanForPeripherals(withServices: nil)会持续接收所有周边设备广播,CPU 与电量开销高于按服务 UUID 过滤的扫描。Demo 选择全量扫描换取兼容性(发现不广播AE00服务 UUID 的老设备);生产环境若已知目标服务 UUID,建议传入[CBUUID(string: "AE00")]以显著降低功耗。 - 及时停止扫描:连接成功后立即
stopScan(),避免后台持续扫描导致的系统资源占用与 App 被系统挂起(bluetooth-central后台模式资源有限)。 - 去重列表的规模控制:
didDiscover每次回调都重建并发布整个数组,设备数量大时(商场等拥挤环境)RxSwift 发布频率较高;可结合 RSSI 过滤或节流(throttle)优化 UI 刷新频率。 - 超时兜底:回连路径的 10 秒
Timer保证「扫不到就不死等」;OTA 命令层另有JL_OTAResultFailCmdTimeout重试机制(Demo 中重试 3 次、延迟递增)。
Extension Points
JLAdvParse:广播包解析的公开入口,otaBleMacAddress(_:isEqualToCBAdvDataManufacturerData:)之外可按杰理协议扩展其他广播字段解析(电量、左右耳状态、场景信息等,SDK 文档中ble_ad相关字段即此类扩展)。ble_FILTER_ENABLE+filterKey:SDK 模式支持按名称前缀启用内置过滤;Demo 置false由 App 侧在discoverEntity中自定义过滤规则(如mItem.count > 0、mUUID去重)。- RxSwift Subject 通道:
discoverPeripheralsSubject、subNotifySubject、subConnectInitSubject、reconnectSubject等构成可插拔的事件总线,UI 层可自由组合订阅实现不同的交互流程。 discoverServices(nil)之后的 GATT 匹配:扫描发现的自然延伸——didDiscoverServices中按SERVICE_UUID == "AE00"过滤并discoverCharacteristics,再按CHARACTERISTIC_WRITE/CHARACTERISTIC_NOTIFY建立读写通道(详见「连接与 GATT」目录页)。- 双模式共存:自定义模式(
BleManager)与 SDK 模式(bleMutipleManager)可分别承载不同业务(如 App 自管连接 + SDK 管理 OTA 回连),两者通过JL_OTAManager.bleAddr/lastUUID等状态桥接。
Tests
仓库未提供独立单元测试工程,验证依赖三个可运行的 Demo 工程(源码摘录于本页所有代码示例):
code/MiniDemo/MiniSingleDemo/— 自定义蓝牙模式完整流程(扫描 → 连接 → 认证 → OTA),含 Mermaid 时序图(App->>BleManager: startScan()→discoverPeripheralsSubject设备列表)。code/MiniDemo/JLAssistOTADemo/—JL_Assist自定义蓝牙连接变体,didDiscover广播比对逻辑与 MiniSingleDemo 一致。code/MiniDemo/JLBleKitOTADemo/— SDK 蓝牙连接模式,覆盖scanStart()、kJL_BLE_M_FOUND去重过滤、makeEntity(withUUID:)/connectEntity(forMac:)回连。- 官方 FAQ 文档(
doc/Release_V2.x各版本jl_ota_sdk_qa)提供广播包使用、回连失败等问题的排障指南。
Related Links
- 蓝牙连接与 GATT 服务发现 — 扫描发现后的服务/特征发现、数据收发通道建立
- BLE 握手认证(Pair) —
inputPairData、isAuthed认证流程(扫描发现后初始化阶段触发) - OTA 升级流程 — 升级中断后的 UUID/MAC 回连触发点(
JL_OTAResultReconnectWithUUID/JL_OTAResultReconnectWithMacAddr) - OTA 升级开发示例(自定义蓝牙) — 本页自定义模式源码出处
- OTA 升级开发示例(SDK 蓝牙连接) — 本页 SDK 模式源码出处
- OTA 升级开发示例(JL_Assist 自定义蓝牙连接) — JL_Assist 变体源码出处