杰理 SDK 文档中心
首页
首页
  • SDK 框架库

    • JL_BLEKit 蓝牙通信核心
    • JL_AdvParse 广播包解析
    • JL_HashPair 加密配对
    • JL_OTALib 固件升级
    • JLDialUnit 彩屏仓与表盘控制
    • JLBmpConvertKit 位图转换
    • JLPackageResKit 资源包处理
    • JLLogHelper 日志工具
  • 核心功能模块

    • 音乐与媒体控制
    • 音效调节与均衡器
    • 设备发现、连接与设置
    • Auracast 广播接收与发射
    • 文件浏览、闹钟、FM 与灯光控制
    • ANC、按键设置与查找设备
    • AI 翻译与自定义命令
  • 应用架构与工程支撑

    • 杰理之家 App 架构与导航
    • 数据存储与缓存
    • Swift 工具与扩展层
    • JLAudioUnitKit 示例工程
    • SDKTestHelper 测试工具
  • 开发文档与资源

    • 文档中心与 JL_OTALib API 说明
    • 自定义蓝牙接入方式
    • 调试技巧与问题排查
    • 版本历史与社区支持

设备发现、连接与设置

本文档系统阐述杰理 iOS 蓝牙 Demo(JieLi_Home_Demo)中设备发现(BLE 扫描)、设备连接(含 GATT over EDR、失败重试)以及连接后的设置与状态管理机制。核心实现位于 JL_RunSDK 单例、SearchView 搜索视图与 JL_BLEMultiple 底层 SDK 的组合,覆盖从"打开搜索"到"设备配对成功并写入历史记录"的完整端到端链路。

Purpose and Scope

本页聚焦三类核心能力及其背后的实现机制:

  • 设备发现:JL_BLEMultiple 的扫描配置(过滤、认证、超时、设备类型)、kJL_BLE_M_FOUND 发现通知、SearchView 的列表过滤与刷新。
  • 设备连接:connectEntity:Result: 的连接回调状态机、失败自动重连、GATT over EDR(ATT)设备的经典蓝牙前置连接校验、连接成功后的历史入库。
  • 连接后设置与状态管理:kUI_JL_BLE_SCAN_OPEN/CLOSE 广播包开关、kUI_JL_DEVICE_CHANGE 设备切换通知、JLPreparation 连接后准备任务链、setActiveUUID: 手动切换设备。

以下主题属于兄弟页面,不在本文展开:设备信息展示与功能设置面板(见 DeviceInfoVC 相关文档)、OTA 升级流程、EQ 音效设置、音乐播放控制等。本文仅在它们与连接状态联动(例如进入设备详情时关闭广播包)时作简要引用。

Overview

在杰理蓝牙体系中,App 与设备之间的链路分为两层:

  1. BLE(低功耗蓝牙)链路:负责发现、配对与指令透传。JL_BLEMultiple 封装了 CoreBluetooth 的扫描、连接与断连逻辑,并对外暴露 JL_EntityM(设备实体)与 JL_EntityM_Status(连接状态)。
  2. 经典蓝牙(EDR)链路:部分设备(如挂脖耳机、TWS 的某些型号)走 GATT over EDR,即指令通过经典蓝牙连接上的 GATT 服务传输。这类设备要求系统经典蓝牙先完成配对,App 才能建立 GATT 会话。

JL_RunSDK 是本 Demo 的全局运行时门面(单例),负责:

  • 初始化并配置 JL_BLEMultiple(过滤、认证、超时、设备类型白名单);
  • 通过 JL_Tools 通知总线收发 kUI_JL_BLE_SCAN_OPEN / kUI_JL_BLE_SCAN_CLOSE 等全局事件,协调各界面之间的扫描开关;
  • 维护当前激活设备 mBleEntityM、已连接 UUID 历史(linkedUuidArr)与本地 SQLite 设备历史(localSqlArr);
  • 通过 KVO 监听已连接的 ATT 设备数组 bleAttDevices。

整个"发现 → 连接 → 设置"生命周期围绕一组全局通知常量展开,界面层只负责发通知与监听通知,真正的蓝牙动作集中在 JL_RunSDK 与 JL_BLEMultiple 中——这是典型的通知驱动的解耦架构:搜索界面弹出时通知"打开广播",连接成功后通知"关闭广播",任何界面都可以在不直接持有蓝牙对象的情况下参与连接流程。

Architecture

下图展示了设备发现、连接与设置相关的组件分层与依赖关系(节点名均取自真实类名):

