import 'dart:async'; import 'dart:io'; import 'package:device_plugin_interface/device_plugin_interface.dart'; import 'package:recorder_holder/recorder_holder.dart'; import 'package:recorder_holder/recorder_holder_ota.dart'; import '../../core/utils/logger.dart'; import 'holder_device_service.dart'; /// EaiRec 支架(中科蓝讯方案)的固件 OTA 端口 —— AB FOT 协议。 /// /// 协议实现全在 `recorder_holder/lib/src/ota/`(帧格式、CRC、TLV、状态机), /// 本类只做两件事:把它接到通用的 [DeviceOtaPort] 上,以及处理**连接交接**。 /// /// ⚠️ **OTA 期间必须让出常规 BLE 连接**:一部手机对同一个外设只能有一条链路, /// 而 `FotBleTransport` 自己用 flutter_blue_plus 连 OTA 服务(`0xFF12`, /// 与常规的 `abc0`/`ae00`/`FDA5` 是不同的 service,设备重启进 OTA 模式后才暴露)。 /// 所以 [start] 先 `setEnterOta(true)` 让原生放手,[close] 再 `setEnterOta(false)` /// 把设备交还给常规会话。这与恒玄那条 SPP OTA 的交接思路一致。 /// /// ⚠️ 设备进 OTA 模式后**可能换一个蓝牙地址广播**,所以 `knownRemoteId` 只是 /// Android 上的快路径,失败会自动退回按 service UUID 扫描(两端共用那条路径)。 class HolderOtaPort implements DeviceOtaPort { HolderOtaPort(this._link); static const String _tag = 'HolderOtaPort'; final HolderDeviceService _link; /// 原生插件单例。`setEnterOta` 只在插件层暴露(SDK 没转发),直接用它, /// 少一层转发就少一处会忘记同步的地方。 final RecorderHolder _plugin = RecorderHolder(); final StreamController _progress = StreamController.broadcast(); FotBleTransport? _transport; FotOtaSession? _session; StreamSubscription? _sub; bool _running = false; bool _terminal = false; bool _succeeded = false; /// 我们为了 OTA 把常规连接让出去了,[close] 时要还回来 bool _handover = false; int _lastPercent = -1; @override bool get isRunning => _running; @override Stream get progressStream => _progress.stream; @override Future start(DeviceOtaRequest request) async { if (_running) throw DeviceException(DeviceErrorCode.otaBusy); if (request is! DeviceOtaFileRequest) { throw DeviceException( DeviceErrorCode.notSupported, '${request.runtimeType}'); } final file = File(request.filePath); if (!await file.exists()) { throw DeviceException(DeviceErrorCode.otaFileInvalid, request.filePath); } final bytes = await file.readAsBytes(); if (bytes.isEmpty) { throw DeviceException(DeviceErrorCode.otaFileInvalid, '固件为空'); } _running = true; _terminal = false; _succeeded = false; _lastPercent = -1; _emit(DeviceOtaState.entering, 0); // 1) 让出常规连接。原生 setEnterOta(true) 会 disconnect 且不自动重连。 final mac = _link.connectedAddress; _handover = true; await _plugin.setEnterOta(true); // 设备要重启进 OTA 模式并重新广播,给它一点时间;连太早只会白等超时。 await Future.delayed(const Duration(seconds: 2)); if (!_running) return; // 期间被 cancel 了 // 2) 建 OTA 链路(自管连接,与原生那条互斥) final config = FotOtaConfig( knownRemoteId: Platform.isAndroid && mac.isNotEmpty ? mac : null, ); final transport = FotBleTransport(config: config); _transport = transport; final firmware = FotFirmware.fromBytes(bytes); final session = FotOtaSession( transport: transport, firmware: firmware, config: config, ); _session = session; _sub = session.progress.listen(_onProgress, onError: (Object e) { _fail(DeviceErrorCode.otaTransferFailed, e.toString()); }); try { await transport.open(); if (!_running) return; await session.start(); } catch (e) { _fail(DeviceErrorCode.otaTransferFailed, e.toString()); } } void _onProgress(FotProgress p) { // 终态锁定:迟到的事件一律忽略,否则成功会被收尾事件覆盖成失败 if (_terminal) return; switch (p.stage) { case FotStage.connecting: _emit(DeviceOtaState.entering, 0); break; case FotStage.handshaking: case FotStage.querying: _emit(DeviceOtaState.inquiring, 0); break; case FotStage.transferring: _emitPercent(p.percent); break; case FotStage.finishing: case FotStage.verifying: // 压缩包在这一步解压+烧写,可能要几十秒,UI 别当成卡死 _emit(DeviceOtaState.verifying, 100); break; case FotStage.completed: _terminal = true; _succeeded = true; _running = false; _emit(DeviceOtaState.done, 100); break; case FotStage.failed: _fail(DeviceErrorCode.otaTransferFailed, p.message ?? p.deviceStatus?.toString() ?? 'failed'); break; case FotStage.idle: break; } } /// 进度只在单调递增时上报,避免数字来回跳 void _emitPercent(int p) { if (p <= _lastPercent) return; _lastPercent = p; _emit(DeviceOtaState.transferring, p); } void _fail(String code, String message) { if (_terminal) return; _terminal = true; _running = false; Logger.w(_tag, 'OTA 失败: $message'); _emit(DeviceOtaState.failed, -1, errorCode: code, errorMessage: message); } void _emit(DeviceOtaState st, int percent, {String? errorCode, String? errorMessage}) { if (_progress.isClosed) return; _progress.add(DeviceOtaProgress( state: st, sentBytes: 0, totalBytes: 0, percent: percent, tsMs: DateTime.now().millisecondsSinceEpoch, errorCode: errorCode, errorMessage: errorMessage, )); } @override Future cancel() async { if (!_running) return; _running = false; _terminal = true; await _teardownLink(); _emit(DeviceOtaState.cancelled, -1); } /// 会话没了(常规连接断开)。升级中那次断开是我们自己让出的,忽略。 void onSessionGone() { if (_handover) return; if (_running) _fail(DeviceErrorCode.disconnectedRemote, '设备断开'); } Future _teardownLink() async { await _sub?.cancel(); _sub = null; try { _session?.dispose(); } catch (_) {} _session = null; try { await _transport?.close(); } catch (_) {} _transport = null; } @override Future close() async { _running = false; await _teardownLink(); if (!_progress.isClosed) await _progress.close(); if (!_handover) return; _handover = false; // 把设备交还给常规会话。升级成功时设备正在重启,多等几秒。 await _plugin.setEnterOta(false); final delay = _succeeded ? const Duration(seconds: 5) : const Duration(seconds: 2); Future.delayed(delay, () { if (!_link.isConnected) _link.tryAutoConnect(); }); } }