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.
 
 
 
 
 
 

159 lines
5.2 KiB

import 'dart:async';
import 'package:device_plugin_interface/device_plugin_interface.dart';
import '../../core/utils/logger.dart';
import 'eaicar_device_plugin.dart';
import 'eaicar_link.dart';
/// EaiCar(杰理方案)的 OTA 端口。
///
/// 推送本身在原生 `EaiCarOta`(从 lumi `PbMangerImp` 搬:`cmdOTAData` + 0xfc 后
/// 扫描回连 + 认证 + 续传),这里只做三件事:
/// 1. 把原生事件翻译成 [DeviceOtaProgress];
/// 2. 终态锁定——成功后迟到的 failed/cancelled 不能把它覆盖回去(恒玄踩过);
/// 3. [close]:升级成功设备会重启,退出页面时延时回连记住的设备。
///
/// 与恒玄不同,**这条链路不用断 app 层连接**:OTA 数据走同一条 GATT 的 AE01,
/// 私有通道 ABC1 由原生在升级期间挡掉。stage2 阶段设备会主动断开重启,
/// 那次「断开」是升级流程的一部分,不是失败——由原生判定,这里不看链路状态。
///
/// ⚠️ 上层 `DeviceOtaController` 的看门狗 90s 没进度就判失败,而 stage2 等待可达 3 分钟,
/// 原生在这段窗口每 15s 发一次 `stage2` 心跳事件喂它。
class EaiCarOtaPort implements DeviceOtaPort {
EaiCarOtaPort(this._link, this._plugin);
static const String _tag = 'EaiCarOtaPort';
final EaiCarLink _link;
final EaiCarDevicePlugin _plugin;
final StreamController<DeviceOtaProgress> _progress =
StreamController<DeviceOtaProgress>.broadcast();
StreamSubscription<Map<String, Object?>>? _sub;
bool _running = false;
bool _terminal = false;
bool _succeeded = false;
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}');
}
if (!_link.isConnected) {
throw DeviceException(DeviceErrorCode.noActiveSession, '链路未就绪');
}
_running = true;
_terminal = false;
_succeeded = false;
_lastPercent = -1;
_sub ??= _link.otaEvents.listen(_onEvent, onError: (Object e) {
_fail(DeviceErrorCode.otaTransferFailed, '$e');
});
_emit(DeviceOtaState.inquiring, 0);
try {
await _link.otaStart(request.filePath);
} catch (e) {
_fail(DeviceErrorCode.otaTransferFailed, '$e');
rethrow;
}
}
void _onEvent(Map<String, Object?> e) {
if (_terminal) return;
final state = '${e['state']}';
final percent = (e['percent'] as num?)?.toInt() ?? -1;
switch (state) {
case 'preparing':
_emit(DeviceOtaState.entering, 0);
break;
case 'transferring':
// 只在单调递增时上报,SDK 的 preparing/prepared 会把百分比打回 0
if (percent > _lastPercent) {
_lastPercent = percent;
_emit(DeviceOtaState.transferring, percent);
}
break;
case 'stage2':
// 设备断开重启进 stage2,原生在扫描回连;进度条停在 15% 但要持续有事件喂看门狗
Logger.i(_tag, 'stage2 等待回连: ${e['message'] ?? ''}');
_emit(DeviceOtaState.rebooting, _lastPercent < 15 ? 15 : _lastPercent);
break;
case 'done':
_terminal = true;
_succeeded = true;
_running = false;
_emit(DeviceOtaState.done, 100);
break;
case 'cancelled':
_terminal = true;
_running = false;
_emit(DeviceOtaState.cancelled, -1);
break;
case 'failed':
_fail(DeviceErrorCode.otaTransferFailed,
'${e['message'] ?? ''} (code=${e['code']})');
break;
}
}
void _fail(String code, String message) {
if (_terminal) return;
_terminal = true;
_running = false;
_emit(DeviceOtaState.failed, -1, errorCode: code, errorMessage: message);
}
@override
Future<void> cancel() async {
if (!_running) return;
await _link.otaCancel();
// 原生会回 cancelled;5s 内没回就自己收尾(契约要求)
Future.delayed(const Duration(seconds: 5), () {
if (!_terminal) {
_terminal = true;
_running = false;
_emit(DeviceOtaState.cancelled, -1);
}
});
}
@override
Future<void> close() async {
if (_running) await cancel();
await _sub?.cancel();
_sub = null;
// 升级成功设备会重启,广播回来要几秒;失败/取消时原生已把链路断开,一样回连
final delay = _succeeded ? const Duration(seconds: 8) : const Duration(seconds: 2);
Future.delayed(delay, () async {
try {
await _plugin.tryAutoConnect();
} catch (e) {
Logger.w(_tag, 'OTA 后回连失败: $e');
}
});
}
void _emit(DeviceOtaState s, int percent,
{String? errorCode, String? errorMessage}) {
if (_progress.isClosed) return;
_progress.add(DeviceOtaProgress(
state: s,
sentBytes: 0,
totalBytes: 0,
percent: percent,
tsMs: DateTime.now().millisecondsSinceEpoch,
errorCode: errorCode,
errorMessage: errorMessage,
));
}
}