更新睡眠报告提示样式,更新ios蓝牙开关判定

This commit is contained in:
wyf
2025-09-04 11:20:44 +08:00
parent aad3a00ac3
commit 9f73a8111a
12 changed files with 1845 additions and 277 deletions

View File

@@ -0,0 +1,441 @@
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;
}
}
}

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'package:img_picker/img_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:intl/intl.dart';
@@ -32,7 +33,7 @@ class DailyLogUtils {
// 写入 warning 日志
static Future<void> writeWarning(String content) async {
print("[dailylog-->waring] $content]");
print("[dailylog-->waring] $content]");
await _writeLogWithLevel('WARNING', content);
}
@@ -54,7 +55,7 @@ class DailyLogUtils {
}
// 读取当天日志
static Future<String> readTodayLog() async {
static Future<String> readTodayLog() async {
final file = await _getLogFile();
return await file.readAsString();
}
@@ -93,4 +94,51 @@ class DailyLogUtils {
return logFiles;
}
static Future<String> readLogsByDateRange(
DateTime fromDate, DateTime toDate) async {
try {
final dir = await getApplicationDocumentsDirectory();
final dateFormat = DateFormat('yyyy-MM-dd');
String combinedLogs = '';
// 遍历从 fromDate 到 toDate 的日期
for (DateTime date = fromDate;
!date.isAfter(toDate);
date = date.add(Duration(days: 1))) {
final dateStr = dateFormat.format(date); // 格式化日期
final file = File('${dir.path}/$dateStr.log'); // 日志文件路径
if (await file.exists()) {
final logContent = await file.readAsString();
combinedLogs += '日志日期: $dateStr\n$logContent\n\n'; // 合并日志内容
}
}
if (combinedLogs.isNotEmpty) {
return combinedLogs; // 返回合并后的日志
} else {
return '该时间段内没有日志记录'; // 如果没有日志
}
} catch (e) {
return '读取日志失败: $e';
}
}
// 添加分享功能
// static Future<void> exportLog(DateTime date) async {
// try {
// final dir = await getApplicationDocumentsDirectory();
// final dateStr = DateFormat('yyyy-MM-dd').format(date);
// final file = File('${dir.path}/$dateStr.log');
// if (await file.exists()) {
// await Share.shareXFiles(
// [XFile(file.path)],
// text: '应用日志 $dateStr',
// );
// }
// } catch (e) {
// print('导出日志失败: $e');
// }
// }
}

View File

@@ -0,0 +1,140 @@
import 'dart:convert';
import 'dart:io';
class FirmwareVersionInfo {
final String version;
final String fileName;
final String downloadUrl;
FirmwareVersionInfo({
required this.version,
required this.fileName,
required this.downloadUrl,
});
@override
String toString() {
return 'FirmwareVersionInfo{version: $version, fileName: $fileName, downloadUrl: $downloadUrl}';
}
}
class FirmwareVersionService {
/// 从远程URL获取最新的固件版本信息
/// [baseUrl] 基础URL如 "https://share.file.he-info.cn"
/// [firmwarePath] 固件路径,如 "/ota/aithv2/"
static Future<FirmwareVersionInfo?> getLatestFirmwareVersion({
required String baseUrl,
required String firmwarePath,
}) async {
try {
// 构建完整的API URL
final apiUrl = '$baseUrl/~/api/get_file_list?uri=$firmwarePath';
// 发送HTTP请求 - 使用基础的http客户端不依赖项目特定实现
final response = await _makeHttpRequest(apiUrl);
if (response == null) {
return null;
}
// 解析响应数据
return _parseFirmwareData(response, baseUrl, firmwarePath);
} catch (e) {
print('获取固件版本信息失败: $e');
return null;
}
}
/// 发送HTTP请求
static Future<dynamic> _makeHttpRequest(String url) async {
try {
// 使用基础的http客户端
final httpClient = HttpClient();
final request = await httpClient.getUrl(Uri.parse(url));
final response = await request.close();
if (response.statusCode != 200) {
return null;
}
final responseBody = await response.transform(utf8.decoder).join();
return jsonDecode(responseBody);
} catch (e) {
print('HTTP请求失败: $e');
return null;
}
}
/// 解析固件数据
static FirmwareVersionInfo? _parseFirmwareData(
dynamic data,
String baseUrl,
String firmwarePath
) {
if (data == null || data['list'] == null) {
return null;
}
final versionList = data['list'] as List;
int? maxVersionNum;
String? latestFileName;
for (var item in versionList) {
if (item is Map && item.containsKey('n')) {
final fileName = item['n'] as String;
try {
// 解析版本号 - 假设文件名格式为 "something-version.extension"
final versionMatch = _extractVersionFromFileName(fileName);
if (versionMatch != null) {
final versionNum = int.parse(versionMatch);
if (maxVersionNum == null || versionNum > maxVersionNum) {
maxVersionNum = versionNum;
latestFileName = fileName;
}
}
} catch (e) {
// 解析失败跳过
continue;
}
}
}
if (latestFileName != null && maxVersionNum != null) {
// 构建下载URL
final downloadUrl = '$baseUrl/${firmwarePath.replaceFirst('/', '')}$latestFileName';
return FirmwareVersionInfo(
version: maxVersionNum.toString(),
fileName: latestFileName,
downloadUrl: downloadUrl,
);
}
return null;
}
/// 从文件名中提取版本号
static String? _extractVersionFromFileName(String fileName) {
// 多种可能的文件名格式处理
final patterns = [
// 格式: something-123.bin
RegExp(r'-(\d+)\.'),
// 格式: v123.bin
RegExp(r'v(\d+)\.'),
// 格式: firmware_123.bin
RegExp(r'_(\d+)\.'),
];
for (final pattern in patterns) {
final match = pattern.firstMatch(fileName);
if (match != null && match.groupCount >= 1) {
return match.group(1);
}
}
return null;
}
}