You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

203 lines
6.5 KiB

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<DeviceOtaProgress> _progress =
StreamController<DeviceOtaProgress>.broadcast();
StreamSubscription<BesOtaEvent>? _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<DeviceOtaProgress> get progressStream => _progress.stream;
@override
Future<void> 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<void> 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<void> 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);
}