flowchart TD
    subgraph sg_UI["UI 层"]
        SearchView["SearchView 搜索视图"]
        DeviceVC["DeviceVC 设备列表"]
        DeviceInfoVC["DeviceInfoVC 设备详情"]
    end

    subgraph sg_Notify["通知总线 (JL_Tools post/add)"]
        ScanOpen["kUI_JL_BLE_SCAN_OPEN"]
        ScanClose["kUI_JL_BLE_SCAN_CLOSE"]
        DevChange["kUI_JL_DEVICE_CHANGE"]
    end

    subgraph sg_Core["核心运行时"]
        RunSDK["JL_RunSDK 单例"]
        BleMultiple["JL_BLEMultiple"]
        EntityM["JL_EntityM 设备实体"]
        ManagerM["JL_ManagerM 指令管理器"]
    end

    subgraph sg_Aux["持久化与辅助"]
        Sqlite["SqliteManager"]
        Cache["JLUI_Cache"]
        Elastic["ElasticHandler"]
        Preparation["JLPreparation"]
    end

    SearchView -->|"post/add"| ScanOpen
    SearchView -->|"post"| ScanClose
    DeviceVC -->|"post 重启广播"| ScanOpen
    DeviceInfoVC -->|"post 暂停回连"| ScanClose
    RunSDK -->|"addNote 注册"| ScanOpen
    RunSDK -->|"addNote 注册"| ScanClose
    RunSDK -->|"持有并配置"| BleMultiple
    BleMultiple -->|"扫描产出"| EntityM
    EntityM -->|"mCmdManager"| ManagerM
    RunSDK -->|"post"| DevChange
    SearchView -->|"connectEntity"| BleMultiple
    SearchView -->|"installWithDevice"| Sqlite
    RunSDK -->|"checkOutDevices 读取历史"| Sqlite
    SearchView -->|"setToBlackList"| Elastic
    ManagerM -->|"任务链 JLTaskChain"| Preparation
    RunSDK -->|"setRenameUUID 清除回连"| Cache

架构解读:

  • UI 层是连接流程的发起者:SearchView 负责搜索与点击连接;DeviceVC 负责列表展示与断开后重连(重启广播包);DeviceInfoVC 在进入详情或暂停回连时关闭广播包。
  • 通知总线是三层之间的"软总线"。界面用 [JL_Tools post:...] 广播意图,JL_RunSDK 在 addNote 中注册对应 selector 响应,从而在不产生强耦合的前提下统一切换扫描状态。
  • 核心运行时中,JL_RunSDK 是唯一持有 JL_BLEMultiple 的地方;JL_BLEMultiple 产出 JL_EntityM,连接成功后通过 entity.mCmdManager 拿到 JL_ManagerM 用于下发指令与读取设备模型。
  • 持久化与辅助:SqliteManager 维护连接历史(搜索界面可直接展示历史设备);ElasticHandler 维护黑名单(用于过滤"已拒绝过"或需要特殊处理的设备);JLPreparation 在连接建立后按任务链完成设备初始化(如获取设备信息、恢复设置)。

关键通知常量

JL_RunSDK.m 在文件头部集中声明了本主题相关的全局通知名,全部以 UI_JL_ 前缀命名,通过 JL_Tools 的 post/add/remove 机制分发:

常量字符串值语义
kUI_JL_BLE_SCAN_OPENUI_JL_BLE_SCAN_OPEN打开 BLE 广播/扫描(搜索、重连时发出)
kUI_JL_BLE_SCAN_CLOSEUI_JL_BLE_SCAN_CLOSE关闭 BLE 广播/扫描(连接中、进详情时发出)
kUI_JL_DEVICE_CHANGEUI_JL_DEVICE_CHANGE当前激活设备发生切换,携带 JLDeviceChangeType 枚举值
kUI_JL_DEVICE_PREPARINGUI_JL_DEVICE_PREPARING设备连接后进入准备阶段
kUI_JL_DEVICE_SHOW_OTAUI_JL_DEVICE_SHOW_OTA触发 OTA 入口展示
kUI_TURN_TO_DEVICEVCUI_TURN_TO_DEVICEVC跳转到设备列表页
NSString *kUI_JL_DEVICE_CHANGE          = @"UI_JL_DEVICE_CHANGE";
NSString *kUI_JL_DEVICE_PREPARING       = @"UI_JL_DEVICE_PREPARING";

NSString *kUI_JL_BLE_SCAN_OPEN          = @"UI_JL_BLE_SCAN_OPEN";
NSString *kUI_JL_BLE_SCAN_CLOSE         = @"UI_JL_BLE_SCAN_CLOSE";

Source: JL_RunSDK.m

