Files
tuiche/lib/common/util/BluetoothFirmwareUpdater.dart

442 lines
13 KiB
Dart
Raw 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:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:easydevice/easydevice.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:vbvs_app/component/tool/TopSlideNotification.dart';
import 'package:vbvs_app/pages/mh_page/device/controller/mht_bluetooth_controller.dart';
import 'package:ef/ef.dart'; // THapp 所在的包
// 固件升级配置(区分平台)
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);
}
}
final completer = Completer<void>();
final task = UpgradeTask(
mac: mac,
thapp: thapp,
completer: completer,
startTime: DateTime.now(),
);
_upgradeTasks[mac] = task;
_updateUiProgress(mac, 0, 'waiting');
_processUpgradeQueue();
return completer.future;
}
void _processUpgradeQueue() {
final waitingTasks =
_upgradeTasks.values.where((task) => task.status == 'waiting').toList();
for (final task in waitingTasks) {
if (_currentConcurrentUpgrades < _maxConcurrentUpgrades) {
_currentConcurrentUpgrades++;
_startSingleUpgrade(task);
}
//screen shots
}
}
Future<void> _startSingleUpgrade(UpgradeTask task) async {
try {
task.status = 'downloading';
if (!task.thapp.isConnected) {
throw Exception("[蓝牙指令执行日志] 设备未连接".tr);
}
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');
TopSlideNotification.show(Get.context!,
text: "设备 ${task.mac} 固件升级成功".tr, textColor: Colors.green);
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();
try {
await _sendCommand(task.thapp, "blog enable");
} catch (e, stack) {
ef.log("[蓝牙指令执行日志] 重新开启日志失败: $e\n$stack");
}
}
}
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);
}
final totalFrames =
(firmwareData.length / UpgradeConfig.maxFrameSize).ceil();
final stopwatch = Stopwatch()..start();
DateTime lastFrameTime = DateTime.now();
for (int sentFrames = 0; sentFrames < totalFrames; sentFrames++) {
if (task.status == 'cancelled') {
throw Exception("[蓝牙指令执行日志] 升级已取消".tr);
}
final elapsed = stopwatch.elapsed;
final frameInterval = UpgradeConfig.getFrameInterval(elapsed);
final now = DateTime.now();
if (now.difference(lastFrameTime) < frameInterval) {
await Future.delayed(Duration(milliseconds: 10));
continue;
}
final offset = sentFrames * UpgradeConfig.maxFrameSize;
final end = offset + UpgradeConfig.maxFrameSize;
final frameData = firmwareData.sublist(
offset,
end > firmwareData.length ? firmwareData.length : end,
);
final base64Data = base64Encode(frameData);
final mmxCommand =
'''mmx path="/root/mtd/update" write base64 len=${frameData.length} offset=$offset data="$base64Data"''';
try {
await _sendCommand(task.thapp, mmxCommand);
} catch (e, stack) {
ef.log("[蓝牙指令执行日志] 发送固件帧失败: $e\n$stack");
rethrow;
}
final waitTime = frameInterval - now.difference(lastFrameTime);
if (waitTime > Duration.zero) await Future.delayed(waitTime);
final progress = ((sentFrames + 1) / totalFrames * 100).round();
_updateUiProgress(task.mac, progress, 'upgrading');
lastFrameTime = now;
}
stopwatch.stop();
}
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,
};
controller.update();
}
}
}
void _cleanupTask(String mac) {
_firmwareDataCache.remove(mac);
}
void cancelUpgrade(String mac) {
final task = _upgradeTasks[mac];
if (task != null) {
task.status = 'cancelled';
_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;
}, 0, 20000 // vota 命令可能需要更长时间
);
return result == true;
} catch (e, stack) {
ef.log("[vota命令异常] $e\n$stack");
return false;
}
}
}