杰理 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 说明
    • 自定义蓝牙接入方式
    • 调试技巧与问题排查
    • 版本历史与社区支持

文件浏览、闹钟、FM 与灯光控制

本文档介绍杰理智能设备 iOS 应用中与设备功能控制相关的页面体系——包括文件浏览、闹钟(Alarm)、FM 收音与灯光控制四类设备功能页,重点剖析它们所依赖的通用基类 BasicViewController 的实现机制、页面生命周期与设备断开/来电中断处理逻辑。

目的与范围

本页面向的是 DevicesViewController/DeviceInfoVC 目录下的设备功能控制能力。该目录是设备详情(DeviceInfoVC)下所有功能子页面的公共容器,其中:

  • 文件浏览(File Browse):浏览设备内存储(如 TF 卡、Flash)中的音频文件,用于选择播放曲目或管理设备文件;
  • 闹钟(Alarm):读取/设置设备的闹钟配置(时间、重复周期、铃声等);
  • FM 收音(FM Radio):控制设备 FM 调频(频点扫描、上下调台、存台/取台);
  • 灯光控制(Light Control):控制设备上的灯光效果(RGB 颜色、呼吸灯模式、亮度等)。

这些页面共享同一套「设备功能页」架构:以 BasicViewController 为基类,通过杰理 BLE SDK(JL_BLEKit 类库)向设备下发命令,并监听蓝牙连接状态。本文重点覆盖公共基类与页面生命周期机制;各功能页自身的业务细节(闹钟时间格式、FM 频段表等)属于对应子页面的实现,不在本页展开。

说明:受源文件检索预算限制,本页以已读取的 BasicViewController.m 与目录结构为事实依据;各功能页内部的具体命令封装未能在本次检索中完整读取,相关部分已如实标注。

概述

在杰理智能设备(音箱、耳机、TWS 充电盒等)的 iOS 配套 App 中,设备功能页承担着「把手机 UI 操作翻译为 BLE 设备命令」的职责。无论用户是浏览设备上的文件、设置闹钟、切换 FM 频点,还是调节灯光,交互路径都遵循同一模式:

  1. 用户从设备详情页进入某个功能页;
  2. 功能页通过 SDK 封装好的命令接口与设备通信(读状态 / 下发设置);
  3. 设备执行后通过通知或回调把结果返回给页面,页面刷新 UI。

因此,理解这些功能页的关键在于理解它们共同的基类 BasicViewController——它统一处理了导航栏、来电打断、设备断线退出等「页面无关」的横切逻辑,让每个功能子页只需关心自己的业务 UI 与数据。

BasicViewController 位于 DevicesViewController/DeviceInfoVC/ 目录,与 DeviceInfoVC(设备信息主页面)、NavViewController 并列,构成设备功能模块的视图控制器家族。其源码创建于 2023 年 9 月,属于该 App 设备控制链路的基座层。

架构

下图展示了设备功能页体系的组件关系与数据流(节点名称均取自仓库真实类名):

flowchart TD
    subgraph sg_AppLayer["App 视图层 (NewJieliZhiNeng)"]
        DeviceInfoVC["DeviceInfoVC 设备详情入口"]
        FeaturePage["功能子页 (文件浏览 / 闹钟 / FM / 灯光)"]
        BasicVC["BasicViewController 基类"]
    end

    subgraph sg_Infra["公共基础设施"]
        NavTopView["NavTopView 自定义导航栏"]
        JL_Tools["JL_Tools 通知/工具类"]
        CTCallCenter["CTCallCenter 来电监听"]
    end

    subgraph sg_SDK["杰理 BLE SDK 层 (JL_BLEKit)"]
        SDKCommand["SDK 命令接口 (设备读写)"]
        BLEGATT["BLE GATT 通道"]
    end

    DeviceInfoVC -->|"push 创建"| FeaturePage
    FeaturePage -->|"继承/复用"| BasicVC
    BasicVC --> NavTopView
    BasicVC --> JL_Tools
    BasicVC --> CTCallCenter
    BasicVC -->|"注册 kJL_BLE_M_ENTITY_DISCONNECTED"| JL_Tools
    FeaturePage -->|"调用命令"| SDKCommand
    SDKCommand --> BLEGATT
    BLEGATT -->|"状态回调/通知"| JL_Tools
    JL_Tools -->|"通知分发"| BasicVC