JL_RunSDK 在 init 末尾调用 [self addNote] 注册 kUI_JL_BLE_SCAN_OPEN 与 kUI_JL_BLE_SCAN_CLOSE 的监听,成为广播包开关的唯一执行者:

-(void)addNote{
    [JL_Tools add:kUI_JL_BLE_SCAN_OPEN Action:@selector(noteBleScanOpen:) Own:self];
    [JL_Tools add:kUI_JL_BLE_SCAN_CLOSE Action:@selector(noteBleScanClose:) Own:self];

Source: JL_RunSDK.m

核心机制详解:JL_RunSDK 单例与扫描配置

单例与初始化

JL_RunSDK 使用 dispatch_once 实现进程级单例,所有界面通过 [JL_RunSDK sharedMe] 获取同一实例,从而保证全局只有一份 JL_BLEMultiple 与一套连接状态:

+(instancetype)sharedMe{
    static JL_RunSDK *SDK = nil;
    static dispatch_once_t predicate;
    dispatch_once(&predicate, ^{
        SDK = [[self alloc] init];
    });
    return SDK;
}

Source: JL_RunSDK.m

初始化时,JL_RunSDK 完成五件关键工作:

  1. 创建 preparationArr(准备队列)与 linkedUuidArr(已连接 UUID 历史);
  2. 初始化并配置 JL_BLEMultiple(过滤、认证、超时、设备类型);
  3. 立即执行 noteBleScanOpen: 开启首轮扫描(App 启动即进入可发现状态);
  4. 创建 JLPublicSetting 公共设置管理器与 JLDialInfoExtentedModel 表盘扩展模型;
  5. 注册通知监听(addNote)、刷新本地历史设备(refreshLocalDevices)、KVO 监听 bleAttDevices。

扫描参数配置

设备发现的"口径"在 init 中一次性确定:是否过滤、是否认证、超时时长、允许发现的设备类型。这些参数直接决定 blePeripheralArr 中会产出哪些 JL_EntityM:

/*--- 初始化JL_SDK ---*/
self.mBleMultiple = [[JL_BLEMultiple alloc] init];
//是否开启过滤
self.mBleMultiple.BLE_FILTER_ENABLE = YES;
//是否开启设备认证
self.mBleMultiple.BLE_PAIR_ENABLE = YES;
self.mBleMultiple.BLE_TIMEOUT = 7;

/*--- 选择设备类型搜索 ---*/
self.mBleMultiple.bleDeviceTypeArr = @[@(JL_DeviceTypeTradition),
                                       @(JL_DeviceTypeSoundBox),
                                       @(JL_DeviceTypeTWS),
                                       @(JL_DeviceTypeChargingBin),
                                       @(JL_DeviceTypeHeadset),
                                       @(JL_DeviceTypeSoundCard),
];

Source: JL_RunSDK.m

设计意图:

  • BLE_FILTER_ENABLE = YES 让 SDK 只上报符合杰理协议特征的外设,避免把周围无关 BLE 设备刷进列表;
  • BLE_PAIR_ENABLE = YES 开启设备认证(配对握手),未通过认证的实体在 UI 上表现为"未认证"图标(icon_nor);
  • BLE_TIMEOUT = 7 把单次连接超时控制在 7 秒,配合搜索视图的自动重连逻辑形成"快速失败、快速重试"的用户体验;
  • bleDeviceTypeArr 相当于设备类型白名单:传统设备、音箱、TWS、充电仓、耳机、声卡都在可发现范围内。

设备实体与指令管理器

扫描产出的每个 JL_EntityM 携带 mItem(名称)、mUUID、mEdr(经典蓝牙地址)、mUID/mPID(厂商/产品号)、mType(设备类型)、mConnectWay(连接方式,如 JLEntityConnectTypeATT)、mIsAuth(是否认证)等属性;连接成功后,entity.mCmdManager 即 JL_ManagerM,是所有指令(音乐、EQ、闹钟、充电仓等)的入口。其它业务模块统一通过以下模式获取当前设备模型:

JL_ManagerM *manager = [[JL_RunSDK sharedMe] mBleEntityM].mCmdManager;
JLModel_Device *model = [manager outputDeviceModel];

Source: JL_RunSDK.m

设备切换:setActiveUUID

当用户从历史列表或搜索结果中手动切换到某台设备时,JL_RunSDK 会先让旧设备的充电仓管理器停止推送,把旧 UUID 记入 linkedUuidArr,再切换 mBleUUID,最后广播 kUI_JL_DEVICE_CHANGE 通知全局刷新:

+(void)setActiveUUID:(NSString*)uuid{
    if ([self sharedMe].mBleEntityM.mCmdManager) {
        [[self sharedMe].mBleEntityM.mCmdManager.mChargingBinManager cmdID3_PushEnable:YES];
    }
    if ([self sharedMe].mBleUUID) [[self sharedMe] addLinkedArrayUuid:[self sharedMe].mBleUUID];

    [[self sharedMe] changeUUID:uuid];
    [JL_Tools post:kUI_JL_DEVICE_CHANGE Object:@(JLDeviceChangeTypeManualChange)];
}

Source: JL_RunSDK.m

这段代码体现了"多设备管理"的设计约束:杰理体系允许连接多台设备(含 ATT 设备),但同一时刻只有一个"激活设备"(mBleUUID)。切换时必须先回收旧设备的资源(充电仓推送),再记录历史,最后通过通知让所有 UI 同步——状态变更永远走通知总线,而不是让各界面直接读写 mBleUUID。

设备搜索与连接流程(SearchView)

SearchView 是搜索/连接的用户入口:弹出半屏面板、展示可连接设备列表、处理点击连接与失败重试。它不直接调用 CoreBluetooth,而是通过 JL_RunSDK/JL_BLEMultiple 与通知总线协作。

开启搜索

startSearch 把搜索视图标记为活跃状态,立即从 blePeripheralArr 过滤出候选设备并注册发现通知:

-(void)startSearch{
    [[JLUI_Cache sharedInstance] setIsSearchView:YES];

    isConnectOK = YES;

    bleSDK = [JL_RunSDK sharedMe];
    self.foundArray = [self filterDevices];

    [JL_Tools add:kJL_BLE_M_FOUND Action:@selector(noteBleFoundDevice:) Own:self];
    ...
}

-(NSMutableArray *)filterDevices{
    NSMutableArray *tmpArray = [NSMutableArray new];
    for (JL_EntityM * entity in bleSDK.mBleMultiple.blePeripheralArr) {
        if (entity.mIsSupportLeAudio && entity.mLeAudioConnected) {
            continue;
        }
        [tmpArray addObject:entity];
    }
    return tmpArray;
}

Sources: SearchView.m(startSearch)、SearchView.m(filterDevices)

filterDevices 会把已连接且支持 LE Audio 的设备从候选列表中剔除——避免用户重复连接一个已经建立 LE Audio 会话的设备,这是对多连接场景(BLE + LE Audio 并存)的必要过滤。

noteBleFoundDevice: 收到 kJL_BLE_M_FOUND 后重新过滤并局部刷新表格(reloadSections),保证列表随扫描结果实时增长而不闪烁。

点击连接前的三重校验

tableView:didSelectRowAtIndexPath: 在真正发起连接前执行三层保护:

  1. 扫描状态切换:立即 post kUI_JL_BLE_SCAN_CLOSE,暂停广播包回连,避免连接过程中被其它逻辑打断;
  2. GATT over EDR 校验:若设备 mConnectWay == JLEntityConnectTypeATT,则检查其 mEdr 地址是否已出现在 [JL_BLEMultiple outputEdrList](系统已配对的经典蓝牙列表)中;不在列表中则弹窗提示"该设备为 GATT over EDR 设备,请先在系统蓝牙中连接",并引导 [R openBluetoothSettings] 跳转系统设置;
  3. EDR 已连接校验:调用 [JL_RunSDK isConnectedEdr:bleEntity] 兜底,未连则提示 user_connect_edr 文案。
if (bleEntity.mConnectWay == JLEntityConnectTypeATT) {
    NSString *edrAddr = bleEntity.mEdr;
    NSArray *edrList = [JL_BLEMultiple outputEdrList];
    for (NSString *edr in edrList) {
        if ([edr.uppercaseString isEqualToString:edrAddr.uppercaseString]) {
            [self connectWithEntity:bleEntity];
            return;
            break;
        }
    }
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:[LanguageCls localizableTxt:@"tips_1"]
        message:[LanguageCls localizableTxt:@"This device is a GATT over EDR connection device; you need to connect the device via classic Bluetooth in the background first."]
        preferredStyle:UIAlertControllerStyleAlert];
    ...
    [R openBluetoothSettings];
    return;
}

