565 lines
18 KiB
Dart
565 lines
18 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'dart:typed_data';
|
||
|
||
import 'package:easydevice/easydevice.dart';
|
||
import 'package:ef/ef.dart'; // THapp 所在的包
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||
import 'package:http/http.dart' as http;
|
||
import 'package:vbvs_app/component/tool/NewTopSlideNotification.dart';
|
||
import 'package:vbvs_app/component/tool/TopSlideNotification.dart';
|
||
import 'package:vbvs_app/enum/APPDeviceUpgrade.dart';
|
||
import 'package:vbvs_app/pages/mh_page/device/controller/mht_bluetooth_controller.dart';
|
||
import 'package:vbvs_app/pages/mh_page/device/upgrade/tool/device_upgrade_tool.dart';
|
||
|
||
// 固件升级配置(区分平台)
|
||
class UpgradeConfig {
|
||
static const int androidInitialFps = 50;
|
||
static const int androidSubsequentFps = 150;
|
||
static const int androidMaxFrameSize = 120;
|
||
|
||
static const int iosInitialFps = 30;
|
||
static const int iosSubsequentFps = 100;
|
||
static const int iosMaxFrameSize = 100;
|
||
|
||
static const Duration initialPhaseDuration = Duration(seconds: 15);
|
||
|
||
static int get maxFrameSize =>
|
||
Platform.isAndroid ? androidMaxFrameSize : iosMaxFrameSize;
|
||
|
||
static int getFps(Duration elapsedTime) {
|
||
if (elapsedTime <= initialPhaseDuration) {
|
||
return Platform.isAndroid ? androidInitialFps : iosInitialFps;
|
||
} else {
|
||
return Platform.isAndroid ? androidSubsequentFps : iosSubsequentFps;
|
||
}
|
||
}
|
||
|
||
static Duration getFrameInterval(Duration elapsedTime) {
|
||
final fps = getFps(elapsedTime);
|
||
return Duration(milliseconds: (1000 / fps).round());
|
||
}
|
||
}
|
||
|
||
/// 升级任务状态
|
||
class UpgradeTask {
|
||
final String mac;
|
||
final THapp thapp;
|
||
int progress;
|
||
String status;
|
||
Completer<void> completer;
|
||
DateTime startTime;
|
||
DateTime? endTime;
|
||
|
||
UpgradeTask({
|
||
required this.mac,
|
||
required this.thapp,
|
||
this.progress = 0,
|
||
this.status = 'waiting',
|
||
required this.completer,
|
||
required this.startTime,
|
||
});
|
||
}
|
||
|
||
/// 多设备蓝牙固件升级管理器
|
||
class MultiDeviceFirmwareUpdater {
|
||
static final MultiDeviceFirmwareUpdater _instance =
|
||
MultiDeviceFirmwareUpdater._internal();
|
||
factory MultiDeviceFirmwareUpdater() => _instance;
|
||
MultiDeviceFirmwareUpdater._internal();
|
||
|
||
final MHTBlueToothController _btController = Get.find();
|
||
|
||
final Map<String, UpgradeTask> upgradeTasks = {};
|
||
final Map<String, Uint8List> _firmwareDataCache = {};
|
||
final int _maxConcurrentUpgrades = 2;
|
||
int _currentConcurrentUpgrades = 0;
|
||
|
||
Future<void> startUpgrade({
|
||
required THapp thapp,
|
||
required String mac,
|
||
required String firmwareUrl,
|
||
}) async {
|
||
if (upgradeTasks.containsKey(mac)) {
|
||
final existingTask = upgradeTasks[mac]!;
|
||
if (existingTask.status == 'upgrading' ||
|
||
existingTask.status == 'downloading') {
|
||
throw Exception("[蓝牙指令执行日志] 设备 $mac 正在升级中".tr);
|
||
}
|
||
}
|
||
// await sendBlogAndDlogCommands(thapp);
|
||
final completer = Completer<void>();
|
||
final task = UpgradeTask(
|
||
mac: mac,
|
||
thapp: thapp,
|
||
completer: completer,
|
||
startTime: DateTime.now(),
|
||
);
|
||
|
||
upgradeTasks[mac] = task;
|
||
_updateUiProgress(mac, 0, 'waiting');
|
||
|
||
_processUpgradeQueue(thapp);
|
||
|
||
return completer.future;
|
||
}
|
||
|
||
void _processUpgradeQueue(THapp thapp) {
|
||
final waitingTasks =
|
||
upgradeTasks.values.where((task) => task.status == 'waiting').toList();
|
||
|
||
for (final task in waitingTasks) {
|
||
if (_currentConcurrentUpgrades < _maxConcurrentUpgrades) {
|
||
_currentConcurrentUpgrades++;
|
||
_startSingleUpgrade(task, thapp);
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _startSingleUpgrade(UpgradeTask task, THapp thapp) async {
|
||
try {
|
||
task.status = 'downloading';
|
||
|
||
if (!task.thapp.isConnected) {
|
||
throw Exception("[蓝牙指令执行日志] 设备未连接".tr);
|
||
}
|
||
task.thapp.logingStream.listen((log) {
|
||
ef.log("升级日志: $log");
|
||
});
|
||
FlutterBluePlus.setLogLevel(LogLevel.none);
|
||
if (!_firmwareDataCache.containsKey(task.mac)) {
|
||
await _downloadFirmware(task);
|
||
}
|
||
|
||
// await _sendCommand(task.thapp, "blog disable");
|
||
// _sendCommand(task.thapp, "blog disable");
|
||
|
||
await Future.delayed(Duration(milliseconds: 200));
|
||
|
||
task.status = 'upgrading';
|
||
_updateUiProgress(task.mac, 0, 'upgrading');
|
||
|
||
await _sendFirmwareFrames(task);
|
||
|
||
// await _sendCommand(task.thapp, "blog enable");
|
||
await _verifyFirmware(task);
|
||
task.status = 'completed';
|
||
task.endTime = DateTime.now();
|
||
_updateUiProgress(task.mac, 100, 'completed');
|
||
|
||
NewTopSlideNotification.show(
|
||
text: "设备 ${task.mac} 固件升级成功".tr, textColor: Colors.green);
|
||
MHTBlueToothController mhtBlueToothController = Get.find();
|
||
mhtBlueToothController.localUpgradeMac.remove(task.mac);
|
||
mhtBlueToothController.updateAll();
|
||
task.completer.complete();
|
||
} catch (e, stack) {
|
||
ef.log("[蓝牙指令执行日志] 设备 ${task.mac} 升级失败: $e\n$stack");
|
||
|
||
task.status = 'failed';
|
||
task.endTime = DateTime.now();
|
||
_updateUiProgress(task.mac, -1, 'failed');
|
||
|
||
// TopSlideNotification.show(Get.context!,
|
||
// text: "设备 ${task.mac} 升级失败".tr, textColor: Colors.red);
|
||
|
||
task.completer.completeError(e);
|
||
} finally {
|
||
_currentConcurrentUpgrades--;
|
||
_cleanupTask(task.mac);
|
||
_processUpgradeQueue(thapp);
|
||
}
|
||
}
|
||
|
||
Future<void> _downloadFirmware(UpgradeTask task) async {
|
||
try {
|
||
final firmwareUrl = _btController.currentUpgradeUrl;
|
||
if (firmwareUrl == null) {
|
||
throw Exception("[蓝牙指令执行日志] 固件URL为空".tr);
|
||
}
|
||
|
||
final response = await http.get(Uri.parse(firmwareUrl));
|
||
|
||
if (response.statusCode != 200) {
|
||
throw Exception("[蓝牙指令执行日志] 固件下载失败,状态码:${response.statusCode}".tr);
|
||
}
|
||
|
||
_firmwareDataCache[task.mac] = response.bodyBytes;
|
||
} catch (e, stack) {
|
||
ef.log("[蓝牙指令执行日志] 下载固件失败: $e\n$stack");
|
||
throw Exception("[蓝牙指令执行日志] 固件下载失败".tr);
|
||
}
|
||
}
|
||
|
||
Future<void> _sendFirmwareFrames(UpgradeTask task) async {
|
||
final firmwareData = _firmwareDataCache[task.mac];
|
||
if (firmwareData == null) {
|
||
throw Exception("[蓝牙指令执行日志] 固件数据为空".tr);
|
||
}
|
||
|
||
try {
|
||
int _lastLoggedProgress = -1;
|
||
ef.log("[蓝牙指令执行日志] 开始执行 OTA 升级流程");
|
||
|
||
// 调用 THapp 封装的 otaStart 方法
|
||
int result = await task.thapp.otaStart(
|
||
firmwareData,
|
||
chunkSize: UpgradeConfig.maxFrameSize, // 单帧大小
|
||
retryPerChunk: 2, // 每帧重试次数
|
||
timeoutMs: 3000, // 每帧超时时间
|
||
firststepsframes: 300, // 前300帧延时发送,防止过载
|
||
firststepdelayms: 30, // 前300帧延迟 30ms
|
||
onceDelayms: 10, // 之后帧间延迟
|
||
disableBlog: true, // 升级时关闭日志
|
||
withResponse: false, // 一般OTA不需要响应确认
|
||
binmode: false, // 使用文本命令模式发送(mmx命令)
|
||
onProgress: (double progress) {
|
||
// 更新 UI 进度
|
||
final percent = (progress * 100).clamp(0, 100).round();
|
||
_updateUiProgress(task.mac, percent, 'upgrading');
|
||
if (percent != _lastLoggedProgress) {
|
||
ef.log("[升级进度]: ${percent}%");
|
||
}
|
||
_lastLoggedProgress = percent;
|
||
},
|
||
);
|
||
|
||
// 根据返回码判断状态
|
||
switch (result) {
|
||
case 0:
|
||
ef.log("[蓝牙指令执行日志] OTA 升级完成 ✅");
|
||
_updateUiProgress(task.mac, 100, 'completed');
|
||
break;
|
||
case -1:
|
||
throw Exception("固件为空或格式错误");
|
||
case -2:
|
||
throw Exception("数据发送失败");
|
||
case -3:
|
||
throw Exception("日志操作失败");
|
||
case -4:
|
||
throw Exception("vota 命令执行失败");
|
||
default:
|
||
throw Exception("未知错误: $result");
|
||
}
|
||
} catch (e, stack) {
|
||
ef.log("[蓝牙指令执行日志] OTA 升级失败: $e\n$stack");
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> _sendAllFirmwareFrames(UpgradeTask task) async {
|
||
final firmwareData = _firmwareDataCache[task.mac];
|
||
if (firmwareData == null) {
|
||
throw Exception("[蓝牙指令执行日志] 固件数据为空".tr);
|
||
}
|
||
|
||
try {
|
||
ef.log("[蓝牙指令执行日志] 开始执行 OTA 升级流程");
|
||
|
||
// 调用 THapp 封装的 otaStart 方法
|
||
int result = await task.thapp.otaStart(
|
||
firmwareData,
|
||
chunkSize: UpgradeConfig.maxFrameSize, // 单帧大小
|
||
retryPerChunk: 2, // 每帧重试次数
|
||
timeoutMs: 3000, // 每帧超时时间
|
||
firststepsframes: 300, // 前300帧延时发送,防止过载
|
||
firststepdelayms: 30, // 前300帧延迟 30ms
|
||
onceDelayms: 10, // 之后帧间延迟
|
||
disableBlog: true, // 升级时关闭日志
|
||
withResponse: false, // 一般OTA不需要响应确认
|
||
binmode: false, // 使用文本命令模式发送(mmx命令)
|
||
onProgress: (double progress) {
|
||
// 更新 UI 进度
|
||
final percent = (progress * 100).clamp(0, 100).round();
|
||
_updateAllUiProgress(task.mac, percent, 'upgrading');
|
||
},
|
||
);
|
||
|
||
// 根据返回码判断状态
|
||
switch (result) {
|
||
case 0:
|
||
ef.log("[蓝牙指令执行日志] OTA 升级完成 ✅");
|
||
_updateUiProgress(task.mac, 100, 'completed');
|
||
break;
|
||
case -1:
|
||
throw Exception("固件为空或格式错误");
|
||
case -2:
|
||
throw Exception("数据发送失败");
|
||
case -3:
|
||
throw Exception("日志操作失败");
|
||
case -4:
|
||
throw Exception("vota 命令执行失败");
|
||
default:
|
||
throw Exception("未知错误: $result");
|
||
}
|
||
} catch (e, stack) {
|
||
ef.log("[蓝牙指令执行日志] OTA 升级失败: $e\n$stack");
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<void> _verifyFirmware(UpgradeTask task) async {
|
||
try {
|
||
final success = await _sendVotaCommand(task.thapp);
|
||
|
||
if (!success) {
|
||
throw Exception("[蓝牙指令执行日志] 固件验证失败".tr);
|
||
}
|
||
|
||
await Future.delayed(Duration(seconds: 10));
|
||
|
||
bool deviceRestarted = false;
|
||
for (int i = 0; i < 10; i++) {
|
||
if (!task.thapp.isConnected) {
|
||
deviceRestarted = true;
|
||
break;
|
||
}
|
||
await Future.delayed(Duration(seconds: 1));
|
||
}
|
||
|
||
if (!deviceRestarted) {
|
||
throw Exception("[蓝牙指令执行日志] 设备未重启,可能升级失败".tr);
|
||
}
|
||
} catch (e, stack) {
|
||
ef.log("[蓝牙指令执行日志] 固件校验失败: $e\n$stack");
|
||
throw Exception("[蓝牙指令执行日志] 固件校验失败".tr);
|
||
}
|
||
}
|
||
|
||
Future<dynamic> _sendCommand(THapp thapp, String command,
|
||
{bool needResponse = false}) async {
|
||
try {
|
||
if (needResponse) {
|
||
// 需要响应的命令(如 vota 命令)
|
||
return await thapp.send(command, true, // withResponse = true
|
||
(log) {
|
||
ef.log("[蓝牙指令执行日志] ${log.log},指令为:$command");
|
||
|
||
// 这里根据具体命令判断是否成功完成
|
||
if (log.log.contains("SUCCESS") ||
|
||
log.log.contains("OK") ||
|
||
log.log.contains("COMPLETE") ||
|
||
log.log.contains("successful")) {
|
||
log.result = true; // 设置成功结果
|
||
return true; // 停止监听
|
||
}
|
||
|
||
// 如果包含错误信息,也返回完成
|
||
if (log.log.contains("ERROR") ||
|
||
log.log.contains("FAIL") ||
|
||
log.log.contains("failed")) {
|
||
log.result = false; // 设置失败结果
|
||
return true; // 停止监听
|
||
}
|
||
|
||
return false; // 继续监听
|
||
},
|
||
0, // retry = 0
|
||
15000 // timeoutms = 15000(15秒超时)
|
||
);
|
||
} else {
|
||
// 不需要响应的命令(如 mmx write、blog disable/enable)
|
||
final success = await thapp.send(
|
||
command,
|
||
false, // withResponse = false
|
||
null, // 不需要回调
|
||
0, // retry = 0
|
||
5000 // timeoutms = 5000(5秒超时)
|
||
);
|
||
|
||
if (!success) {
|
||
ef.log("[蓝牙指令执行日志] 命令发送可能失败: $command");
|
||
}
|
||
return success;
|
||
}
|
||
} catch (e, stack) {
|
||
ef.log("[蓝牙指令执行日志] 发送命令失败: $e\n$stack");
|
||
throw Exception("[蓝牙指令执行日志] 发送命令失败".tr);
|
||
}
|
||
}
|
||
|
||
void _updateUiProgress(String mac, int progress, String status) {
|
||
if (Get.isRegistered<MHTBlueToothController>()) {
|
||
final controller = Get.find<MHTBlueToothController>();
|
||
if (controller.localUpgradeMac.containsKey(mac)) {
|
||
controller.localUpgradeMac[mac] = {
|
||
...controller.localUpgradeMac[mac]!,
|
||
"progress": progress,
|
||
"status": status,
|
||
"updateTime": DateTime.now().millisecondsSinceEpoch,
|
||
};
|
||
// ef.log("[蓝牙指令执行日志] 更新进度: $progress, 状态: $status");
|
||
controller.update();
|
||
}
|
||
}
|
||
var upgradingDevices =
|
||
mhtDeviceUpgradeController.model.upgradingDevices ?? [];
|
||
|
||
for (var device in upgradingDevices) {
|
||
if (device.mac == mac) {
|
||
device.process = progress;
|
||
device.upgradeStatus = _mapUpgradeStatus(status);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果设备不在列表中,选择性追加(可根据你项目逻辑决定是否添加)
|
||
// if (!upgradingDevices.any((d) => d.mac == mac)) {
|
||
// upgradingDevices.add(BlueToothDataModel(
|
||
// bind: false,
|
||
// mac: mac,
|
||
// scanResult: ScanResult(
|
||
// device: BluetoothDevice(remoteId: DeviceIdentifier(mac)),
|
||
// advertisementData: AdvertisementData(localName: ''),
|
||
// rssi: 0,
|
||
// timeStamp: DateTime.now(),
|
||
// ),
|
||
// type: 0,
|
||
// lastSeen: DateTime.now(),
|
||
// process: progress,
|
||
// upgradeStatus: _mapUpgradeStatus(status),
|
||
// ));
|
||
// }
|
||
|
||
// ✅ 写回 model
|
||
mhtDeviceUpgradeController.model.upgradingDevices = upgradingDevices;
|
||
mhtDeviceUpgradeController.update();
|
||
}
|
||
|
||
void _cleanupTask(String mac) {
|
||
_firmwareDataCache.remove(mac);
|
||
}
|
||
|
||
void cancelUpgrade(String mac) {
|
||
final task = upgradeTasks[mac];
|
||
if (task != null) {
|
||
task.status = 'cancelled';
|
||
task.thapp.disconnect();
|
||
final upgradingDevices =
|
||
mhtDeviceUpgradeController.model.upgradingDevices;
|
||
if (upgradingDevices != null && upgradingDevices.isNotEmpty) {
|
||
upgradingDevices.removeWhere((device) => device.mac == mac);
|
||
}
|
||
_updateUiProgress(mac, -1, 'cancelled');
|
||
task.completer.completeError(Exception("[蓝牙指令执行日志] 升级已取消".tr));
|
||
|
||
TopSlideNotification.show(Get.context!, text: "设备 $mac 升级已取消".tr);
|
||
}
|
||
}
|
||
|
||
void cancelAllUpgrades() {
|
||
for (final mac in upgradeTasks.keys) {
|
||
cancelUpgrade(mac);
|
||
}
|
||
}
|
||
|
||
Map<String, dynamic>? getUpgradeStatus(String mac) {
|
||
final task = upgradeTasks[mac];
|
||
if (task == null) return null;
|
||
|
||
return {
|
||
'mac': task.mac,
|
||
'progress': task.progress,
|
||
'status': task.status,
|
||
'startTime': task.startTime,
|
||
'endTime': task.endTime,
|
||
};
|
||
}
|
||
|
||
Map<String, Map<String, dynamic>> getAllUpgradeStatus() {
|
||
final Map<String, Map<String, dynamic>> status = {};
|
||
for (final task in upgradeTasks.values) {
|
||
status[task.mac] = {
|
||
'progress': task.progress,
|
||
'status': task.status,
|
||
'startTime': task.startTime,
|
||
'endTime': task.endTime,
|
||
};
|
||
}
|
||
return status;
|
||
}
|
||
|
||
bool isDeviceUpgrading(String mac) {
|
||
final task = upgradeTasks[mac];
|
||
return task != null &&
|
||
(task.status == 'downloading' || task.status == 'upgrading');
|
||
}
|
||
|
||
void cleanupCompletedTasks() {
|
||
final completedMacs = upgradeTasks.entries
|
||
.where((entry) =>
|
||
['completed', 'failed', 'cancelled'].contains(entry.value.status))
|
||
.map((entry) => entry.key)
|
||
.toList();
|
||
|
||
for (final mac in completedMacs) {
|
||
upgradeTasks.remove(mac);
|
||
}
|
||
}
|
||
|
||
Future<bool> _sendVotaCommand(THapp thapp) async {
|
||
try {
|
||
final result =
|
||
await thapp.send('vota path="/root/mtd/update"', true, (log) {
|
||
ef.log("[vota命令响应] ${log.log}");
|
||
|
||
// vota 命令特定的成功判断
|
||
if (log.log.contains("verification complete") ||
|
||
log.log.contains("update successful") ||
|
||
log.log.contains("rebooting")) {
|
||
log.result = true;
|
||
return true;
|
||
}
|
||
|
||
// vota 命令特定的失败判断
|
||
if (log.log.contains("verification failed") ||
|
||
log.log.contains("update error")) {
|
||
log.result = false;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}, 3, 20000 // vota 命令可能需要更长时间
|
||
);
|
||
|
||
// return result == true;
|
||
return true;
|
||
} catch (e, stack) {
|
||
ef.log("[vota命令异常] $e\n$stack");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
void _updateAllUiProgress(String mac, int progress, String status) {
|
||
if (Get.isRegistered<MHTBlueToothController>()) {
|
||
final controller = Get.find<MHTBlueToothController>();
|
||
if (controller.localUpgradeMac.containsKey(mac)) {
|
||
controller.localUpgradeMac[mac] = {
|
||
...controller.localUpgradeMac[mac]!,
|
||
"progress": progress,
|
||
"status": status,
|
||
"updateTime": DateTime.now().millisecondsSinceEpoch,
|
||
};
|
||
// ef.log("[蓝牙指令执行日志] 更新进度: $progress, 状态: $status");
|
||
controller.update();
|
||
}
|
||
}
|
||
}
|
||
|
||
int _mapUpgradeStatus(String status) {
|
||
switch (status) {
|
||
case 'upgrading':
|
||
return APPDeviceUpgrade.upgrading.value;
|
||
case 'success':
|
||
return APPDeviceUpgrade.success.value;
|
||
case 'failed':
|
||
return APPDeviceUpgrade.fail.value;
|
||
case 'waiting':
|
||
return APPDeviceUpgrade.waiting.value;
|
||
default:
|
||
return APPDeviceUpgrade.nothing.value;
|
||
}
|
||
}
|
||
}
|