各组件职责说明:

  • BasicViewController:所有设备功能页的基类。负责统一的返回按钮、自定义导航栏(NavTopView)、安全区适配、设备断线自动退出、来电自动退出等横切行为,并暴露 initUI / initData 钩子供子类实现。
  • NavTopView:自定义顶部导航视图,带退出按钮(existBtn),由基类创建并布局。
  • JL_Tools:杰理工具类,提供 add:Action:Own: 通知注册机制,用于监听 SDK 广播的设备事件(如断开连接 kJL_BLE_M_ENTITY_DISCONNECTED)。
  • CTCallCenter:系统电话框架,用于在来电/拨号时让功能页自动返回,避免通话与设备操作互相干扰。
  • SDK 命令层(JL_BLEKit):各功能页实际下发命令的通道;本仓库业务代码依赖该类库与设备交互,命令细节在 SDK 侧封装。

该架构的设计意图很明确:把「页面共性」收敛到基类,把「设备差异」留给子类。闹钟、FM、灯光、文件浏览虽然业务完全不同,但它们在「进入页面 → 监听连接 → 发送命令 → 处理回调 → 异常退出」的生命周期上完全一致,因此可以共享同一套基座。

基础控制器实现剖析

BasicViewController 是整个设备功能页体系的骨架。它的实现集中在 viewDidLoad 及其调用的几个方法中,下面逐段分析其真实代码。

页面初始化与导航栏

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor colorFromHexString:@"#FFFFFF"];
    UIImage *image = [[UIImage imageNamed:@"Theme.bundle/icon_return.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
    UIBarButtonItem *leftBtn = [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStyleDone target:self action:@selector(backBtnAction)];
    leftBtn.tintColor = [UIColor grayColor];
    [self.navigationItem setLeftBarButtonItem:leftBtn];
    
    _naviView = [[NavTopView alloc] init];
    
    [_naviView.existBtn addTarget:self action:@selector(backBtnAction) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:_naviView];
    
    [_naviView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.right.top.equalTo(self.view);
        make.height.equalTo(@kJL_HeightNavBar);
    }];
    
    [self handOnCall];
    
    self.view.backgroundColor = kDF_RGBA(248, 250, 252, 1.0);
    [self initData];
    [self initUI];
    
    [JL_Tools add:kJL_BLE_M_ENTITY_DISCONNECTED Action:@selector(handleDisconnect) Own:self];
    
    _bottomSafeAreaHeight = 0;
    if (@available(iOS 11.0, *)) {
        _bottomSafeAreaHeight = self.view.safeAreaInsets.bottom;
    }
}

Source: BasicViewController.m

关键设计点:

  • 双返回入口:基类同时设置了系统导航栏的 leftBarButtonItem(使用 Theme.bundle/icon_return.png 图标)和自定义 NavTopView 的 existBtn,两者都指向 backBtnAction。这样无论子页面是否隐藏系统导航栏,用户都有返回途径;backBtnAction 内部执行 popViewControllerAnimated:true。
  • Masonry 自动布局:NavTopView 使用 mas_makeConstraints 固定为顶部通栏、高度 kJL_HeightNavBar,与系统导航栏高度约定保持一致。
  • 背景色统一:先用 #FFFFFF 打底,随后覆盖为 kDF_RGBA(248, 250, 252, 1.0) 浅灰蓝,保证所有功能页视觉一致。
  • 模板方法模式:initData 与 initUI 在基类中是空实现(见下文),子类按需覆写——这是 Objective-C 中常见的模板方法,约束了子页面的初始化顺序:先数据后 UI。

子类扩展钩子

-(void)initUI{
    
}

-(void)initData{
    
}

Source: BasicViewController.m

