设备扫描与连接管理
JL_OTA_Flutter 示例应用中负责 BLE 设备发现(扫描)与连接生命周期管理的能力模块。它通过 BleMethod(MethodChannel)向原生端下发扫描/连接指令,并通过 BleEventStream(EventChannel)接收扫描结果与连接状态事件,最终由 OtaConnectionManager 消费连接事件并联动清理 OTA 会话数据。
Purpose and Scope
本页面向开发者完整说明「设备扫描与连接管理」这一子系统的实现机制,覆盖:
- 扫描指令的发起与停止(
BleMethod.startScan/BleMethod.stopScan) - 扫描结果与扫描状态的推送通道(
BleEventStream.scanDeviceListStream/scanStateStream) - 设备连接与断开的指令(
BleMethod.connectDevice/BleMethod.disconnectBtDevice) - 连接状态事件的消费(
BleEventStream.deviceConnectionStream) - OTA 连接管理器如何订阅上述事件并触发数据清理(
OtaConnectionManager)
以下内容属于兄弟页面,不在本页展开:OTA 升级流程本身、固件文件管理(ota_file_manager.dart)、各类对话框(dialog/ 目录)、设置管理(setting_manager.dart)。本页只关注「从扫描到连接成功、再到连接断开」这条链路。
Overview
在 JL_OTA 的示例应用架构中,蓝牙能力由原生插件(com.jieli.ble_plugin)提供,Flutter 侧通过两条标准化通道与原生端通信:
- MethodChannel(指令通道):
com.jieli.ble_plugin/methods,由BleMethod封装。所有"动作"类调用——检查蓝牙环境、开始/停止扫描、连接/断开设备、设置扫描过滤与通讯方式——都以静态方法形式下发到原生端。 - EventChannel(事件通道):
com.jieli.ble_plugin/events,由BleEventStream封装。原生端主动推送的"状态"类数据——蓝牙开关状态、扫描状态、扫描设备列表、设备连接状态、OTA 连接状态——都以广播流形式暴露给 UI 层。
这一"指令/事件分离"的设计意图在于:扫描与连接是异步的、由原生 BLE 栈驱动的过程,Flutter 侧无法也不应轮询设备状态,而是通过事件流被动接收。BleEventStream.baseStream 采用单例模式(??= 只初始化一次),保证整个应用共享同一条原生广播流,避免重复订阅造成的事件丢失或资源浪费。
连接成功后,OtaConnectionManager 订阅设备连接流与 OTA 连接流,一旦设备断开(状态为 AppConstants.connectionDisconnect)或 OTA 连接事件触发,立即调用 onOtaDataCleaned 回调,将上一轮 OTA 会话的临时数据清空——这是保证"设备换了、旧数据不能残留"的关键联动逻辑。
Architecture
flowchart TD
subgraph sg_Flutter["Flutter 应用层"]
UI["UI 界面 / 业务逻辑"]
ConnMgr["OtaConnectionManager"]
end
subgraph sg_Plugin["BLE 插件封装层 (libs/)"]
BleMethod["BleMethod<br/>MethodChannel 指令封装"]
BleEventStream["BleEventStream<br/>EventChannel 事件流"]
BaseStream["baseStream 单例广播流"]
end
subgraph sg_Native["原生 Android 插件"]
Native["com.jieli.ble_plugin<br/>原生 BLE 实现"]
BLE["BLE 硬件 / 蓝牙栈"]
end
UI -->|"startScan / connectDevice<br/>/ disconnectBtDevice"| BleMethod
BleMethod -->|"invokeMethod"| Native
Native -->|"扫描结果 / 连接状态<br/>广播事件"| BaseStream
BaseStream --> BleEventStream
BleEventStream -->|"scanDeviceListStream<br/>deviceConnectionStream"| UI
BleEventStream -->|"otaConnectionStream<br/>deviceConnectionStream"| ConnMgr
ConnMgr -->|"onOtaDataCleaned"| UI
Native <-->|"GATT / 扫描回调"| BLE
架构要点:
- 指令下行:
UI层只依赖BleMethod的静态方法,不直接接触 MethodChannel,屏蔽了平台通道细节;方法内部统一捕获PlatformException并rethrow,保证错误向上层传播且可被调用方感知。 - 事件上行:原生端把扫描设备列表、连接状态等事件推入 EventChannel;
BleEventStream用where+map组合子按KEY_TYPE过滤出各语义流(扫描列表流、连接状态流等),并对Map数据执行ScanDevice.fromMap/DeviceConnection.fromMap反序列化,UI 层拿到的是强类型对象而非原始 Map。 - 会话联动:
OtaConnectionManager是两个订阅的汇聚点——设备连接流(判断断开)与 OTA 连接流(判断 OTA 会话变化)任一触发清理逻辑,确保 OTA 数据与当前设备绑定。
扫描流程详解
设备扫描是「发现可连接设备」的第一步。Flutter 侧只负责下发指令与接收结果,实际的 BLE 扫描由原生插件驱动。
扫描的发起与停止
BleMethod 提供三个与扫描直接相关的静态方法:
// 检查是否正在扫描
static Future<bool> isScanning() async {
try {
return await _methodChannel.invokeMethod(
BleMethodConstants.METHOD_IS_SCANNING,
) ??
false;
} on PlatformException catch (e) {
print("Failed to check if scanning: ${e.message}");
rethrow;
}
}
// 开始扫描
static Future<void> startScan() async {
try {
await _methodChannel.invokeMethod(BleMethodConstants.METHOD_START_SCAN);
} on PlatformException catch (e) {
print("Failed to start scan: ${e.message}");
rethrow;
}
}
// 停止扫描
static Future<void> stopScan() async {
try {
await _methodChannel.invokeMethod(BleMethodConstants.METHOD_STOP_SCAN);
} on PlatformException catch (e) {
print("Failed to stop scan: ${e.message}");
rethrow;
}
}
Source: ble_method.dart
设计意图:
- 扫描是"发后不管"的异步操作。
startScan()立即返回,扫描到的设备通过事件流持续推送,因此调用方不需要等待任何结果,只需在 UI 生命周期合适时机(如页面进入/退出)调用 start/stop 配对。 - 幂等检查。
isScanning()用于在 UI 层避免重复启动扫描(例如下拉刷新时先检查),返回值?? false意味着原生端异常时按"未扫描"处理,不会阻塞界面。 - 统一错误出口。每个方法都捕获
PlatformException打印日志后rethrow,让上层(页面或管理器)可以决定是提示用户还是静默处理。
扫描过滤
原生端维护一个"过滤字符串",用于在扫描时只上报名称或地址匹配的设备,减少无效事件:
// 读取当前过滤字符串
static Future<String?> getScanFilter() async {
try {
return await _methodChannel.invokeMethod<String>(
BleMethodConstants.METHOD_GET_SCAN_FILTER,
);
} on PlatformException catch (e) {
print("Failed to get scan filter: ${e.message}");
rethrow;
}
}
// 设置新的过滤字符串
static Future<void> setScanFilter(String? filter) async {
try {
await _methodChannel.invokeMethod(
BleMethodConstants.METHOD_SET_SCAN_FILTER,
{BleMethodConstants.ARG_FILTER: filter},
);
} on PlatformException catch (e) {
print("Failed to set scan filter: ${e.message}");
rethrow;
}
}
Source: ble_method.dart
过滤逻辑发生在原生侧,而非 Flutter 侧,这样扫描结果从源头就被裁剪,降低事件通道的负载与 UI 层的过滤成本。示例应用中的 device_filter_dialog.dart 即为该能力的 UI 入口。
扫描结果接收
原生端扫描到设备后,通过 EventChannel 推送类型为 TYPE_SCAN_DEVICE_LIST 的事件,BleEventStream 将其转换为强类型流:
// 扫描设备列表流
static Stream<List<ScanDevice>> get scanDeviceListStream {
return baseStream
.where((event) => event is Map && event[BleEventConstants.KEY_TYPE] == BleEventConstants.TYPE_SCAN_DEVICE_LIST)
.map((event) {
final list = event[BleEventConstants.KEY_VALUE][BleEventConstants.KEY_LIST] as List? ?? [];
return list
.whereType<Map>()
.map((deviceMap) => ScanDevice.fromMap(deviceMap))
.toList();
});
}
Source: ble_event_stream.dart
转换管线分三步:先按事件类型过滤,再从 KEY_VALUE.KEY_LIST 提取设备数组,最后逐个 ScanDevice.fromMap 反序列化。?? [] 与 whereType<Map>() 的防御性处理保证原生端返回异常结构时流不会崩溃,而是产出空列表——扫描页面据此刷新为"暂无设备"状态。每次事件推送的都是完整列表(而非增量),因此 UI 层订阅后直接整体替换即可,天然规避了增量合并的竞态问题。
连接管理详解
连接管理负责把扫描列表中的设备建立/断开 GATT 连接,并把连接状态反馈给业务层。
连接与断开指令
// 连接设备
static Future<void> connectDevice(int index) async {
try {
await _methodChannel.invokeMethod(
BleMethodConstants.METHOD_CONNECT_DEVICE,
{BleMethodConstants.ARG_INDEX: index},
);
} on PlatformException catch (e) {
print("Failed to connect device at index $index: ${e.message}");
rethrow;
}
}
// 断开设备连接
static Future<void> disconnectBtDevice(int index) async {
try {
await _methodChannel.invokeMethod(
BleMethodConstants.METHOD_DISCONNECT_BT_DEVICE,
{BleMethodConstants.ARG_INDEX: index},
);
} on PlatformException catch (e) {
print("Failed to disconnect device at index $index: ${e.message}");
rethrow;
}
}
Source: ble_method.dart
关键设计:按索引寻址而非按地址寻址。 两个方法都以扫描列表中的 index 为参数,这意味着调用方必须持有与原生端一致的设备列表顺序。示例应用的扫描页面将 scanDeviceListStream 的列表顺序视为唯一事实来源,用户点击列表项时直接把该行索引传给 connectDevice。这样的好处是:原生端不需要暴露完整的设备地址/名称协议,Flutter 侧只传一个 int,通道载荷极小;代价是两端列表必须严格同步——这也是为什么扫描列表采用"整表推送"而非增量推送。
连接状态接收
// 设备连接状态流
static Stream<DeviceConnection> get deviceConnectionStream {
return baseStream
.where((event) {
return event is Map && event[BleEventConstants.KEY_TYPE] == BleEventConstants.TYPE_DEVICE_CONNECTION;
})
.map((event) {
final data = event[BleEventConstants.KEY_VALUE];
return DeviceConnection.fromMap(data);
});
}
Source: ble_event_stream.dart
原生端在连接状态变化(连接中、已连接、断开等)时推送 TYPE_DEVICE_CONNECTION 事件,DeviceConnection 对象携带 state 字段。业务层通过 AppConstants.connectionDisconnect 常量判定"已断开",从而触发后续清理逻辑(见下文 OtaConnectionManager)。
通讯方式与 SDK 蓝牙开关
BleMethod 还暴露了连接相关的运行时配置读写接口,用于切换通讯方式(如 BLE 经典/低功耗)以及是否使用 SDK 自带的蓝牙连接实现:
// 读取当前使用的通讯方式
static Future<int> getConnectWay() async {
try {
return await _methodChannel.invokeMethod(
BleMethodConstants.METHOD_GET_CONNECT_WAY,
) ??
true;
} on PlatformException catch (e) {
print("Failed to check if BLE way is used: ${e.message}");
rethrow;
}
}
// 设置当前使用的通讯方式
static Future<void> setConnectWay(int connectWay) async {
await _methodChannel.invokeMethod(BleMethodConstants.METHOD_SET_CONNECT_WAY, {
BleMethodConstants.ARG_CONNECT_WAY: connectWay,
});
}
Source: ble_method.dart
这些配置属于持久化的连接偏好,通常在进入扫描页面前由设置管理(setting_manager.dart)读取并写入,扫描与连接行为随后按此偏好执行。注意 getConnectWay 的兜底值写法为 ?? true(int 类型的布尔兜底),这是插件封装层的一个既有实现细节,语义上表示"原生端无返回时默认走 BLE 方式"。
OtaConnectionManager:连接事件与会话数据的联动
OtaConnectionManager 是连接管理链路在业务侧的核心消费者。它不直接发起扫描或连接,而是监听两个事件流,把「连接生命周期」翻译成「OTA 会话数据清理」动作:
/// Manages OTA update connections and device connections.
class OtaConnectionManager {
final Function onOtaDataCleaned;
StreamSubscription<Map<String, dynamic>>? _otaConnectionSubscription;
StreamSubscription<DeviceConnection>? _deviceConnectionSubscription;
OtaConnectionManager({required this.onOtaDataCleaned});
void subscribeToOtaConnectionStream() {
_otaConnectionSubscription?.cancel();
_otaConnectionSubscription = BleEventStream.otaConnectionStream.listen((otaData) {
onOtaDataCleaned();
});
}
void subscribeToDeviceConnectionStream() {
_deviceConnectionSubscription?.cancel();
_deviceConnectionSubscription = BleEventStream.deviceConnectionStream.listen((connection) {
if (connection.state == AppConstants.connectionDisconnect) {
onOtaDataCleaned();
}
}) as StreamSubscription<DeviceConnection>?;
}
void dispose() {
_otaConnectionSubscription?.cancel();
_deviceConnectionSubscription?.cancel();
}
}
Source: ota_connection_manager.dart
设计意图逐点拆解:
- 职责单一。管理器只做"事件 → 回调"的翻译:OTA 连接事件一到就清理(表示进入了新的 OTA 会话),设备断开事件一到也清理(表示当前会话的设备已离开)。实际的数据清理由外部注入的
onOtaDataCleaned回调完成,管理器不感知 OTA 数据的具体形态,便于替换与测试。 - 重复订阅保护。每个
subscribeToXxxStream先cancel()旧订阅再重新订阅,防止页面重建或重复初始化时产生多个监听者导致回调多次触发。 - 状态过滤。设备连接流里只对
connectionDisconnect状态响应,忽略连接中/已连接等中间态——因为只有断开才意味着"上一轮 OTA 数据必须失效"。 - 资源释放。
dispose()成对取消两个订阅,与订阅方法对称,避免内存泄漏。
这一联动解决了真实场景中的关键一致性问题:用户给设备 A 做完 OTA 后断开,再连接设备 B 时,若不清理,残留的升级进度、临时文件或日志会串到设备 B 的会话中。连接断开即清理,从根上保证了「一个设备一个会话」。
Core Flow:扫描 → 连接 → 状态回传
sequenceDiagram
participant UI as 扫描页面 / 业务层
participant BM as BleMethod (MethodChannel)
participant Native as 原生 BLE 插件
participant ES as BleEventStream (EventChannel)
participant OCM as OtaConnectionManager
UI->>BM: checkBluetoothEnvironment()
BM-->>UI: 环境可用
UI->>BM: startScan()
BM->>Native: METHOD_START_SCAN
Native->>Native: 底层 BLE 扫描
Native->>ES: TYPE_SCAN_DEVICE_LIST 事件(整表)
ES-->>UI: scanDeviceListStream 推送 List<ScanDevice>
UI->>UI: 渲染设备列表
UI->>BM: connectDevice(index)
BM->>Native: METHOD_CONNECT_DEVICE (index)
Native->>Native: GATT 连接流程
Native->>ES: TYPE_DEVICE_CONNECTION 事件
ES-->>UI: deviceConnectionStream 推送 DeviceConnection
ES-->>OCM: deviceConnectionStream 推送 DeviceConnection
OCM->>OCM: state == connectionDisconnect ?<br/>onOtaDataCleaned() : 忽略
UI->>BM: disconnectBtDevice(index) 或 stopScan()
流程说明:
- 进入扫描页前先调用
checkBluetoothEnvironment()确认蓝牙可用,失败时引导用户开启蓝牙(对应事件流中的bluetoothStateStream也用于监听运行时开关状态)。 startScan()下发指令后,原生端持续扫描,每次结果变化都以完整列表形式推送TYPE_SCAN_DEVICE_LIST事件。scanDeviceListStream反序列化出List<ScanDevice>,UI 层整体替换列表渲染。- 用户点击某一项,把该行
index传给connectDevice(index);原生端建立 GATT 连接。 - 连接状态变化通过
TYPE_DEVICE_CONNECTION事件广播,deviceConnectionStream同时被 UI 层与OtaConnectionManager订阅——UI 更新界面状态,管理器判断是否为断开事件并决定是否清理 OTA 数据。 - 页面退出或主动断开时调用
disconnectBtDevice(index)/stopScan(),指令侧与事件侧形成完整闭环。
Usage Examples
示例一:启动扫描并订阅设备列表
以下模式取自示例应用的扫描页典型用法——先检查环境,再启动扫描,同时订阅设备列表流:
// 检查蓝牙环境是否可用
final bool isAvailable = await BleMethod.checkBluetoothEnvironment();
if (isAvailable) {
await BleMethod.startScan();
}
// 订阅扫描设备列表(整表推送,直接替换)
BleEventStream.scanDeviceListStream.listen((devices) {
// 用 devices 刷新列表 UI
});
Source: ble_method.dart 与 ble_event_stream.dart
示例二:按索引连接设备
// 用户点击设备列表第 index 项
await BleMethod.connectDevice(index);
// 通过连接状态流感知结果
BleEventStream.deviceConnectionStream.listen((connection) {
if (connection.state == AppConstants.connectionDisconnect) {
// 设备断开:刷新 UI 为未连接状态
}
});
Source: ble_method.dart 与 ble_event_stream.dart
示例三:连接管理器的订阅与释放
final manager = OtaConnectionManager(onOtaDataCleaned: () {
// 清空上一轮 OTA 的临时文件与进度数据
});
manager.subscribeToOtaConnectionStream();
manager.subscribeToDeviceConnectionStream();
// ... 页面销毁时
manager.dispose();
Source: ota_connection_manager.dart
API Reference
BleMethod(指令封装,静态方法)
| 方法签名 | 说明 | 平台方法/参数 |
|---|---|---|
Future<bool> isScanning() | 查询原生端是否正在扫描,失败兜底 false | METHOD_IS_SCANNING |
Future<bool> checkBluetoothEnvironment() | 检查蓝牙环境(开关、权限)是否可用 | METHOD_CHECK_BLUETOOTH_ENVIRONMENT |
Future<void> startScan() | 启动 BLE 扫描,异步返回,结果走事件流 | METHOD_START_SCAN |
Future<void> stopScan() | 停止 BLE 扫描 | METHOD_STOP_SCAN |
Future<String?> getScanFilter() | 读取当前扫描过滤字符串 | METHOD_GET_SCAN_FILTER |
Future<void> setScanFilter(String? filter) | 设置扫描过滤字符串 | METHOD_SET_SCAN_FILTER,参数 ARG_FILTER |
Future<void> connectDevice(int index) | 按扫描列表索引连接设备 | METHOD_CONNECT_DEVICE,参数 ARG_INDEX |
Future<void> disconnectBtDevice(int index) | 按索引断开设备 | METHOD_DISCONNECT_BT_DEVICE,参数 ARG_INDEX |
Future<int> getConnectWay() | 读取当前通讯方式 | METHOD_GET_CONNECT_WAY |
Future<void> setConnectWay(int connectWay) | 设置通讯方式 | METHOD_SET_CONNECT_WAY,参数 ARG_CONNECT_WAY |
Future<bool> isUseSdkBluetooth() | 是否使用 SDK 蓝牙连接实现 | METHOD_IS_USING_SDK_BLUETOOTH |
Future<void> setConnectUsingSdkBluetooth(bool isUsingSDKBluetooth) | 设置是否使用 SDK 蓝牙连接 | METHOD_SET_USING_SDK_BLUETOOTH,参数 ARG_IS_USING_SDK_BLUETOOTH |
Throws(所有方法): PlatformException —— 原生端调用失败时抛出,封装层打印日志后 rethrow,由调用方决定如何处理。
BleEventStream(事件封装,静态流属性)
| 流属性 | 返回类型 | 事件类型 | 说明 |
|---|---|---|---|
baseStream | Stream<dynamic> | — | 单例原始广播流,所有语义流的数据源 |
bluetoothStateStream | Stream<bool> | TYPE_BLUETOOTH_STATE | 蓝牙开关状态变化 |
scanStateStream | Stream<String> | TYPE_SCAN_DEVICE_LIST | 扫描状态(如扫描中/停止) |
scanDeviceListStream | Stream<List<ScanDevice>> | TYPE_SCAN_DEVICE_LIST | 扫描设备完整列表(整表推送) |
deviceConnectionStream | Stream<DeviceConnection> | TYPE_DEVICE_CONNECTION | 设备连接状态(含 state 字段) |
otaConnectionStream | Stream<Map<String, dynamic>> | TYPE_OTA_CONNECTION | OTA 连接状态(含 state、deviceType) |
OtaConnectionManager(会话联动管理器)
| 方法 | 说明 |
|---|---|
OtaConnectionManager({required Function onOtaDataCleaned}) | 构造器,注入清理回调 |
void subscribeToOtaConnectionStream() | 订阅 OTA 连接流,每次事件触发 onOtaDataCleaned() |
void subscribeToDeviceConnectionStream() | 订阅设备连接流,仅当 state == connectionDisconnect 时触发清理 |
void dispose() | 取消全部订阅,释放资源 |
Configuration Options
扫描与连接行为没有独立的配置文件,运行时偏好通过 BleMethod 的 setter 下发并在原生端持久化:
| 配置项 | 类型 | 默认/兜底 | 说明 |
|---|---|---|---|
| 扫描过滤字符串 | String? | null(不过滤) | setScanFilter 设置,原生端按名称/地址过滤扫描结果 |
| 通讯方式 | int | ?? true(原生无返回时默认 BLE) | setConnectWay 设置,决定连接使用的蓝牙传输方式 |
| 是否使用 SDK 蓝牙 | bool | ?? true | setConnectUsingSdkBluetooth 设置,切换 SDK 自带连接实现与系统默认连接 |
Failure Modes, Edge Cases & Concurrency
PlatformException 与重抛策略
BleMethod 的所有方法在 PlatformException 时打印日志并 rethrow。这意味着:
- 调用方必须捕获异常,否则会冒泡为未处理异常。扫描页通常在进入时对
checkBluetoothEnvironment做 try/catch 并引导用户处理。 - 打印日志只做诊断,不吞错误——错误语义完整保留给上层。
扫描/连接的异步竞态
- 整表推送规避增量竞态:扫描列表每次推送完整列表,UI 直接替换,不会出现两次增量事件交错导致列表错乱。
- 索引寻址的前提:
connectDevice(index)依赖列表顺序与原生端一致。若用户在扫描停止后仍持有旧索引,或列表被新扫描结果替换,索引可能指向错误设备甚至越界——示例应用通过"停止扫描后才允许点击连接"或"每次列表刷新重置选中态"来规避。 - 重复订阅:
OtaConnectionManager的订阅方法先cancel再订阅;若业务代码不遵守该模式(如页面热重载反复 listen),回调可能重复触发,清理逻辑执行多次。虽无害(清理是幂等的),但属于应避免的模式。
连接断开的边界
- 设备物理断开(走远、关机)时原生端推送断开事件,
OtaConnectionManager据此清理 OTA 数据;若原生端未及时上报,UI 可能短暂停留在"已连接"假象——deviceConnectionStream的被动性决定了该延迟无法完全消除。 - 蓝牙全局关闭时,
bluetoothStateStream推送false,UI 应同时停止扫描并置灰连接入口,避免connectDevice在不可用环境下发。
Performance & Operational Notes
- 事件通道为共享单例:
baseStream只初始化一次,多个订阅者共享同一原生流,无重复注册开销;但订阅者不使用时必须取消订阅,否则会持续接收事件。 - 扫描结果负载:扫描列表整表推送,设备数量大时单次事件载荷较高。可通过
setScanFilter在原生侧裁剪结果,这是推荐的降载手段。 - 连接状态事件频率:连接过程会产生多次状态变化事件,
OtaConnectionManager通过状态过滤只关心断开态,业务 UI 也应按需订阅、及时取消,避免不必要的重建。
Extension Points
- 新增连接相关指令:在原生插件 MethodChannel 注册新方法名,并在
BleMethod中增加静态方法封装,保持"指令走 Method、事件走 EventChannel"的既有约定。 - 扩展设备模型:
ScanDevice/DeviceConnection的fromMap是反序列化唯一入口,新增字段只需在模型中补充映射,事件流管线无需改动。 - 替换清理逻辑:
OtaConnectionManager.onOtaDataCleaned为注入式回调,可在示例应用外层替换为自定义清理策略(如保留断点续传数据)而不修改管理器本身。
Related Links
- BLE 事件常量定义(事件类型与键名来源)
- BLE 方法常量定义(MethodChannel 方法名与参数键来源)
- BLE 插件原生实现(
com.jieli.ble_plugin原生侧扫描/GATT 逻辑) - 设备过滤对话框(扫描过滤功能的 UI 入口)
- 同一示例应用的兄弟页面:OTA 升级流程、文件管理(
ota_file_manager.dart)、设置管理(setting_manager.dart)——这些能力在各自目录页中单独说明。