杰理 SDK 文档中心
首页
首页
  • 项目概览

    • 项目概述与功能特性
    • 工程结构与运行环境
  • 快速开始

    • SDK 集成步骤
    • 连接方式选择指南
  • 核心 SDK 架构

    • SDK 框架组成
    • JL_OTAManager 升级管理 API
    • 设备认证与广播解析
  • 蓝牙连接与设备发现

    • 设备扫描与广播发现
    • 原生 CoreBluetooth 连接
    • JL_BLEKit SDK 连接
    • JL_Assist 自定义连接
    • GATT Over BR/EDR 经典蓝牙升级
  • OTA 升级工作流

    • 标准升级流程
    • 自动化测试与批量升级
    • 广播音箱升级
    • 升级文件管理
  • 示例工程

    • 完整示例应用
    • 迷你示例工程
    • 第三方依赖与工具
  • 开发支持与版本发布

    • 文档中心与 API 说明
    • SDK 版本与构建产物
    • 调试技巧与日志辅助

设备扫描与广播发现

本文档介绍 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 提供了两种等价的接入方式,其扫描实现路径不同:

  1. 自定义蓝牙连接(JL_Assist / JL_OTALib):App 自持 CBCentralManager,通过 BleManager 单例直接调用 scanForPeripherals(withServices:options:) 扫描,在 didDiscover 回调中自行维护设备列表(RxSwift discoverPeripheralsSubject),并借助 JLAdvParse 解析广播包提取 MAC。
  2. 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_SERVICEString"AE00"杰理私有服务 UUID,SDK 扫描识别设备的关键过滤条件
jl_BLE_RCSP_WString"AE01"写入特征 UUID(RCSP Write)
jl_BLE_RCSP_RString"AE02"通知特征 UUID(RCSP Read/Notify)
ble_FILTER_ENABLEBoolfalse是否启用 SDK 内置设备过滤(结合名称前缀 filterKey),Demo 置 false 由 App 侧过滤
ble_TIMEOUTInt10连接/扫描超时秒数

自定义模式(BleManager)

选项类型默认值说明
maxCountInt10回连超时计时器最大累计次数(配合 1 秒 Timer 即 10 秒)
SERVICE_UUIDString"AE00"服务发现时用于匹配杰理服务 UUID
CHARACTERISTIC_WRITEString"AE01"写入特征 UUID
CHARACTERISTIC_NOTIFYString"AE02"通知特征 UUID
discoverPeripheralsSubjectRxSwift Subject—设备列表发布通道,UI 订阅刷新

扫描行为参数(系统级)

参数值说明
scanForPeripherals(withServices:options:)nil, nil全量扫描所有广播设备;若已知服务 UUID 可传入以提升扫描效率
kCBAdvDataManufacturerDataData广播包厂商数据,内含设备真实 MAC
kCBAdvDataLocalNameString广播本地名称,用于列表显示与名称过滤

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 变体源码出处
Next
原生 CoreBluetooth 连接