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