闹钟管理
闹钟管理是 Flutter 侧通过 BLE 桥接层与设备端 RTC 闹钟进行交互的能力封装,覆盖闹钟列表读取、新增/编辑保存、开关切换、删除、铃声选择、设备时间同步与响铃停止等完整生命周期操作。
Purpose and Scope
本页文档面向 6-device-features.alarm(闹钟管理)目录项,全面说明该能力在 Flutter SDK(jl_home 包)中的实现:
- 对外 API 层:
BleAlarmManager(发送接口)与 BLE 事件流(接收接口)的角色划分; - 数据模型:
AlarmModel、RingInfoModel、RingModel的结构与序列化方式; - 桥接机制:通过
BleBaseManager.invokeMethod走方法通道(MethodChannel)到达 Android 原生 SDK 的过程; - 关键算法:
rtcMode位掩码表示的循环模式(单次/每天/工作日/自定义星期)解析逻辑; - 失败模式、边界情况与扩展点。
以下主题属于其他目录页,不在本页展开:
- 设备连接与指令分发:
BleBaseManager、BleMethodConstants的底层通道实现,请参见设备通信相关页面; - 铃声文件管理:
RingInfoModel中ringData/fileClus指向的文件系统操作,请参见文件管理页面; - 原生 SDK 内部实现:Android 侧
AlarmManager.kt、AlarmProcessor.kt等仅作对端说明引用,其协议细节属于原生层文档。
Overview
闹钟管理(Alarm Management)是智能硬件(耳机/音箱类产品)的典型设备功能:设备端维护一组 RTC 闹钟条目,App 负责展示、编辑并下发指令。Flutter-JL_Home 仓库采用 Flutter 逻辑层 + 原生方法通道 + BLE 传输 的三层架构:
- Flutter 调用层(
libs/Send Interface/ble_alarm_manager.dart):BleAlarmManager提供全部闹钟操作的静态方法,统一封装参数拼装与结果类型转换,业务代码(UI)只面对强类型的AlarmModel/RingModel; - 桥接层:每个方法调用
BleBaseManager.invokeMethod,以BleMethodConstants中定义的字符串方法名与参数键拼装参数,经方法通道发送到 Android 原生侧; - 原生层(
code/JieLi_Home_Demo/android/.../alarm/):AlarmManager.kt、AlarmOperationManager.kt负责将请求翻译为 BLE 指令包,与设备交互;设备上报的事件由AlarmProcessor.kt/AlarmHandler.java处理后经ble_event_stream.dart回传给 Flutter 业务层。
该设计的核心意图是将"设备协议细节"与"UI 业务"彻底隔离:Flutter 侧只关心语义化 API(如"获取全部闹钟""保存闹钟"),而协议打包、校验、重发、超时全部收敛在原生层,便于多端复用与协议演进。
Architecture
flowchart TD
subgraph sg_Flutter["Flutter 应用层"]
UI["业务 UI / ViewModel"]
Stream["ble_event_stream.dart<br/>(BLE 事件流)"]
Processor["ble_alarm_processor.dart<br/>(闹钟事件处理器)"]
end
subgraph sg_Send["发送接口层 libs/Send Interface"]
Manager["BleAlarmManager"]
Base["BleBaseManager.invokeMethod"]
Const["BleMethodConstants<br/>(方法名/参数键)"]
end
subgraph sg_Model["数据模型层"]
Alarm["AlarmModel"]
Ring["RingInfoModel / RingModel"]
end
subgraph sg_Native["Android 原生 SDK"]
AlarmMgr["AlarmManager.kt"]
AlarmOpMgr["AlarmOperationManager.kt"]
NativeModel["AlarmModel.kt / AlarmBean.java<br/>AlarmListInfo.java / DefaultAlarmBell.java"]
AlarmProc["AlarmProcessor.kt / AlarmHandler.java<br/>AlarmNotifyHandle.java"]
end
subgraph sg_Device["BLE 设备"]
Dev["设备 RTC 闹钟存储"]
end
UI -->|"静态调用"| Manager
Manager -->|"构造参数"| Const
Manager -->|"invokeMethod"| Base
Base -->|"MethodChannel"| AlarmMgr
AlarmMgr --> AlarmOpMgr
AlarmOpMgr --> NativeModel
AlarmOpMgr -->|"BLE 指令"| Dev
Dev -->|"事件/通知"| AlarmProc
AlarmProc --> Processor
Processor --> Stream
Stream -->|"事件回调"| UI
Manager -->|"fromMap/toMap"| Alarm
Alarm --> Ring
架构要点:
- 单向调用链(下发):
UI → BleAlarmManager → BleBaseManager → 原生 → 设备。BleAlarmManager的所有方法均为static,无需实例化,调用方通过BleMethodConstants常量保证方法名与参数键不散落为魔法字符串; - 反向事件链(上报):设备主动上报的闹钟状态(如响铃中、闹钟更新通知)由原生
AlarmProcessor解析后经ble_event_stream转发给 Flutter 侧ble_alarm_processor,再分发到业务层,实现"下发-上报"的双向闭环; - 模型层解耦:
AlarmModel同时承担"设备协议字段容器"(rtc*前缀字段)与"UI 展示辅助"(formattedTime、getRepeatDays、ringTypeDescription)两种职责,减少业务侧重复解析代码。
数据模型
AlarmModel
AlarmModel(alarm_model.dart)是闹钟条目的核心模型。字段以 rtc(Real-Time Clock)前缀命名,直接映射设备 RTC 闹钟的协议字段;rtcMode 是位掩码(bitmask),每一位表示星期几重复。
| 字段 | 类型 | 含义 |
|---|---|---|
rtcYear / rtcMonth / rtcDay | int | 闹钟日期(年/月/日),单次闹钟使用 |
rtcHour / rtcMin / rtcSec | int | 闹钟时间(时/分/秒) |
rtcEnable | bool | 闹钟开关状态 |
rtcMode | int | 循环模式位掩码(0=单次,bit0=每天,bit1~bit7=周一~周日) |
rtcIndex | int | 闹钟在设备列表中的索引(0 起) |
rtcName | String | 闹钟名称 |
ringInfo | RingInfoModel? | 铃声信息(类型 + 具体铃声数据),可空 |
ringData | String? | 铃声原始数据(如自定义铃声标识),可空 |
模型提供三条序列化/克隆路径:
factory AlarmModel.fromMap(dynamic mapData):从原生层返回的 Map 构建模型。实现上先做一次类型归一化——把非String键统一toString()转成字符串键,再对ringInfo做空值与类型双重安全检查后才调用RingInfoModel.fromMap,其余字段全部用??提供默认值,避免原生层缺字段时抛空指针:factory AlarmModel.fromMap(dynamic mapData) { final Map<String, dynamic> map = {}; if (mapData is Map) { mapData.forEach((key, value) { if (key is String) { map[key] = value; } else if (key != null) { map[key.toString()] = value; } }); } // 安全处理 ringInfo RingInfoModel? ringInfo; if (map['ringInfo'] != null && map['ringInfo'] is Map) { ringInfo = RingInfoModel.fromMap(map['ringInfo']); } return AlarmModel( rtcYear: map['rtcYear'] ?? 0, // ... 其余字段均带 ?? 默认值 ringInfo: ringInfo, ringData: map['ringData'], ); }Source: alarm_model.dart
toMap():反向序列化,供saveAlarmInfo/selectRing等写操作拼装参数;ringInfo为空时序列化为null;copyWith(...):不可变风格的部分更新,业务层修改单个字段(如切换rtcEnable)时使用。
RingInfoModel / RingModel
RingInfoModel(定义于ring_info_model.dart,由AlarmModel导入)描述闹钟绑定的铃声:type字段区分铃声来源——0x00设备默认铃声、0x01媒体(文件)选择铃声;AlarmModel.ringTypeDescription基于该字段给出可读描述;RingModel(libs/model/ring_model.dart)是铃声列表项模型,至少包含index与name字段——getDefaultRings()的兜底分支RingModel(index: 0, name: 'Unknown ring')验证了这一点,用于设备默认铃声列表的展示。
循环模式位掩码解析
rtcMode 是闹钟重复策略的紧凑编码,一个 int 承载 8 个布尔位。getRepeatDays(Map<String, String> localizedDays)(alarm_model.dart)按如下优先级把位掩码翻译为本地化星期文案:
flowchart TD
Start([getRepeatDays]) --> Mode0{"rtcMode == 0?"}
Mode0 -->|"是"| Once["返回 ['once'] 单次"]
Mode0 -->|"否"| Bit0{"bit0 == 1?"}
Bit0 -->|"是"| Every["返回 ['everyDay'] 每天"]
Bit0 -->|"否"| Week6{"bit1-bit6 全为 1?"}
Week6 -->|"是"| MonSat["周一至周六"]
Week6 -->|"否"| Week5{"bit1-bit5 全为 1?"}
Week5 -->|"是"| Workdays["工作日(周一至周五)"]
Week5 -->|"否"| Each["按 bit1-bit7 逐位收集<br/>周一 ~ 周日"]
解析顺序的设计意图:
- 特例优先:
rtcMode == 0表示"单次"、bit0表示"每天",这两个是最高频且语义最简的配置,先短路返回,避免落入逐位收集的通用分支; - 组合特例:
bit1~bit6全为 1 时折叠为"周一至周六",bit1~bit5全为 1 时折叠为"工作日(周一至周五)"——这两类在消费电子产品中极为常见(上学/通勤场景),折叠后可让 UI 直接展示短语而非六个星期标签; - 通用回退:其余情况逐位检查
bit1(周一)到bit7(周日),按位序追加本地化名称; - 可本地化:方法接收
localizedDays字典而非硬编码文案,键名once/everyDay/monday...sunday,UI 层可注入任意语言。
// 特例:每天 (bit0 = 1)
if (bt_0 == 1) {
return [localizedDays['everyDay'] ?? 'Every day'];
}
// 工作日 (周一至周五,bit1-bit5 全部为1)
if (bt_5 + bt_4 + bt_3 + bt_2 + bt_1 == 5) {
return [
localizedDays['monday'] ?? 'Mon',
// ... tuesday ~ friday
];
}
// 单独处理每一天
if (bt_1 == 1) days.add(localizedDays['monday'] ?? 'Mon');
if (bt_2 == 1) days.add(localizedDays['tuesday'] ?? 'Tue');
// ... bt_3 ~ bt_7
return days;
Source: alarm_model.dart
模型层还提供 formattedTime(HH:mm 补零)与 formattedDate(yyyy-MM-dd)两个计算属性,供列表/编辑页直接渲染,避免业务层重复格式化。
核心流程
下发流程:获取闹钟列表
以 getAllAlarmList() 为例,展示完整的数据往返路径(ble_alarm_manager.dart):
sequenceDiagram
participant UI as 业务 UI
participant Mgr as BleAlarmManager
participant Base as BleBaseManager
participant Native as 原生 AlarmManager.kt
participant Dev as BLE 设备
UI->>Mgr: getAllAlarmList()
activate Mgr
Mgr->>Base: invokeMethod(methodGetAllAlarmList)
activate Base
Base->>Native: MethodChannel 调用
activate Native
Native->>Dev: 发送 BLE 查询指令
Dev-->>Native: 返回闹钟列表数据
Native-->>Base: List<dynamic> (Map 列表)
deactivate Native
Base-->>Mgr: List<dynamic>
deactivate Base
Mgr->>Mgr: _convertToAlarmModels 遍历并 fromMap
Mgr-->>UI: List<AlarmModel>
deactivate Mgr
写操作流程:保存闹钟
写操作(新增/编辑/开关/删除/选铃)统一遵循 invokeMethod(方法名, arguments: {参数键: 值}) 模式。以保存为例,saveAlarmInfo(alarmMap, isNewAlarm) 携带完整闹钟 Map 与新增标志两个参数下发给原生层;原生层根据 isNewAlarm 决定走"插入新槽位"还是"覆盖 rtcIndex 指向的槽位"协议分支。选择铃声的流程类似,但参数不同:selectRing 用 ringIndex 指向设备默认铃声表,selectCardRing 用 cardType + fileClus 指向存储卡上的铃声文件,二者是设备端两类铃声来源(内置 vs 文件)的不同寻址方式。
Usage Examples
示例一:读取并展示闹钟列表
getAllAlarmList() 返回原生层原始 List<dynamic>,_convertToAlarmModels 只接受 Map 元素并逐个 AlarmModel.fromMap 转换,非 Map 元素被静默跳过——这一宽容策略保证原生层混入异常元素时列表不整体崩溃:
static Future<List<AlarmModel>> getAllAlarmList() async {
try {
final List<dynamic> result = await BleBaseManager.invokeMethod(
BleMethodConstants.methodGetAllAlarmList,
);
return _convertToAlarmModels(result);
} catch (e) {
rethrow;
}
}
static List<AlarmModel> _convertToAlarmModels(List<dynamic> rawData) {
List<AlarmModel> alarmList = [];
for (var item in rawData) {
if (item is Map) {
final alarm = AlarmModel.fromMap(item);
alarmList.add(alarm);
}
}
return alarmList;
}
Source: ble_alarm_manager.dart
示例二:新增/编辑闹钟
业务层把 AlarmModel 转为 Map 后调用 saveAlarmInfo;isNewAlarm 决定设备端是插入还是覆盖。注意该方法返回 bool,调用方可以据此向用户反馈保存是否成功:
/// Save alarm information
static Future<bool> saveAlarmInfo(
Map<String, dynamic> alarmMap,
bool isNewAlarm,
) async {
return await BleBaseManager.invokeMethod(
BleMethodConstants.methodSaveAlarm,
arguments: {
BleMethodConstants.argAlarm: alarmMap,
BleMethodConstants.argIsNewAlarm: isNewAlarm,
},
);
}
Source: ble_alarm_manager.dart
示例三:选择铃声(内置铃声 vs 存储卡铃声)
两条选铃路径共享 alarm(目标闹钟)与 ringName 参数,区别在铃声寻址方式:selectRing 用 ringIndex(设备默认铃声表下标),selectCardRing 用 cardType + fileClus(文件系统簇号)。两者均在异常时返回 false 而非抛出,适合 UI 直接据此提示失败:
/// Set selected ringtone
static Future<bool> selectRing({
required Map<String, dynamic> alarm,
required String ringName,
required int ringIndex,
}) async {
try {
final result = await BleBaseManager.invokeMethod(
BleMethodConstants.methodSelectAlarmRing,
arguments: {
BleMethodConstants.argAlarm: alarm,
BleMethodConstants.argRingName: ringName,
BleMethodConstants.argIndex: ringIndex,
},
);
return result == true;
} catch (e) {
return false;
}
}
/// Set card selected ringtone
static Future<bool> selectCardRing({
required Map<String, dynamic> alarm,
required String ringName,
required int cardType,
required int fileClus,
}) async {
try {
final result = await BleBaseManager.invokeMethod(
BleMethodConstants.methodSelectAlarmCardRing,
arguments: {
BleMethodConstants.argAlarm: alarm,
BleMethodConstants.argRingName: ringName,
BleMethodConstants.argCardType: cardType,
BleMethodConstants.argFileClus: fileClus,
},
);
return result == true;
} catch (e) {
return false;
}
}
Source: ble_alarm_manager.dart
方法常量与参数约定
所有方法名与参数键集中在 BleMethodConstants(libs/constant/ble_method_constants.dart),由 BleAlarmManager 引用,保证 Flutter 与原生侧契约单一来源。从 ble_alarm_manager.dart 的实际调用可归纳出闹钟域使用的常量:
| 常量(方法) | 用途 | 携带参数 |
|---|---|---|
methodGetAllAlarmList | 获取全部闹钟列表 | 无 |
methodSaveAlarm | 保存(新增/编辑)闹钟 | argAlarm(完整闹钟 Map)、argIsNewAlarm(bool) |
methodUpdateAlarmState | 更新闹钟开关 | argIndex、argAlarmStatus |
methodDeleteCurrentAlarm | 删除指定闹钟 | argIndex |
methodSetDeviceSyncTime | 同步设备时间 | 无 |
methodGetDefaultRings | 获取设备默认铃声列表 | 无 |
methodSelectAlarmRing | 选择内置默认铃声 | argAlarm、argRingName、argIndex |
methodSelectAlarmCardRing | 选择存储卡铃声 | argAlarm、argRingName、argCardType、argFileClus |
methodStopAlarmBell | 停止响铃 | 无 |
表中方法名/参数键均取自 ble_alarm_manager.dart 的实际引用;常量文件本身未在本页逐一展开,其定义应与上表一致。
API Reference
BleAlarmManager
全部为 static 方法,调用前需确保 BLE 已连接(连接管理见设备通信相关页面)。
static Future<List<AlarmModel>> getAllAlarmList()
获取设备全部闹钟。
- 返回:
List<AlarmModel>;转换过程中非Map元素被跳过。 - 抛出:
rethrow原样向上传播底层(通道/原生)异常,调用方需自行 try-catch。
static Future<void> updateAlarmStatus(int index, bool status)
切换指定索引闹钟的开关状态。
- 参数:
index(int,闹钟槽位索引);status(bool,目标开关状态)。 - 返回:
void;无失败反馈,调用方需结合事件流或重新拉取列表确认。
static Future<void> deleteAlarm(int index)
删除指定索引闹钟。
- 参数:
index(int,闹钟槽位索引)。 - 返回:
void。
static Future<bool> setDeviceSyncTime()
以手机当前时间同步设备 RTC。
- 返回:
bool,同步是否成功。通常在闹钟列表页进入前调用,保证设备端时间基准正确。
static Future<bool> saveAlarmInfo(Map<String, dynamic> alarmMap, bool isNewAlarm)
保存闹钟信息。
- 参数:
alarmMap(AlarmModel.toMap()产物);isNewAlarm(true=新增,false=覆盖编辑)。 - 返回:
bool保存结果。
static Future<List<RingModel>> getDefaultRings()
获取设备默认铃声列表。
- 返回:
List<RingModel>;通道异常时返回空列表(内部吞掉异常),非 Map 元素兜底为RingModel(index: 0, name: 'Unknown ring')。
static Future<bool> selectRing({required Map<String, dynamic> alarm, required String ringName, required int ringIndex})
为闹钟选择设备内置默认铃声。
- 参数:
alarm(目标闹钟 Map);ringName(铃声名);ringIndex(铃声在默认铃声表中的下标)。 - 返回:
bool;异常时返回false。
static Future<bool> selectCardRing({required Map<String, dynamic> alarm, required String ringName, required int cardType, required int fileClus})
为闹钟选择存储卡(TF 卡)上的铃声文件。
- 参数:
cardType(存储卡类型)、fileClus(文件起始簇号),二者共同定位文件系统中的铃声。 - 返回:
bool;异常时返回false。
static Future<void> stopAlarmBell()
停止设备当前响铃(如用户点击"关闭闹铃")。
- 返回:
void。
Failure Modes、边界情况与并发
失败模式
- 通道层异常:
getAllAlarmList采用rethrow,任何通道/原生异常都会冒泡——这是有意为之:列表是后续操作的前提,失败必须可见;而selectRing/selectCardRing/getDefaultRings采用内部捕获并返回兜底值(false/ 空列表),因为选铃失败不应中断整个页面流程,UI 只需提示失败即可; - 原生缺字段:
AlarmModel.fromMap对所有字段提供??默认值(数值 0、布尔 false、字符串空串),旧固件返回的残缺数据不会导致解析崩溃,但可能呈现"1970-01-01 00:00"之类的空闹钟——业务层应过滤rtcIndex无效或全零的条目; - 异常元素注入:
_convertToAlarmModels对非Map元素静默跳过,避免单条脏数据拖垮整个列表。
边界情况
- 索引寻址:
updateAlarmStatus/deleteAlarm以rtcIndex为唯一寻址依据。设备端槽位在删除后是否重排由原生协议决定,Flutter 侧每次操作后应重新拉取列表,否则本地缓存的索引可能错位; - 单次闹钟:
rtcMode == 0表示单次闹钟,依赖rtcYear/Month/Day日期字段;编辑此类闹钟切换为重复模式时,业务层需同步清理/保留日期字段,模型层本身不做该判断; - 空铃声:
ringInfo可空,ringTypeDescription对空值返回'Unknown',UI 层需处理"未选铃声"状态。
并发与一致性
BleAlarmManager为无状态静态类,本身无并发问题;但底层方法通道与 BLE 链路通常串行化指令,连续快速调用写操作(如连点开关)可能被原生层排队或丢弃,业务层应做防抖或依赖事件流确认最终状态;- 开关切换后设备若在本地播放/停止响铃,状态变化经由事件流异步回传,UI 侧以事件流为准而非本地乐观更新,避免状态回跳。
性能与运维注意事项
getAllAlarmList每次全量拉取,闹钟数量通常很小(个位数~十位数),无需分页;但 BLE 传输速率低,涉及铃声数据(ringData)时注意控制单次载荷;setDeviceSyncTime建议在闹钟编辑页首次展示前调用一次即可,避免频繁同步消耗电量;- 所有方法最终都走 MethodChannel,Android 主线程与 BLE 工作线程的切换由原生层处理;Flutter 侧
Future天然异步,不会阻塞 UI。
扩展点
- 新闹钟操作:在
BleAlarmManager增加静态方法,遵循invokeMethod(常量方法名, arguments)模式,并在BleMethodConstants补充方法名/参数键常量,同时扩展原生侧AlarmOperationManager的对应协议实现; - 铃声来源扩展:
RingInfoModel.type目前区分0x00内置与0x01媒体文件;若新增来源(如云铃声),可在ringTypeDescription增加分支并新增对应选铃方法; - 循环模式新语义:
getRepeatDays的位掩码约定若被新固件扩展(如节假日模式),需在该方法中补充特例分支;注意保持现有特例优先级,避免破坏旧固件兼容; - 事件流消费:
ble_event_stream.dart中注册ble_alarm_processor后,业务层可订阅闹钟相关事件(如响铃中/停止),实现"设备端响铃时 App 弹窗"等交互。
Tests
本页覆盖的代码(BleAlarmManager、AlarmModel)未在本次检索中发现独立测试文件。从实现可推导的推荐测试点:
AlarmModel.fromMap的缺字段兜底与ringInfo空值/非 Map 分支;getRepeatDays的位掩码矩阵:0(单次)、bit0(每天)、bit1~6 全置位(周一至周六)、bit1~5 全置位(工作日)、任意组合(自定义星期);_convertToAlarmModels混入非 Map 元素的过滤行为;selectRing/selectCardRing异常路径返回false。
Related Links
- BleAlarmManager 实现(发送接口)
- AlarmModel 数据模型
- BLE 事件流(接收接口)
- Android 原生 AlarmManager.kt
- Android 原生 AlarmOperationManager.kt
- Android 原生 AlarmProcessor.kt
- 设备通信与方法通道:见设备通信相关目录页(
BleBaseManager/BleMethodConstants) - 铃声文件管理:见文件管理相关目录页(
RingInfoModel/ringData)