Source: SearchView.m

设计意图:ATT 设备无法由 App 直接发起经典蓝牙配对(iOS 系统限制),必须先由用户在系统设置中完成 EDR 配对,App 才能在其上建立 GATT 会话。这里的地址比对(不区分大小写)就是为了把"系统已配对"与"尚未配对"两种状态区分开,给出明确指引而不是让用户面对一个永远连不上的设备。

连接请求与结果回调

校验通过后,connectWithEntity: 调用 connectEntity:Result:,回调携带 JL_EntityM_Status 状态码:

-(void)connectWithEntity:(JL_EntityM*)bleEntity {
    if (isConnecting) {
        return;
    }
    ...
    isConnecting = true;
    __weak typeof(self) wSelf = self;
    [bleSDK.mBleMultiple connectEntity:bleEntity
                                Result:^(JL_EntityM_Status status) {
        __strong typeof(wSelf) sSelf = wSelf;
        sSelf->isConnecting = false;
        sSelf->isConnectOK = YES;
        UIWindow *win = [DFUITools getWindow];
        NSString *txt = [JL_RunSDK textEntityStatus:status];

        //加入黑名单
        [[ElasticHandler sharedInstance] setToBlackList:bleEntity];

        if (status == JL_EntityM_StatusPaired) {
            //插入连接历史记录数据库
            [[SqliteManager sharedInstance] installWithDevice:bleEntity];
            [JL_Tools mainTask:^{
                [DFUITools showText:txt onView:win delay:1.0];
                [JL_Tools delay:0.5 Task:^{
                    [wSelf dismissAction];
                    [self->deviceTable reloadData];
                }];
            }];
            [self->bleSDK.mBleMultiple scanStop];
            self->bleUUID = nil;
        } else {
            if(status == JL_EntityM_StatusConnectTimeout ||
               status == JL_EntityM_StatusConnectFail) {
                [self->bleSDK.mBleMultiple scanContinue];
                if (sSelf->currentConnectIndex < sSelf->maxReconnected) {
                    sSelf->currentConnectIndex += 1;
                    [sSelf connectWithEntity:bleEntity];
                }
            }else{
                [JL_Tools mainTask:^{
                    [DFUITools showText:txt onView:win delay:1.0];
                    [JL_Tools delay:0.5 Task:^{
                        [wSelf dismissAction];
                    }];
                }];
            }
        }
    }];
}

