固件文件管理
固件文件管理是 JL OTA 示例应用中负责管理设备端 OTA 固件文件的模块:通过 BLE 事件流获取设备上的固件文件列表、维护当前选中文件的状态、调用底层插件删除设备上的固件文件,并在 UI 中实时刷新。
Purpose and Scope
本页面向 5-example-app/5-3-firmware-files 目录,完整说明示例应用(code/JL_OTA/example)与 OTA 插件(code/JL_OTA/lib)中与固件文件管理相关的实现:
OtaFileManager:文件列表状态与删除操作的核心管理类;- BLE 事件流
otaFileListStream:固件文件列表如何从设备事件中解析出来; UpdatePage页面:文件列表状态、订阅与选中路径持久化的集成方式;- 底层常量
TYPE_OTA_FILE_LIST、KEY_NAME、KEY_PATH的数据契约。
以下内容不在本页范围内,请参见目录中对应的兄弟页面:OTA 升级流程与传输控制(5-x-ota-upgrade)、设备连接与会话管理(5-x-connection)、设置与偏好管理(5-x-settings)。本页只聚焦"固件文件"这一数据域:列表来源、选中状态、删除操作。
Overview
在 JL OTA 系统中,固件文件存储在设备端(而非应用端)。应用通过 BLE 与设备通信,设备将自身的固件文件清单以事件形式上报,应用负责:
- 订阅文件列表事件:监听插件广播的
otaFileList类型事件,解析出{name, path}结构的文件列表; - 维护选中状态:记录用户当前选中的固件文件路径,作为后续 OTA 升级操作的输入;
- 删除设备端文件:调用底层 BLE 方法按索引删除设备上的固件文件,删除成功后事件流会再次推送新列表,UI 随之刷新。
这种"设备端存文件、事件流同步、管理器维护状态"的设计,使得应用 UI 与 BLE 协议细节解耦:OtaFileManager 通过两个回调(onFileListUpdated、onSelectedFileChanged)把状态变化"推"给页面,页面只需在回调中 setState 即可。
Architecture
flowchart TD
subgraph sg_Device["设备端 (BLE)"]
FW["固件文件存储"]
end
subgraph sg_Plugin["OTA 插件层 (jl_ota)"]
PC["MethodChannel<br/>com.jieli.ble_plugin/methods"]
ES["BleEventStream<br/>otaFileListStream"]
BC["BleEventConstants<br/>TYPE_OTA_FILE_LIST / KEY_PATH"]
BM["BleMethod<br/>deleteOtaIndex(index)"]
end
subgraph sg_App["示例应用层 (example)"]
UP["UpdatePage<br/>_otaFileList / _selectedFilePath"]
OFM["OtaFileManager"]
FP["FilePreferenceManager<br/>loadOtaPath 持久化"]
end
FW -->|"上报 otaFileList 事件"| PC
PC --> ES
ES -->|"解析为 List<Map<String,String>>"| UP
ES --> BC
UP --> OFM
OFM -->|"deleteFile(index)"| BM
BM -->|"BLE 命令删除文件"| FW
UP <-->|"loadOtaPath / 选中路径"| FP
UP -->|"onFileListUpdated / onSelectedFileChanged 回调"| OFM
架构说明
- 事件数据契约:设备上报的事件类型为
otaFileList(BleEventConstants.TYPE_OTA_FILE_LIST),每个文件条目包含name与path两个字符串字段(KEY_NAME/KEY_PATH)。事件流在 ble_event_stream.dart 中被统一映射为标准List<Map<String, String>>。 - 管理器与页面解耦:
OtaFileManager不持有 BuildContext,只持有列表数据与两个回调;页面负责把回调翻译为setState,从而让文件管理逻辑可独立测试、可复用到其他页面。 - 删除链路:删除操作通过
BleMethod.deleteOtaIndex(index)走插件通道下发 BLE 命令;命令按索引(而非路径)定位文件,这是设计上需要特别注意的点(详见"Failure Modes")。
核心实现分析
OtaFileManager:文件列表状态与删除操作
OtaFileManager 是整个固件文件管理的枢纽,定义在 ota_file_manager.dart:
/// Manages OTA files, including reading and deleting files.
class OtaFileManager {
final List<Map<String, String>> otaFileList;
final Function(List<Map<String, String>>) onFileListUpdated;
final Function(String?) onSelectedFileChanged;
OtaFileManager({
required this.otaFileList,
required this.onFileListUpdated,
required this.onSelectedFileChanged,
});
Future<void> deleteFile(int index) async {
try {
await BleMethod.deleteOtaIndex(index);
final filePath = otaFileList[index][BleEventConstants.KEY_PATH];
onSelectedFileChanged(filePath);
} catch (e) {
//print("Failed to delete file: $e");
}
}
}
设计要点:
- 数据是"注入"而非"内部持有":
otaFileList通过构造函数传入,与页面共享同一份列表引用。当页面从事件流收到新列表并赋值给_otaFileList后,管理器读取的也是最新数据。这要求调用方在更新列表时同步更新(页面正是通过onFileListUpdated回调 +setState完成)。 - 回调即状态通知:
onSelectedFileChanged接收String?路径,用于"删除后同步选中态"。页面侧的实现会把与当前选中路径相同的值置为null,实现"删除即取消选中"的语义。 - 删除失败静默处理:
catch块为空(原print被注释),说明删除失败不影响调用链,UI 保持原状,等下一次事件流刷新。这是示例应用的容错取舍:宁可静默也不弹错误中断用户操作。
文件列表事件流:从设备事件到 UI 数据
插件侧将 BLE 事件统一通过 BleEventStream 暴露,固件文件列表流的定义位于 ble_event_stream.dart:
// OTA文件列表流
static Stream<List<Map<String, String>>> get otaFileListStream {
return baseStream
.where((event) => event[BleEventConstants.TYPE] == BleEventConstants.TYPE_OTA_FILE_LIST)
.map((event) => (event[BleEventConstants.DATA] as List<dynamic>).map((fileMap) {
return {
BleEventConstants.KEY_NAME: fileMap[BleEventConstants.KEY_NAME] as String? ?? '',
BleEventConstants.KEY_PATH: fileMap[BleEventConstants.KEY_PATH] as String? ?? '',
};
}).toList());
}
来源:ble_event_stream.dart(中间省略了
.where与.map之间的管道细节,以上为实际观测到的关键行)
该流做了两件事:
- 按类型过滤:只保留
TYPE == 'otaFileList'的事件,避免其他 BLE 事件(连接状态、升级进度等)混入; - 字段规范化:把设备上报的原始
Map收敛为固定{name, path}两个键,并用as String? ?? ''兜底空值,保证下游 UI 永远拿到结构稳定的数据。
TYPE_OTA_FILE_LIST = 'otaFileList' 与 KEY_PATH = 'path' 在 ble_event_constants.dart 中定义,是全链路的事件-字段契约。
UpdatePage 集成:状态、订阅与持久化
页面侧(update_page.dart)负责三件事:
List<Map<String, String>> _otaFileList = []; // 用于存储文件列表
String? _selectedFilePath; // 用于存储当前选中的文件路径
StreamSubscription<List<Map<String, String>>>? _otaFileListSubscription;
late OtaFileManager _otaFileManager;
void initState() {
super.initState();
_initializeManagers();
_initialize();
_methodChannel.setMethodCallHandler(_handleMethodCall);
FilePreferenceManager.loadOtaPath().then((path) {
setState(() {
_selectedFilePath = path;
});
});
}
void _initializeManagers() {
_otaFileManager = OtaFileManager(
otaFileList: _otaFileList,
onFileListUpdated: (newList) {
setState(() {
_otaFileList = newList;
});
},
onSelectedFileChanged: (filePath) {
setState(() {
if (_selectedFilePath == filePath) {
_selectedFilePath = null;
}
});
},
);
// ...
}
关键行为:
- 启动时恢复选中路径:
FilePreferenceManager.loadOtaPath()异步读取上次保存的固件路径,回填_selectedFilePath,实现"重启应用后仍记住上次选中的固件"; - 回调即 setState:
onFileListUpdated直接替换_otaFileList触发重建;onSelectedFileChanged采用"相等则置空"的切换语义——删除的恰是当前选中文件时,选中态自动清空; - 平台通道:页面还持有
MethodChannel('com.jieli.ble_plugin/methods')处理非流式方法调用,文件列表这类持续变化的数据则走事件流订阅(_otaFileListSubscription),两类通道职责分明。
Core Flow
固件文件删除的完整时序如下:
sequenceDiagram
participant UI as UpdatePage
participant M as OtaFileManager
participant B as BleMethod (插件)
participant D as 设备 (BLE)
participant S as BleEventStream
UI->>UI: initState 订阅 otaFileListStream
D-->>S: 上报 otaFileList 事件
S-->>UI: 解析为 List{name, path}
UI->>UI: onFileListUpdated → setState(_otaFileList)
UI->>M: deleteFile(index)
M->>B: BleMethod.deleteOtaIndex(index)
B->>D: 下发 BLE 删除命令
D-->>B: 删除成功
B-->>M: 返回(异常被 catch 吞掉)
M->>M: 读取 otaFileList[index][path]
M-->>UI: onSelectedFileChanged(filePath)
UI->>UI: 若为当前选中路径则置空
D-->>S: 再次上报新文件列表
S-->>UI: onFileListUpdated → setState 刷新
流程要点
- 删除按索引定位:
deleteOtaIndex(index)使用列表下标,调用方必须保证index与当前otaFileList一致; - 删除后立即同步选中态:管理器在删除命令返回后马上读取原列表该索引的
path并通知回调——此时设备端已删除该文件,UI 侧将选中路径清空,避免"选中一个不存在的文件"; - 最终一致性由事件流兜底:无论删除成功与否,设备都会(在成功时)再次广播文件列表,页面最终会以事件流数据为准刷新,因此删除与选中态的更新是"乐观 + 校正"的组合。
Usage Examples
在页面中装配 OtaFileManager 并订阅文件列表
以下代码展示示例应用中文件管理模块的完整装配方式:管理器通过回调与页面状态绑定,事件流订阅驱动列表刷新。
late OtaFileManager _otaFileManager;
void _initializeManagers() {
_otaFileManager = OtaFileManager(
otaFileList: _otaFileList,
onFileListUpdated: (newList) {
setState(() {
_otaFileList = newList;
});
},
onSelectedFileChanged: (filePath) {
setState(() {
if (_selectedFilePath == filePath) {
_selectedFilePath = null;
}
});
},
);
// 同时初始化 DialogManager / PopupMenuManager / OtaConnectionManager
}
删除设备上的固件文件
删除操作完全封装在 OtaFileManager.deleteFile 中,页面只需传入列表索引;删除成功后将自动通过回调同步选中态。
Future<void> deleteFile(int index) async {
try {
await BleMethod.deleteOtaIndex(index);
final filePath = otaFileList[index][BleEventConstants.KEY_PATH];
onSelectedFileChanged(filePath);
} catch (e) {
//print("Failed to delete file: $e");
}
}
消费文件列表事件流
需要独立获取固件文件列表(不依赖页面)时,可直接订阅插件暴露的事件流:
static Stream<List<Map<String, String>>> get otaFileListStream {
return baseStream
.where((event) => event[BleEventConstants.TYPE] == BleEventConstants.TYPE_OTA_FILE_LIST)
.map((event) => (event[BleEventConstants.DATA] as List<dynamic>).map((fileMap) {
return {
BleEventConstants.KEY_NAME: fileMap[BleEventConstants.KEY_NAME] as String? ?? '',
BleEventConstants.KEY_PATH: fileMap[BleEventConstants.KEY_PATH] as String? ?? '',
};
}).toList());
}
启动时恢复上次选中的固件路径
选中路径通过 FilePreferenceManager 持久化,应用启动时异步恢复:
FilePreferenceManager.loadOtaPath().then((path) {
setState(() {
_selectedFilePath = path;
});
});
Configuration Options
固件文件管理没有独立配置文件,其"配置"体现在事件契约常量与通道名上:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
TYPE_OTA_FILE_LIST | String | 'otaFileList' | 固件文件列表事件类型标识,事件流过滤依据 |
KEY_NAME | String | 'name' | 文件条目中的文件名键 |
KEY_PATH | String | 'path' | 文件条目中的路径键(同时用于选中态同步) |
| 平台通道名 | String | 'com.jieli.ble_plugin/methods' | 页面持有的 MethodChannel 名称,处理非流式方法调用 |
| 选中路径持久化 | 偏好存储 | 无(null) | 由 FilePreferenceManager.loadOtaPath() 在启动时恢复 |
常量定义见 ble_event_constants.dart 与 update_page.dart。
API Reference
OtaFileManager({required List<Map<String, String>> otaFileList, required Function(List<Map<String, String>>) onFileListUpdated, required Function(String?) onSelectedFileChanged})
构造管理器实例,注入共享列表数据与两个状态回调。
参数:
otaFileList(List<Map<String, String>>):与页面共享的固件文件列表引用,页面更新后管理器读取到最新数据;onFileListUpdated(Function(List<Map<String, String>>)):列表整体更新通知,页面在此回调中setState;onSelectedFileChanged(Function(String?)):选中文件变化通知,参数为文件路径,null表示无选中。
Future<void> deleteFile(int index)
按列表索引删除设备上的固件文件。
参数:
index(int):otaFileList中的下标,须与当前列表一致。
行为:
- 调用
BleMethod.deleteOtaIndex(index)下发 BLE 删除命令; - 成功后读取该索引的
KEY_PATH并触发onSelectedFileChanged,实现"删除即取消选中"; - 异常被静默捕获(不影响调用链)。
Throws:
- 不向外抛出:所有异常在方法内部被
catch吞掉(原print已注释)。
Stream<List<Map<String, String>>> otaFileListStream(静态 getter)
暴露固件文件列表事件流,供页面订阅。
返回: 过滤并规范化为 {name, path} 结构的文件列表流;空值字段以空字符串兜底。
Failure Modes、边界情况与并发
删除失败静默处理
deleteFile 的 catch 块为空实现(print 被注释)。这意味着:
- 设备离线、命令超时或 BLE 断连时,删除失败不会抛出异常、不会弹窗,UI 保持原状;
- 设计意图是"容错优先":文件删除属于可重试操作,最终以事件流推送的新列表为准,无需立即打断用户;
- 运维提示:若需要可观测性,应恢复日志输出或接入错误回调,否则失败场景完全不可见。
索引型 API 的并发风险
deleteOtaIndex(index) 按索引删除,而 index 来自 otaFileList 的当前下标。存在两类边界:
- 快速连点删除:用户连续删除多个文件时,每次删除后设备端列表已变化,但本地
otaFileList直到下一次事件流刷新才更新。此时若用旧索引删除第二个文件,可能删错目标或越界。示例代码未对删除操作加互斥锁或索引校验; - 列表与设备不同步:事件流刷新是异步的,删除命令返回与列表刷新之间存在时间窗口,期间
otaFileList[index]可能已过期。
缓解手段(示例代码未实现,属扩展建议):删除期间禁用列表交互、删除成功后主动重新拉取列表、或改用路径定位代替索引定位。
选中态同步的边界逻辑
onSelectedFileChanged 回调在页面侧实现为"相等即置空":
- 删除的恰是当前选中文件 →
_selectedFilePath置null,选中态清空; - 删除的不是当前选中文件 → 选中态保持不变。
该逻辑假设 _selectedFilePath 与设备路径字符串完全一致。若设备路径大小写或格式存在差异,相等比较会失效,需注意路径规范化。
订阅生命周期
_otaFileListSubscription 在 initState 阶段建立(通过 _initialize()),页面销毁时应 cancel 以避免事件流泄漏;本次调研所读代码片段未覆盖 dispose 实现,实际行为以 update_page.dart 完整源码为准。
性能与运维注意事项
- 数据量极小:文件列表是
List<Map<String, String>>,每个条目仅两个短字符串,内存与渲染开销可忽略; - 事件驱动刷新:列表更新完全由 BLE 事件流驱动,无轮询,节省功耗;代价是 UI 状态依赖事件到达的及时性;
- 空值兜底:事件解析层用
as String? ?? ''兜底,保证下游永远不会遇到 null 键,避免渲染层空指针; - 跨平台一致性:
libs/ble_event_stream.dart与code/JL_OTA/lib/ble_event_stream.dart内容一致(发布副本与源码副本),修改事件解析时需注意同步两处。
扩展点
- 回调解耦:
onFileListUpdated/onSelectedFileChanged使OtaFileManager不依赖任何 Widget 与 BuildContext,可轻松复用于其他页面或迁移到独立状态管理; - 持久化钩子:
FilePreferenceManager.loadOtaPath()是选中路径持久化的唯一入口,可扩展为多设备多路径(按设备类型分组保存)而无需改动管理器; - 事件契约扩展:设备上报新字段时,只需在
otaFileListStream的映射中增加键(并同步BleEventConstants常量),下游消费方无感知; - 删除策略替换:
BleMethod.deleteOtaIndex(index)是静态方法调用,可替换为按路径删除或批量删除的新底层方法,管理器接口不变。
Tests
本模块的单元测试未在本次调研范围内发现独立测试文件(示例应用以 integration_test/plugin_integration_test.dart 做插件级集成验证)。OtaFileManager 的回调注入设计使其天然可测:构造时注入桩回调即可验证"删除成功后触发选中态回调""异常时静默返回"等行为,无需真实 BLE 设备。
Related Links
- OtaFileManager 源码
- UpdatePage 集成源码
- BleEventStream 事件流
- BleEventConstants 事件常量
- 相关兄弟页面:OTA 升级流程与传输控制(5-x-ota-upgrade)、设备连接与会话管理(5-x-connection)、设置与偏好管理(5-x-settings)