import 'dart:async'; import 'package:bes_ota_manager/bes_ota_manager.dart'; import 'package:device_plugin_interface/device_plugin_interface.dart'; import '../../core/utils/logger.dart'; import 'bes_bluetooth_service.dart'; import 'bes_device_plugin.dart'; /// 恒玄 SPP OTA 2.0 端口。 /// /// 恒玄的固件推送由 `bes_ota_manager` 插件**自管**一条 SPP 链路,多数固件同时 /// 只接受一条 SPP,所以: /// 1. [start] 先断开 app 层连接、留 2s 让耳机拆完旧链路,再让插件连; /// 2. 升级中 app 层的「已断开」是我们自己断的,**不能**当成 OTA 掉线; /// 3. [close](退出升级页)先 stopOta 释放插件链路,再延时把 app 层连回来 /// (成功 5s:设备在重启;其余 2s:等耳机拆链路)。 /// /// 看门狗、下载、屏幕常亮这些与厂商无关的事在通用 `OtaUpgradeController` 里。 class BesOtaPort implements DeviceOtaPort { BesOtaPort(this._session, this._link); static const String _tag = 'BesOtaPort'; final BesDeviceSession _session; final BesBluetoothService _link; final BesOtaManager _ota = BesOtaManager(); final StreamController _progress = StreamController.broadcast(); StreamSubscription? _sub; bool _running = false; bool _terminal = false; bool _succeeded = false; /// 我们自己为了 OTA 断开了 app 层连接 bool _handover = false; String _address = ''; String _name = ''; 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}'); } // 断开前先抓住设备标识(disconnect 会清空服务字段) final id = _link.connectedDeviceId.value; _address = id.isNotEmpty ? id : _address; final n = _link.connectedDeviceName.value; _name = n.isNotEmpty ? n : _name; if (_address.isEmpty) { throw DeviceException(DeviceErrorCode.noActiveSession, 'no address'); } _running = true; _terminal = false; _succeeded = false; _lastPercent = -1; _emit(DeviceOtaState.entering, 0); // 1) 释放 app 层 SPP,让插件自管 OTA 连接。 _handover = true; await _link.disconnectDevice(); // disconnect 返回只代表本端 socket 已关,耳机侧链路拆除还要在空中传播, // 立刻连 OTA 会被拒;留 2s 缓冲。 await Future.delayed(const Duration(seconds: 2)); if (!_running) return; // 期间被 cancel 了 // 2) 初始化原生 OTA 环境并订阅事件。 await _ota.init(); _sub ??= _ota.events.listen(_onEvent, onError: (Object e) { _fail(DeviceErrorCode.otaTransferFailed, e.toString()); }); // 3) 启动 SPP OTA 2.0。 _emit(DeviceOtaState.transferring, 0); await _ota.startOta( address: _address, name: _name, filePath: request.filePath, protocol: 'SPP', userFlag: 1, upgradeType: 1, user: 1, ); } void _onEvent(BesOtaEvent e) { // 进入终态即锁定:迟到的进度/Canceled/Error 一律忽略, // 否则成功会被迟到的 Canceled 覆盖成失败。 if (_terminal) return; switch (e.event) { case 'onProgress': _emitPercent(((e.progress ?? 0) * 100).round()); break; case 'onStatus': // OTA_SEND_DATA_PROGRESS 状态消息带原始浮点百分比,其余状态文本只记日志 final numeric = double.tryParse(e.message ?? ''); if (numeric != null && numeric > 0) { _emitPercent(numeric.clamp(0, 100).round()); } else { Logger.d(_tag, 'OTA 状态: ${e.message}'); } break; case 'onSuccess': _terminal = true; _succeeded = true; _running = false; _emit(DeviceOtaState.done, 100); _ota.stopOta(); break; case 'onError': _fail(DeviceErrorCode.otaTransferFailed, e.message?.isNotEmpty == true ? e.message! : 'code=${e.code}'); break; } } /// 进度只在单调递增时上报,避免两个来源(整数 onProgress / 浮点 onStatus) /// 高频交替导致数字来回跳。 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; _emit(DeviceOtaState.failed, -1, errorCode: code, errorMessage: message); // 失败即释放原生会话与 SPP 链路,否则它还占着唯一的 SPP _ota.stopOta(); } 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 _ota.stopOta(); _emit(DeviceOtaState.cancelled, -1); } /// 会话没了(app 层断开)。升级中那次断开是我们自己发起的,忽略。 void onSessionGone() { if (_handover) return; if (_running) _fail(DeviceErrorCode.disconnectedRemote, '设备断开'); } @override Future close() async { _running = false; await _sub?.cancel(); _sub = null; // 先停 OTA 会话释放插件占用的 SPP,再回连 app 层,避免两条连接打架。 try { await _ota.stopOta(); } catch (_) {} _ota.dispose(); if (!_progress.isClosed) await _progress.close(); if (!_handover || _address.isEmpty) return; _handover = false; final delay = _succeeded ? const Duration(seconds: 5) // 设备正在重启 : const Duration(seconds: 2); // 等耳机拆完 OTA 链路再回连 final addr = _address; final name = _name; Future.delayed(delay, () { // 期间可能已(自动/手动)连上,避免重复发起 if (_link.deviceStatus.value != 2) { _link.connectToDevice(addr, name); } }); } /// 供插件在会话对象被替换时判断归属。 bool belongsTo(BesDeviceSession s) => identical(s, _session); }