Source: SearchView.m

要点分析:

  • 并发保护:isConnecting 标志保证同一时刻只有一个连接请求在途,防止快速点击造成重复连接;
  • 状态枚举驱动:JL_EntityM_StatusPaired 表示配对成功——此时写入 SQLite 历史(installWithDevice:)、停止扫描(scanStop)、清空当前选中 UUID、延时收起搜索面板;
  • 失败自愈:ConnectTimeout/ConnectFail 时先 scanContinue 恢复扫描(因为发起连接前关闭了广播),再按 maxReconnected 上限自动重试,每次递增 currentConnectIndex;
  • 黑名单联动:无论成功失败都把实体交给 ElasticHandler 加入黑名单,供弹性/回连策略参考;
  • 主线程收敛:所有 UI 更新(提示文本、收起动画)都包在 JL_Tools mainTask: 中,回调线程不直接碰 UI。

完整时序

sequenceDiagram
    participant U as 用户
    participant SV as SearchView
    participant RS as JL_RunSDK
    participant BM as JL_BLEMultiple
    participant DB as SqliteManager

    U->>SV: 打开搜索面板
    SV->>RS: sharedMe 获取单例
    SV->>SV: filterDevices(blePeripheralArr)
    SV->>BM: 注册 kJL_BLE_M_FOUND 监听
    BM-->>SV: noteBleFoundDevice (新设备)
    SV->>SV: reloadSections 刷新列表
    U->>SV: 点击设备行
    SV->>SV: post kUI_JL_BLE_SCAN_CLOSE
    SV->>SV: ATT 设备? → EDR 已配对校验
    SV->>BM: connectEntity:Result:
    BM-->>SV: JL_EntityM_StatusPaired
    SV->>DB: installWithDevice 写入历史
    SV->>BM: scanStop 停止扫描
    SV->>SV: dismissAction 收起面板
    BM->>RS: 更新 mBleEntityM / KVO bleAttDevices
flowchart TD
    Start([开始搜索]) --> Found["blePeripheralArr 发现实体"]
    Found --> Tap{"用户点击设备"}
    Tap -->|"ATT 且 EDR 未配对"| Alert["弹窗提示<br/>跳转系统蓝牙 openBluetoothSettings"]
    Tap -->|"ATT 且 EDR 已配对"| Connect["connectEntity"]
    Tap -->|"普通 BLE 设备"| Connect
    Connect --> Status{"连接状态"}
    Status -->|"Paired"| Paired["写历史 + scanStop<br/>收起搜索面板"]
    Status -->|"Timeout / Fail"| Retry{"重试次数 &lt; maxReconnected?"}
    Retry -->|"是"| Connect
    Retry -->|"否"| Fail["提示失败文案"]
    Status -->|"其他状态"| Other["提示状态文案"]
    Alert --> End([结束])
    Paired --> End
    Fail --> End
    Other --> End

