Files
tuiche/lib/common/util/BluetoothFirmwareUpdater.dart
2025-11-13 09:56:02 +08:00

565 lines
18 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = 1500015秒超时
);
} else {
// 不需要响应的命令(如 mmx write、blog disable/enable
final success = await thapp.send(
command,
false, // withResponse = false
null, // 不需要回调
0, // retry = 0
5000 // timeoutms = 50005秒超时
);
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;
}
}
}