杰理 SDK 文档中心
首页
首页
  • 概览与快速开始

    • 项目概述与能力总览
    • 快速开始与 SDK 集成
  • SDK 核心接口

    • 发送接口 BleMethod
    • 接收接口 BleEventStream
    • 数据模型与常量定义
  • 平台原生实现

    • Android 原生层
    • iOS 原生层架构
    • iOS 蓝牙管理与 SDK 运行
    • 辅助连接与广播音箱
  • OTA 升级功能

    • 升级流程与传输通道
    • 自动回连机制
    • 复用空间升级
    • 自定义命令
  • 示例应用

    • 页面结构与用户旅程
    • 设备扫描与连接管理
    • 固件文件管理
    • 升级执行与状态展示
    • 设置与调试
  • 文档与支持

    • 接口文档与收发说明
    • 调试与问题排查

固件文件管理

固件文件管理是 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 与设备通信,设备将自身的固件文件清单以事件形式上报,应用负责:

  1. 订阅文件列表事件:监听插件广播的 otaFileList 类型事件,解析出 {name, path} 结构的文件列表;
  2. 维护选中状态:记录用户当前选中的固件文件路径,作为后续 OTA 升级操作的输入;
  3. 删除设备端文件:调用底层 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&lt;Map&lt;String,String&gt;&gt;"| 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");
    }
  }
}

来源:ota_file_manager.dart

设计要点:

  • 数据是"注入"而非"内部持有":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 之间的管道细节,以上为实际观测到的关键行)

该流做了两件事:

  1. 按类型过滤:只保留 TYPE == 'otaFileList' 的事件,避免其他 BLE 事件(连接状态、升级进度等)混入;
  2. 字段规范化:把设备上报的原始 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;

@override
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;
        }
      });
    },
  );
  // ...
}

来源:update_page.dart

关键行为:

  • 启动时恢复选中路径: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 刷新

流程要点

  1. 删除按索引定位:deleteOtaIndex(index) 使用列表下标,调用方必须保证 index 与当前 otaFileList 一致;
  2. 删除后立即同步选中态:管理器在删除命令返回后马上读取原列表该索引的 path 并通知回调——此时设备端已删除该文件,UI 侧将选中路径清空,避免"选中一个不存在的文件";
  3. 最终一致性由事件流兜底:无论删除成功与否,设备都会(在成功时)再次广播文件列表,页面最终会以事件流数据为准刷新,因此删除与选中态的更新是"乐观 + 校正"的组合。

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
}

来源:update_page.dart

删除设备上的固件文件

删除操作完全封装在 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");
  }
}

来源:ota_file_manager.dart

消费文件列表事件流

需要独立获取固件文件列表(不依赖页面)时,可直接订阅插件暴露的事件流:

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

启动时恢复上次选中的固件路径

选中路径通过 FilePreferenceManager 持久化,应用启动时异步恢复:

FilePreferenceManager.loadOtaPath().then((path) {
  setState(() {
    _selectedFilePath = path;
  });
});

来源:update_page.dart

Configuration Options

固件文件管理没有独立配置文件,其"配置"体现在事件契约常量与通道名上:

选项类型默认值说明
TYPE_OTA_FILE_LISTString'otaFileList'固件文件列表事件类型标识,事件流过滤依据
KEY_NAMEString'name'文件条目中的文件名键
KEY_PATHString'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 的当前下标。存在两类边界:

  1. 快速连点删除:用户连续删除多个文件时,每次删除后设备端列表已变化,但本地 otaFileList 直到下一次事件流刷新才更新。此时若用旧索引删除第二个文件,可能删错目标或越界。示例代码未对删除操作加互斥锁或索引校验;
  2. 列表与设备不同步:事件流刷新是异步的,删除命令返回与列表刷新之间存在时间窗口,期间 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)
Prev
设备扫描与连接管理
Next
升级执行与状态展示