列表展示细节

cellForRowAtIndexPath: 根据实体属性渲染不同 UI:mIsAuth 决定选中图标(icon_sel/icon_nor);mType 决定设备图标(TWS 耳机、声卡、充电仓、音箱各有专属图片);mUUID 与当前 bleUUID 相等时显示加载动画(activeView)。这组映射说明发现列表不只是"名字列表",而是对设备能力(认证、类型、连接中状态)的可视化编码。

连接后:广播包重启与历史回连(DeviceVC)

DeviceVC(设备列表页)承担"断连后重连"与"历史设备回连"职责。断开或切换时,它通过通知重启广播包,让设备重新进入可发现/可回连状态:

[[JLUI_Cache sharedInstance] setRenameUUID:nil];//清除回连的UUID
[JL_Tools post:kUI_JL_BLE_SCAN_OPEN Object:nil];//重启广播包

Source: DeviceVC.m

设计意图:JLUI_Cache 中缓存的 renameUUID(回连 UUID)用于记忆上次连接的设备;重连前先清除它,再打开广播,确保 SDK 的自动回连逻辑从"全新扫描"开始,而不是执着于旧的设备地址。

同理,DeviceInfoVC 在进入设备详情、或需要暂停广播包回连时(如进行 OTA 等独占操作)发送 kUI_JL_BLE_SCAN_CLOSE:

/*--- 暂停广播包回连操作 ---*/
[JL_Tools post:kUI_JL_BLE_SCAN_CLOSE Object:nil];

Source: DeviceInfoVC.m

连接后准备(JLPreparation)

设备配对成功后并非立即可用:JL_RunSDK 通过 JLPreparation 对当前实体执行"连接后准备"任务链。准备阶段使用 JL_ManagerM 与 JLTaskChain 串行下发初始化指令(如读取设备信息、同步公共设置),并受 preparateTimer/preparationArr 管理,避免多设备并发准备相互干扰:

JL_ManagerM *mgr = self.mBleEntityM.mCmdManager;
JLTaskChain *chain = [JLTaskChain new];

Source: JLPreparation.m

准备期间 UI 层会收到 kUI_JL_DEVICE_PREPARING 通知,用于展示"设备准备中"的状态;准备完成后才允许进入完整的功能面板。这一"连接 → 准备 → 就绪"的阶段划分,是杰理 SDK 保证指令时序安全的关键:设备刚上电时部分能力尚未就绪,直接下发业务指令可能丢失。

配置选项

设备发现与连接相关的全部可配置项集中体现在 JL_RunSDK 初始化对 JL_BLEMultiple 的设置中:

配置项类型默认值说明
BLE_FILTER_ENABLEBOOLYES是否开启设备过滤。开启后仅上报符合杰理协议特征的外设,减少无关设备干扰
BLE_PAIR_ENABLEBOOLYES是否开启设备认证(配对握手)。开启后未认证设备在 UI 上显示为未认证状态
BLE_TIMEOUTNSTimeInterval7单次连接超时(秒)。超时后回调 JL_EntityM_StatusConnectTimeout,触发搜索视图自动重连
bleDeviceTypeArrNSArray<NSNumber*>6 种类型允许发现的设备类型白名单:传统设备、音箱、TWS、充电仓、耳机、声卡

连接行为相关的界面级参数(SearchView 内部):

配置项类型默认值说明
isConnectingBOOLNO连接互斥标志,防止并发连接
currentConnectIndexNSInteger0当前失败重试计数
maxReconnectedNSInteger由 SearchView 定义失败自动重连的最大次数上限(源码中未给出字面量,行为为"达到上限后停止重试并提示失败")
isConnectOKBOOLYES是否允许发起新连接(连接进行中被置 NO 以拦截重复点击)

注:maxReconnected 的字面量定义未在本页读取的代码片段中体现,但其判定逻辑 currentConnectIndex < maxReconnected 已在 connectWithEntity: 中确认存在。

API 参考

JL_RunSDK(全局运行时门面,单例)

+ (instancetype)sharedMe 获取全局唯一的 JL_RunSDK 实例。所有设备发现、连接、切换入口都经由此实例。

- (JL_EntityM *)mBleEntityM 返回当前激活(已连接)的设备实体。调用方通常通过 mBleEntityM.mCmdManager 获取 JL_ManagerM 再下发指令。