viewDidLoad 中 initData 先于 initUI 调用,暗示约定:子类应在 initData 中准备数据模型(例如闹钟列表、FM 频点数组、灯光状态),再在 initUI 中根据数据构建界面。基类提供空实现而非抽象方法,允许子类选择性覆写,减少样板代码。

设备断线处理

-(void)handleDisconnect{
    [self.navigationController popToRootViewControllerAnimated:true];
}

Source: BasicViewController.m

基类在 viewDidLoad 中通过 [JL_Tools add:kJL_BLE_M_ENTITY_DISCONNECTED Action:@selector(handleDisconnect) Own:self] 注册了设备断开通知。一旦蓝牙设备断开(例如用户关闭设备或走出范围),handleDisconnect 会把导航栈直接弹回根控制器。

这是设备功能页最重要的一致性保障:闹钟/FM/灯光/文件浏览页都依赖在线设备才能工作,断开后继续停留在页面只会让所有命令失败并产生误导性的空状态。直接回到根页面(设备列表)是干净、可预期的降级策略,避免每个子页各自处理断线逻辑。

JL_Tools 的 add:Action:Own: 签名意味着:Own: 参数指定通知监听者(此处为 self),框架会在监听者释放时自动移除通知,避免野指针与重复注册问题。

来电自动退出

-(void)handOnCall{
    callCenter = [[CTCallCenter alloc] init];
    __weak typeof (self) weakSelf = self;
    callCenter.callEventHandler = ^(CTCall *call) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if ([call.callState isEqualToString:CTCallStateDisconnected]) {
                kJLLog(JLLOG_DEBUG,@"CTCallCenter:Call has been disconnected");
            } else if ([call.callState isEqualToString:CTCallStateConnected]) {
                kJLLog(JLLOG_DEBUG,@"CTCallCenter:Callhasjustbeen connected");
            } else if ([call.callState isEqualToString:CTCallStateIncoming]) {
                kJLLog(JLLOG_DEBUG,@"CTCallCenter:Call is incoming");
                [weakSelf goBackToRoot];
            } else if ([call.callState isEqualToString:CTCallStateDialing]) {
                kJLLog(JLLOG_DEBUG,@"CTCallCenter:Call is Dialing");
                [weakSelf goBackToRoot];
            } else {
                kJLLog(JLLOG_DEBUG,@"CTCallCenter:Nothing is done");
            }
        });
    };
}

Source: BasicViewController.m

该方法的工程意图有两层:

  1. 避免音频冲突:来电/拨号时手机音频通道被通话占用,设备功能页(尤其 FM 与文件播放相关)继续操作会产生体验冲突,因此主动退出。
  2. 线程安全:CTCallCenter 的回调不保证在主线程,代码显式 dispatch_async(dispatch_get_main_queue(), ...) 切回主队列后再执行 goBackToRoot,避免 UIKit 跨线程操作。

__weak typeof(self) weakSelf 防止 block 持有控制器造成循环引用;goBackToRoot 是基类空实现钩子(默认什么都不做),子类可覆写为更精细的退出行为(如保留某些状态)。kJLLog(JLLOG_DEBUG, ...) 是杰理统一日志宏,便于排查通话状态变化。

设备功能页开发模式

基于基类约定,四类功能页(文件浏览 / 闹钟 / FM / 灯光)遵循相同的开发模板:

阶段基类钩子/机制子页面的典型动作
进入页面initData通过 SDK 读取设备当前状态(闹钟列表、FM 频点、灯光模式)
UI 构建initUI搭建列表/滑块/频点刻度等控件并绑定数据
用户操作事件回调调用 SDK 命令接口下发设置
状态返回通知/回调刷新 UI 或弹出错误提示
异常退出handleDisconnect / goBackToRoot自动返回根页面

这种「基类管生命周期、子类管业务」的划分,使得新增一个设备功能页只需继承 BasicViewController 并实现 initData/initUI,大幅降低了页面样板代码的重复,也保证了所有功能页在断线、来电等异常场景下行为一致。

核心流程

以「进入 FM 收音页并切换频点」为例,展示设备功能页的端到端执行时序(节点均为仓库/SDK 真实组件):

