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
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);
|
|
}
|
|
|