+ (void)setActiveUUID:(NSString *)uuid 手动切换激活设备到指定 UUID。内部会先让旧设备停止充电仓推送、记录旧 UUID 到 linkedUuidArr,再切换并广播 kUI_JL_DEVICE_CHANGE(JLDeviceChangeTypeManualChange)。

- (void)refreshLocalDevices 异步从 SqliteManager 读取本地设备历史,填充 localSqlArr,用于历史列表展示。

- (void)noteBleScanOpen:(NSNotification *)note / - (void)noteBleScanClose:(NSNotification *)note 通知总线回调(在 addNote 中注册),负责真正地开启/关闭 BLE 广播。界面层只 post 对应通知名,不直接调用。

JL_BLEMultiple(底层 SDK 封装,由 JL_RunSDK 持有)

- (void)connectEntity:(JL_EntityM *)entity Result:(JL_EntityM_Status)status 发起连接。回调在非主线程,status 可取 JL_EntityM_StatusPaired、JL_EntityM_StatusConnectTimeout、JL_EntityM_StatusConnectFail 等。调用方负责把 UI 操作切回主线程。

- (void)scanStop / - (void)scanContinue 停止扫描 / 继续扫描。连接成功后调用 scanStop;失败重试前调用 scanContinue 恢复广播。

+ (NSArray *)outputEdrList 返回系统已配对的经典蓝牙(EDR)地址列表,用于 ATT(GATT over EDR)设备的已配对校验。

SearchView(搜索视图)

- (void)startSearch 打开搜索面板:标记 isSearchView、过滤候选设备、注册 kJL_BLE_M_FOUND 监听、播放弹入动画。

- (NSMutableArray *)filterDevices 从 blePeripheralArr 过滤出候选实体;剔除"已连接且支持 LE Audio"的设备。

- (void)noteBleFoundDevice:(NSNotification *)note 收到发现通知后刷新列表(reloadSections)。

- (void)connectWithEntity:(JL_EntityM *)bleEntity 执行连接:防重入(isConnecting)→ connectEntity:Result: → 成功写历史/停止扫描/收起面板,失败按 maxReconnected 自动重试。

- (void)dismissAction 收起搜索面板:重置 isSearchView、post kUI_JL_BLE_SCAN_OPEN 恢复广播、移除 kJL_BLE_M_FOUND 监听。

JL_ManagerM(设备指令管理器)

- (JLModel_Device *)outputDeviceModel 读取当前设备的模型信息(名称、协议版本、能力位等),业务模块初始化 UI 前的标准入口。

- (void)cmdRequestDeviceImageVid:(NSString *)vid Pid:(NSString *)pid ItemArray:(NSArray *)items Result:(void(^)(NSMutableDictionary *dict))result 请求设备图片素材(产品 Logo、双耳连接状态、充电仓状态等)。SearchView 在连接前用它预取图片并缓存到 JLCacheBox,实现"点击即连、图片即显"。

Source: SearchView.m

通知与状态枚举

符号类型说明
kJL_BLE_M_FOUND通知名SDK 扫描到新设备时广播,携带设备列表
JL_EntityM_Status枚举连接状态:Paired 成功;ConnectTimeout/ConnectFail 失败;其余为中间态
JLDeviceChangeTypeManualChange枚举设备切换类型:用户手动切换
JLEntityConnectTypeATT枚举连接方式:GATT over EDR(需经典蓝牙前置配对)

失败模式、边界情况与并发

连接超时与失败重试

  • 触发条件:BLE_TIMEOUT = 7 秒内未完成配对 → JL_EntityM_StatusConnectTimeout;链路异常 → JL_EntityM_StatusConnectFail。
  • 处理:connectWithEntity: 先 scanContinue 恢复广播(连接前广播被关闭),再按 currentConnectIndex < maxReconnected 决定是否重试。重试计数单调递增,超过上限即停止并提示失败文案。
  • 设计取舍:7 秒超时 + 自动重试是对"信号弱/设备忙"场景的折中——重试太快会加剧干扰,太慢则体验差;由 isConnectOK/isConnecting 双标志保证任意时刻只有一个连接在途。

GATT over EDR(ATT 设备)无法直连

  • 症状:点击 ATT 设备后一直停留在"连接中"或直接失败。
  • 根因:iOS 不允许 App 主动发起经典蓝牙(EDR)配对,GATT over EDR 会话必须建立在系统已配对的 EDR 链路上。
  • 防护:didSelectRowAtIndexPath: 先比对 outputEdrList 与实体 mEdr(不区分大小写),未配对则弹窗并引导 openBluetoothSettings;connectWithEntity: 内再次校验,形成双保险。

