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.
 
 
 
 
 
 

222 lines
7.0 KiB

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<DeviceOtaProgress> _progress =
StreamController<DeviceOtaProgress>.broadcast();
FotBleTransport? _transport;
FotOtaSession? _session;
StreamSubscription<FotProgress>? _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<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}');
}
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<void> 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<void> _teardownLink() async {
await _sub?.cancel();
_sub = null;
try {
_session?.dispose();
} catch (_) {}
_session = null;
try {
await _transport?.close();
} catch (_) {}
_transport = null;
}
@override
Future<void> 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();
});
}
}