sequenceDiagram
    participant U as 用户
    participant DV as DeviceInfoVC
    participant FP as 功能子页 (闹钟/FM/灯光/文件)
    participant BC as BasicViewController
    participant SDK as JL_BLEKit SDK
    participant DEV as 蓝牙设备

    U->>DV: 点击功能入口
    DV->>FP: pushViewController 创建页面
    FP->>BC: viewDidLoad (继承基类)
    BC->>BC: 初始化导航栏/NavTopView
    BC->>BC: 注册断线通知 kJL_BLE_M_ENTITY_DISCONNECTED
    BC->>BC: 监听 CTCallCenter 来电
    FP->>BC: initData() → 准备数据
    FP->>BC: initUI() → 构建界面
    FP->>SDK: 发送读取命令 (如 FM 频点/闹钟列表)
    SDK->>DEV: BLE GATT 写命令
    DEV-->>SDK: 状态回调
    SDK-->>FP: 通知/回调返回数据
    FP->>FP: 刷新 UI (频点刻度/闹钟列表)
    alt 设备断开
        DEV--xSDK: 连接断开
        SDK-->>BC: 广播 kJL_BLE_M_ENTITY_DISCONNECTED
        BC->>BC: handleDisconnect()
        BC->>DV: popToRootViewController
    else 来电/拨号
        CTCallCenter-->>BC: 状态 = Incoming/Dialing
        BC->>BC: goBackToRoot()
    end

流程要点:

  1. 页面创建即建基座:功能子页被 push 后,先执行基类 viewDidLoad——导航栏、断线监听、来电监听全部就绪,然后才轮到子类的 initData/initUI。这个顺序保证异常保护机制在任何业务代码运行之前已生效。
  2. 业务数据双向流动:进入页面时子类主动读设备状态;用户操作时通过 SDK 写设备;设备状态变化通过 SDK 通知回流到页面刷新 UI。
  3. 异常路径集中处理:断线与来电两条异常路径都由基类统一兜底,子类无需感知——这正是基类设计最大的价值所在。

使用示例

示例一:自定义功能子页(模板)

基于 BasicViewController 的钩子约定,一个典型的设备功能子页如下(结构依据基类空实现推断,业务细节以实际子页为准):

@interface MyFeatureViewController : BasicViewController
@end

@implementation MyFeatureViewController

- (void)initData {
    // 1. 读取设备状态(闹钟列表 / FM 频点 / 灯光模式)
    // 2. 调用 SDK 命令接口,异步获取数据
}

- (void)initUI {
    // 根据 initData 的结果构建界面
    // 例如:闹钟列表 UITableView、FM 频点 UISlider、灯光色盘
}

@end

示例二:断线后自动返回根页面(基类真实实现)

-(void)handleDisconnect{
    [self.navigationController popToRootViewControllerAnimated:true];
}

Source: BasicViewController.m

当设备断开时,所有功能子页都会自动退出到根页面,用户重新连接设备后即可再次进入。这一行为由基类统一保证,各功能页代码中无需任何断线处理。

示例三:来电中断自动退出(基类真实实现)

} else if ([call.callState isEqualToString:CTCallStateIncoming]) {
    kJLLog(JLLOG_DEBUG,@"CTCallCenter:Call is incoming");
    [weakSelf goBackToRoot];
}

Source: BasicViewController.m

来电瞬间页面回到根控制器,避免 FM 播放/文件操作与通话音频冲突。goBackToRoot 为可覆写钩子,子类若需要保存草稿状态可在覆写中处理。

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

基于 BasicViewController 源码可验证的异常路径,以及设备功能页的固有特性,本模块存在以下失败模式:

设备断线(已验证)

  • 表现:BLE 连接断开,SDK 广播 kJL_BLE_M_ENTITY_DISCONNECTED,基类收到后 popToRootViewControllerAnimated:true。
  • 风险点:如果子页面正在执行 SDK 命令回调中刷新 UI,回调到达时页面可能已被弹出。基类的做法是整体退出而非逐页降级,规避了「页面存在但设备不可用」的一致性问题;子页面需自行注意回调中对 self 的弱引用与页面存活判断。
  • 处理建议:功能子页在 SDK 回调中应检查视图是否仍在窗口(view.window != nil)再刷新 UI。