重复连接与快速点击

isConnecting 在进入 connectWithEntity: 时置位、回调时复位,didSelectRowAtIndexPath: 也以 isConnectOK == NO 提前返回。两层拦截确保同一设备不会并发发起两次连接。

多设备与 LE Audio 共存

filterDevices 剔除"已连接且支持 LE Audio"的实体,避免对同一物理设备重复建链;linkedUuidArr 记录已连接 UUID 防止回连风暴;KVO 监听 bleAttDevices 让 ATT 设备变化(增删)能驱动全局状态刷新。

线程与 UI 收敛

SDK 回调不保证在主线程。所有提示文本、收起动画均通过 JL_Tools mainTask:/delay:Task: 收敛到主线程,dismissAction 的动画同样如此;SqliteManager 的读写(checkOutDevices/installWithDevice)走异步回调,避免阻塞主线程。

已知信息缺口

  • noteBleScanOpen:/noteBleScanClose: 的具体实现体(扫描启动/停止的底层细节)位于 JL_RunSDK 后续行,本次取证未覆盖;其注册关系(addNote)与触发点已确认。
  • maxReconnected 的赋值位置未在本次读取片段中定位,判定逻辑已确认存在。

性能与运维注意事项

  • 扫描功耗:App 启动即 noteBleScanOpen 开启广播,常驻扫描会持续消耗电量与系统蓝牙资源;进入 DeviceInfoVC、OTA 等独占场景应发 kUI_JL_BLE_SCAN_CLOSE 关闭,操作完成再恢复。
  • 连接时序:配对成功到 JLPreparation 准备完成之间存在"准备窗口"(受 preparateTimer/preparationArr 管理),此期间业务指令可能被丢弃——业务模块应监听 kUI_JL_DEVICE_PREPARING/kUI_JL_DEVICE_CHANGE 决定何时展示功能面板。
  • 图片预取:点击连接前通过 cmdRequestDeviceImageVid:Pid:ItemArray: 预取设备图片并缓存到 JLCacheBox(按 UUID 隔离),既减少连接后的等待,也避免重复网络/固件资源请求。
  • 历史库:SqliteManager 是设备历史的唯一持久化出口(installWithDevice 写入、checkOutDevices 读取),任何新入口(如从历史直接回连)都应复用该通道,避免旁路写入导致列表与实况不一致。

扩展点

  1. 新增设备类型:在 bleDeviceTypeArr 中追加 JL_DeviceTypeXXX,并在 SearchView 的 cellForRowAtIndexPath: 图标映射中补充对应图片分支。
  2. 自定义连接前校验:didSelectRowAtIndexPath: 的三重校验(ATT/EDR/黑名单)是天然扩展位——可在 connectWithEntity: 前插入额外的设备能力检查(如固件版本门槛)。
  3. 重连策略调优:修改 BLE_TIMEOUT 与 maxReconnected 即可改变失败重试节奏,无需改动状态机代码。
  4. 连接后初始化扩展:在 JLPreparation 的 JLTaskChain 中追加任务,即可在设备准备阶段注入自定义初始化指令。
  5. 全局状态订阅:任何模块可通过 JL_Tools add: 监听 kUI_JL_DEVICE_CHANGE(设备切换)、kUI_JL_DEVICE_PREPARING(准备中)等通知,实现与连接生命周期解耦的 UI 联动。

测试覆盖说明

仓库内与连接链路直接相关的测试未在本次取证范围内发现独立单元测试文件;现有验证主要依赖 Demo 运行路径:DbTest.m 展示了 JL_ManagerM 的独立构造与歌词指令调用(可作为指令层冒烟用例),连接状态机的行为可通过 SearchView 手动流程(搜索 → 点击 → 配对/失败重试)验证。建议在扩展连接逻辑时补充状态机与重试计数的单测。

相关链接

  • JL_RunSDK.m(运行时门面、通知常量、扫描配置)
  • SearchView.m(设备搜索与连接界面)
  • DeviceVC.m(设备列表与断连重连)
  • DeviceInfoVC.m(设备详情,进入时暂停广播包回连)
  • JLPreparation.m(连接后准备任务链)
  • DbTest.m(JL_ManagerM 指令层示例)
  • 兄弟页面:设备信息与功能设置(DeviceInfoVC)、OTA 升级流程、EQ 音效设置(EQViewController)
Prev
音效调节与均衡器
Next
Auracast 广播接收与发射