来电/拨号中断(已验证)

  • 表现:CTCallCenter 检测到 CTCallStateIncoming / CTCallStateDialing 时执行 goBackToRoot;基类默认空实现,子类可覆写。
  • 并发注意:回调已由 dispatch_async(dispatch_get_main_queue(), ...) 保证在主线程执行,UI 操作安全;weakSelf 避免了 block 对控制器的强持有。

命令超时与设备无响应(SDK 层)

  • 闹钟/FM/灯光命令本质是 BLE 读写,若设备端未应答(固件异常、命令不支持),SDK 层应有超时机制;本仓库业务代码未在本次检索范围内发现超时兜底,页面需依赖 SDK 回调超时或用户手动返回。

页面重复创建与通知注册

  • JL_Tools add:Action:Own: 的 Own: 参数在监听者释放时自动移除监听,因此快速进出页面不会产生重复回调;但基类每次 viewDidLoad 都会注册一次,子类覆写时不应重复调用 [super viewDidLoad] 之外的注册逻辑。

多页面并发访问设备

  • 设备功能页均为单页 push 栈模型(DeviceInfoVC push 子页面),同一时刻只有顶层页面可与设备交互,天然避免了多页面并发写设备的冲突;这是「push 导航 + 基类统一退出」架构的额外收益。

性能与运维考虑

  • 轻量初始化:基类 viewDidLoad 只做导航栏、通知注册与系统监听,无重量级资源加载,功能页可快速呈现。
  • 日志可观测性:基类使用 kJLLog(JLLOG_DEBUG, ...) 输出来电状态日志,配合杰理统一日志体系可定位通话中断路径问题。
  • 资源释放:CTCallCenter 由控制器持有,控制器随导航栈 pop 释放后监听随之失效,无显式释放代码——符合 ARC 生命周期管理。
  • 安全区适配:基类缓存 _bottomSafeAreaHeight(iOS 11+),供子页面对齐底部控件,避免刘海屏/Home 指示条遮挡。

扩展点

扩展点位置说明
initData / initUIBasicViewController子类模板方法,新增功能页的必经入口
goBackToRootBasicViewController来电/拨号时的退出钩子,可覆写为保存状态或局部退出
backBtnActionBasicViewController返回按钮动作,子类可覆写以拦截返回(如未保存确认)
SDK 命令层JL_BLEKit 依赖新增设备能力(如新灯效)只需在 SDK 侧增加命令封装

新增一个设备功能页(例如「EQ 音效」)的标准步骤:新建继承 BasicViewController 的控制器 → 在 initData 中读取设备状态 → 在 initUI 中构建界面 → 在操作事件中调用 SDK 命令。断线、来电等异常行为自动获得。

测试与验证

本次源检索范围内未发现针对 BasicViewController 的单元测试文件;其行为(断线退出、来电退出)属于 UI 级集成行为,需依赖真机 + 真实 BLE 设备验证:

  • 断线场景:进入任一功能页后关闭设备电源或断开蓝牙,验证自动回到根页面;
  • 来电场景:进入 FM/文件页后拨打/接听电话,验证页面自动退出且无崩溃;
  • 快速进出:连续 push/pop 功能页,验证无重复通知回调、无野指针。

相关链接

  • 设备详情入口页:DevicesViewController/DeviceInfoVC/DeviceInfoVC(各功能页的 push 来源)
  • 导航控制器:DevicesViewController/DeviceInfoVC/NavViewController
  • 基类源码:BasicViewController.m
  • 设备音乐/文件播放(文件浏览的关联能力):DeviceMusicVC/DeviceMusic/DeviceMusicVC
  • 杰理 BLE SDK(JL_BLEKit)命令层为功能页与设备通信的底层依赖
Prev
Auracast 广播接收与发射
Next
ANC、按键设置与查找设备