28 changed files with 2340 additions and 1465 deletions
@ -1,747 +1,293 @@ |
|||
import 'package:classic_bluetooth/classic_bluetooth.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:voitrans/data/models/appconfig_model.dart'; |
|||
import '/routes/app_routes.dart'; |
|||
import 'package:logger/web.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:path/path.dart'; |
|||
import 'dart:async'; |
|||
import '../../../data/services/network/api.dart'; |
|||
import '../model/ota_upgrade_model.dart'; |
|||
import 'dart:io'; |
|||
import 'package:dio/dio.dart'; |
|||
import 'package:path_provider/path_provider.dart'; |
|||
import 'package:logger/web.dart'; |
|||
import 'package:ota/ota.dart'; |
|||
import '../../../data/services/ble_manager.dart'; |
|||
import '../../../data/models/appconfig.dart'; |
|||
|
|||
import '../services/version_manager.dart'; |
|||
import '../services/download_manager.dart'; |
|||
import '../services/device_checker.dart'; |
|||
import '../services/ui_manager.dart'; |
|||
import '../services/ota_status_manager.dart'; |
|||
|
|||
/// OTA升级主控制器 - 重构后的简化版本 |
|||
/// 负责协调各个服务,管理升级流程的整体状态 |
|||
class OtaUpgradeController extends GetxController { |
|||
static const int NOT_STARTED = -1; // 未开始 |
|||
static const int VALIDATION_FILE = 0; // 验证文件 |
|||
static const int ONUPGRADE = 1; // 升级中 |
|||
static const int START_UPGRADING = 2; // 开始升级 |
|||
static const int UPGRADE_SUCCESS = 3; // 升级成功 |
|||
static const int UPGRADE_FAILED = 4; // 升级失败 |
|||
static const int CONNECTION_DISCONNECTION = 5; // 连接断开 |
|||
static const int CONNECTION_SUCCESS = 6; // 连接成功 |
|||
static const int DOWNLOADING_UP_FILES = 7; // 下载升级文件 |
|||
|
|||
// 错误信息,用于存储错误提示 |
|||
final RxString errorMessage = ''.obs; |
|||
// 服务依赖 |
|||
final _versionManager = VersionManager(); |
|||
final _downloadManager = DownloadManager(); |
|||
final _deviceChecker = DeviceChecker(); |
|||
final _uiManager = UiManager(); |
|||
final _bleManager = Get.find<BleManager>(); |
|||
final _jlota = Ota.instance; |
|||
|
|||
// 最新版本号 |
|||
// 状态管理 |
|||
final RxString errorMessage = ''.obs; |
|||
final RxString latestVersion = ''.obs; |
|||
// 最新更新内容 |
|||
final RxString updatedescription = ''.obs; |
|||
|
|||
// 当前设备固件版本号 |
|||
final RxString currentVersion = ''.obs; |
|||
|
|||
String _address = ''; |
|||
|
|||
// 更新包地址,用于下载更新文件 |
|||
String updatepackageaddress = ''; |
|||
|
|||
// 下载进度,范围为 0.0 到 1.0 |
|||
final RxString detailedStatus = ''.obs; |
|||
final RxString updatedescription = ''.obs; // 添加缺失的更新描述属性 |
|||
final RxDouble downloadProgress = 0.0.obs; |
|||
|
|||
// 是否有可用更新的状态 |
|||
final isUpdateAvailable = true.obs; |
|||
|
|||
// 当前升级进度,范围为 0.0 到 1.0 |
|||
// final RxDouble progress = 0.0.obs; |
|||
|
|||
// OTA 单例实例,用于处理升级相关逻辑 |
|||
final jlota = Ota.instance; |
|||
|
|||
// 是否正在下载更新包的状态 |
|||
final RxDouble progress = 0.0.obs; |
|||
final RxInt type = OtaStatusManager.NOT_STARTED.obs; |
|||
final RxBool isUpdateAvailable = true.obs; |
|||
final RxBool isDownloading = false.obs; |
|||
|
|||
// 蓝牙管理服务,用于获取设备信息和通信 |
|||
final _bleManager = Get.find<BleManager>(); |
|||
final maxRetryAttempts = 3; // 最大重试次数 |
|||
int currentRetryCount = 0; |
|||
|
|||
final RxBool isFetchingDeviceInfo = true.obs; |
|||
|
|||
final RxDouble progress = 0.0.obs; // 控制器自己的可观察属性 |
|||
final RxInt type = OtaEvent.NOT_STARTED.obs; |
|||
final RxBool isUpgrade = false.obs; |
|||
|
|||
// 新增:详细状态提示 |
|||
final RxString detailedStatus = ''.obs; |
|||
final RxBool isFetchingDeviceInfo = true.obs; |
|||
final RxBool showDetailedStatus = false.obs; |
|||
|
|||
String _address = ''; |
|||
String _updatePackageAddress = ''; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_getCurrentVersion(); |
|||
_initializeController(); |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
jlota.onOtaClose(); |
|||
_jlota.onOtaClose(); |
|||
_bleManager.setEnterOta(false); |
|||
isUpgrade.value = false; |
|||
super.onClose(); |
|||
} |
|||
|
|||
// 添加重试方法 |
|||
void retryConnection() { |
|||
errorMessage.value = ''; |
|||
_getCurrentVersion(); |
|||
} |
|||
|
|||
String getStatusMessage(int type) { |
|||
switch (type) { |
|||
case NOT_STARTED: // -1 |
|||
return "未开始".tr; |
|||
case VALIDATION_FILE: // 0 |
|||
return "验证文件中".tr; |
|||
case ONUPGRADE: // 1 |
|||
return "升级中".tr; |
|||
case START_UPGRADING: // 2 |
|||
return "开始升级".tr; |
|||
case UPGRADE_SUCCESS: // 3 |
|||
return "升级成功".tr; |
|||
case UPGRADE_FAILED: // 4 |
|||
return "升级失败".tr; |
|||
case CONNECTION_DISCONNECTION: // 5 |
|||
return "连接断开".tr; |
|||
case CONNECTION_SUCCESS: // 6 |
|||
return "连接成功".tr; |
|||
case DOWNLOADING_UP_FILES: // 7 (新增) |
|||
return "下载升级文件".tr; |
|||
default: |
|||
return "未知状态".tr; // 更安全的默认值 |
|||
} |
|||
} |
|||
|
|||
// 添加更新方法 |
|||
Future<void> startUpdate() async { |
|||
try { |
|||
checkUpdate(); |
|||
|
|||
// 实现更新逻辑... |
|||
} catch (e) { |
|||
errorMessage.value = e.toString(); |
|||
Get.snackbar( |
|||
'更新失败', |
|||
e.toString(), |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red[600], |
|||
colorText: Colors.white, |
|||
icon: const Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
} |
|||
/// 初始化控制器 |
|||
/// 设置事件监听并加载版本信息 |
|||
Future<void> _initializeController() async { |
|||
_listenToOtaEvents(); |
|||
await _loadVersionInfo(); |
|||
} |
|||
|
|||
Future<void> _getCurrentVersion() async { |
|||
_listenToEvents(); |
|||
/// 加载版本信息 |
|||
/// 获取当前设备版本和最新版本信息,检查是否有更新 |
|||
Future<void> _loadVersionInfo() async { |
|||
try { |
|||
isFetchingDeviceInfo.value = true; |
|||
errorMessage.value = ''; |
|||
// 添加获取设备信息的重试机制 |
|||
DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; |
|||
|
|||
// // 第一次检查设备信息 |
|||
// if (deviceInfo == null) { |
|||
// // 尝试重新获取设备信息 |
|||
// await _bleManager.fetchDeviceInfo(); |
|||
// } |
|||
|
|||
while (currentRetryCount < maxRetryAttempts) { |
|||
deviceInfo = _bleManager.deviceInfo.value; |
|||
|
|||
// 检查信息有效性 |
|||
if (deviceInfo != null && |
|||
deviceInfo.firmwareVersion.isNotEmpty && |
|||
deviceInfo.firmwareProductId.isNotEmpty) { |
|||
break; |
|||
} |
|||
|
|||
// 获取设备信息 |
|||
await _bleManager.fetchDeviceInfo(); |
|||
await Future.delayed(const Duration(seconds: 5)); |
|||
currentRetryCount++; |
|||
} |
|||
// 最终验证 |
|||
if (deviceInfo == null || |
|||
deviceInfo.firmwareVersion.isEmpty || |
|||
deviceInfo.firmwareProductId.isEmpty) { |
|||
throw "获取设备信息失败,请检查连接"; |
|||
} |
|||
// 拆分 firmwareVersion 并判断两边是否一致 |
|||
final firmwareParts = deviceInfo.firmwareVersion.split('.'); |
|||
|
|||
// 从第三个 `.` 开始分割 |
|||
final firstPart = firmwareParts.sublist(0, 3).join('.'); |
|||
final secondPart = firmwareParts.sublist(3).join('.'); |
|||
Logger().d("firstPart${firstPart},secondPart=${secondPart}"); |
|||
// 判断两部分是否一致 |
|||
if (firstPart != secondPart) { |
|||
throw "固件版本格式错误:${deviceInfo.firmwareVersion}"; |
|||
} |
|||
|
|||
// 正常流程 |
|||
currentVersion.value = firstPart; |
|||
Logger().d('getequipmentinfo,id: ${deviceInfo.firmwareProductId}'); |
|||
// 获取当前版本信息 |
|||
final currentInfo = await _versionManager.getCurrentVersionInfo(); |
|||
currentVersion.value = currentInfo['currentVersion']; |
|||
|
|||
// 获取蓝牙地址 |
|||
final connectedDevices = await ClassicBluetooth.getConnectedDevices(); |
|||
if (connectedDevices.isNotEmpty) { |
|||
_address = connectedDevices.first.address; |
|||
} |
|||
|
|||
DBProduct? product = AppConfig.getproduct("DeapSound"); |
|||
|
|||
// // API请求 |
|||
// final response = await Api.getproduct({ |
|||
// "id": deviceInfo.firmwareProductId, |
|||
// }); |
|||
|
|||
// // 处理API响应 |
|||
// if (response['product'] == null) { |
|||
// throw const FormatException('API 响应格式错误'); |
|||
// } |
|||
|
|||
// final resp = OtaConfigResp.fromJson(response); |
|||
// Logger().d('getequipmentinfo,resp.systemversion: ${resp.systemversion}'); |
|||
latestVersion.value = product?.version ?? ""; |
|||
// updatedescription.value = resp.updatedescription!; |
|||
updatepackageaddress = product!.updatepackageaddress!; |
|||
// 获取最新版本信息 |
|||
final latestInfo = await _versionManager.getLatestVersionInfo(); |
|||
latestVersion.value = latestInfo['latestVersion']; |
|||
_updatePackageAddress = latestInfo['updatePackageAddress']; |
|||
|
|||
// 版本检查 |
|||
bool hasUpdate = _isNewVersionAvailable(product.version!); |
|||
// 设置更新描述信息 |
|||
updatedescription.value = |
|||
latestInfo['updateDescription'] ?? '本次更新包含性能优化和错误修复。'; |
|||
|
|||
isUpdateAvailable.value = hasUpdate; |
|||
} catch (e) { |
|||
// 统一错误处理 |
|||
errorMessage.value = e.toString(); |
|||
Get.snackbar( |
|||
'错误', |
|||
e.toString(), |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red[600], |
|||
colorText: Colors.white, |
|||
icon: const Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
// 检查是否有更新 |
|||
isUpdateAvailable.value = _versionManager.isNewVersionAvailable( |
|||
currentVersion.value, |
|||
latestVersion.value, |
|||
); |
|||
// 重要:重新抛出异常以便上层处理(可选) |
|||
} catch (e) { |
|||
_handleError('获取版本信息失败', e.toString()); |
|||
} finally { |
|||
isFetchingDeviceInfo.value = false; |
|||
} |
|||
} |
|||
|
|||
// 版本比较方法优化 - 支持升级和降级 |
|||
bool _isNewVersionAvailable(String latestVersion) { |
|||
try { |
|||
List<int> currentParts = currentVersion.value |
|||
.split('.') |
|||
.map((e) => int.parse(e.trim())) |
|||
.toList(); |
|||
List<int> latestParts = |
|||
latestVersion.split('.').map((e) => int.parse(e.trim())).toList(); |
|||
|
|||
// 确保两个版本号长度一致,短的用0补齐 |
|||
int maxLength = currentParts.length > latestParts.length |
|||
? currentParts.length |
|||
: latestParts.length; |
|||
|
|||
while (currentParts.length < maxLength) { |
|||
currentParts.add(0); |
|||
} |
|||
while (latestParts.length < maxLength) { |
|||
latestParts.add(0); |
|||
} |
|||
|
|||
// 比较每一段,只要有不同就返回true(支持升级和降级) |
|||
for (int i = 0; i < maxLength; i++) { |
|||
if (latestParts[i] != currentParts[i]) { |
|||
return true; // 版本不同,允许更新(升级或降级) |
|||
} |
|||
} |
|||
|
|||
return false; // 版本完全相同,无需更新 |
|||
} catch (e) { |
|||
Logger().d('版本比较错误: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
// 版本更新 |
|||
/// 检查更新 |
|||
/// 显示更新确认对话框或错误提示 |
|||
Future<void> checkUpdate() async { |
|||
try { |
|||
if (latestVersion.value != "") { |
|||
_showUpdateDialog(latestVersion.value); |
|||
if (latestVersion.value.isNotEmpty) { |
|||
_uiManager.showUpdateDialog(latestVersion.value, _startUpdateProcess); |
|||
} else { |
|||
Get.snackbar( |
|||
'错误', |
|||
'获取最新版本信息失败', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.orange[800], |
|||
colorText: Colors.white, |
|||
icon: const Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
_uiManager.showErrorSnackbar('错误', '获取最新版本信息失败', |
|||
backgroundColor: Colors.orange[800]); |
|||
} |
|||
} catch (e) { |
|||
Get.snackbar( |
|||
'错误', |
|||
'获取版本信息失败', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.orange[800], |
|||
colorText: Colors.white, |
|||
icon: const Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
_uiManager.showErrorSnackbar('错误', '获取版本信息失败', |
|||
backgroundColor: Colors.orange[800]); |
|||
} |
|||
} |
|||
|
|||
void _showUpdateDialog(String latest) async { |
|||
Logger().d('显示更新对话框'); |
|||
Get.dialog( |
|||
AlertDialog( |
|||
title: Text('发现新版本 $latest'), |
|||
content: Text('是否立即更新?'), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () async { |
|||
Logger().d('取消按钮被点击'); |
|||
|
|||
Get.back(); |
|||
Future.delayed(Duration(milliseconds: 300)); |
|||
}, |
|||
child: Text('取消'), |
|||
), |
|||
TextButton( |
|||
onPressed: () async { |
|||
Logger().d('更新按钮被点击'); |
|||
|
|||
Get.back(); |
|||
Future.delayed(Duration(milliseconds: 300)); |
|||
|
|||
// 检查设备电量 |
|||
if (!_checkBatteryLevel()) { |
|||
return; // 电量不足,不执行升级 |
|||
} |
|||
|
|||
startDownload(); |
|||
}, |
|||
child: Text('更新'), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
/// 开始更新流程的公共方法 |
|||
/// 供视图调用的更新入口点 |
|||
Future<void> startUpdate() async { |
|||
await _startUpdateProcess(); |
|||
} |
|||
|
|||
// 修改后的startDownload方法 |
|||
Future<void> startDownload() async { |
|||
await _bleManager.setEnterOta(true); |
|||
await Future.delayed(const Duration(seconds: 2)); |
|||
jlota.init(); |
|||
|
|||
isDownloading.value = true; |
|||
showDetailedStatus.value = true; |
|||
detailedStatus.value = '准备下载固件包...'; |
|||
|
|||
int lastIndex = updatepackageaddress.lastIndexOf('/'); |
|||
String fileName = updatepackageaddress.substring(lastIndex + 1); |
|||
String directoryName = "ota_upgrades"; |
|||
/// 检查设备状态后开始下载和升级 |
|||
Future<void> _startUpdateProcess() async { |
|||
// 检查设备电量 |
|||
if (!_deviceChecker.checkBatteryLevel()) { |
|||
return; |
|||
} |
|||
|
|||
final dio = Dio(BaseOptions( |
|||
connectTimeout: const Duration(seconds: 5), |
|||
receiveTimeout: Duration.zero, |
|||
sendTimeout: const Duration(seconds: 5), |
|||
headers: {'Accept-Encoding': 'gzip'}, |
|||
)); |
|||
await _startDownload(); |
|||
} |
|||
|
|||
type.value = DOWNLOADING_UP_FILES; |
|||
/// 开始下载固件 |
|||
/// 初始化OTA环境并下载固件包 |
|||
Future<void> _startDownload() async { |
|||
try { |
|||
detailedStatus.value = '正在获取下载目录...'; |
|||
final Directory? downloadsDir = await getApplicationSupportDirectory(); |
|||
if (downloadsDir == null) { |
|||
throw FileSystemException('无法访问下载目录'); |
|||
} |
|||
|
|||
final targetDir = |
|||
Directory('${downloadsDir.path}/Download/$directoryName'); |
|||
if (!await targetDir.exists()) { |
|||
detailedStatus.value = '创建下载目录...'; |
|||
await targetDir.create(recursive: true); |
|||
} |
|||
|
|||
final savePath = '${targetDir.path}/$fileName'; |
|||
|
|||
// 新增:检查同名文件逻辑 |
|||
detailedStatus.value = '检查本地文件...'; |
|||
bool existingSameFile = false; |
|||
try { |
|||
var files = await targetDir.list().toList(); |
|||
for (var entity in files) { |
|||
if (await FileSystemEntity.isFile(entity.path)) { |
|||
String name = basename(entity.path); // 使用path包中的basename |
|||
if (name == fileName) { |
|||
existingSameFile = true; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} catch (e) { |
|||
Get.snackbar( |
|||
'错误', |
|||
'检查本地文件失败: $e', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red[600], |
|||
colorText: Colors.white, |
|||
icon: Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
|
|||
return; |
|||
} |
|||
|
|||
if (existingSameFile) { |
|||
//Get.snackbar('文件已存在', '跳过下载,直接更新'); |
|||
detailedStatus.value = '文件已存在,准备升级...'; |
|||
startUpgrading(savePath); |
|||
return; |
|||
} |
|||
|
|||
// 新增:删除其他文件逻辑 |
|||
detailedStatus.value = '清理旧文件...'; |
|||
try { |
|||
var files = await targetDir.list().toList(); |
|||
for (var entity in files) { |
|||
if (await FileSystemEntity.isFile(entity.path)) { |
|||
await entity.delete(); |
|||
} |
|||
} |
|||
} catch (e) { |
|||
Get.snackbar( |
|||
'错误', |
|||
'删除旧文件失败: $e', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red[600], |
|||
colorText: Colors.white, |
|||
icon: Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
//isUpgrade.value = false; |
|||
return; |
|||
} |
|||
|
|||
Logger().d('文件保存路径: $savePath'); |
|||
|
|||
detailedStatus.value = '开始下载固件包...'; |
|||
final options = Options( |
|||
receiveTimeout: Duration.zero, |
|||
followRedirects: true, |
|||
maxRedirects: 5, |
|||
); |
|||
// 修改下载部分 |
|||
await dio.download( |
|||
updatepackageaddress, |
|||
savePath, |
|||
options: options, |
|||
onReceiveProgress: (received, total) { |
|||
if (total != -1) { |
|||
downloadProgress.value = received / total; |
|||
detailedStatus.value = |
|||
'正在下载固件包... ${(received / total * 100).toStringAsFixed(1)}%'; |
|||
} |
|||
await _bleManager.setEnterOta(true); |
|||
await Future.delayed(const Duration(seconds: 2)); |
|||
_jlota.init(); |
|||
|
|||
isDownloading.value = true; |
|||
showDetailedStatus.value = true; |
|||
type.value = OtaStatusManager.DOWNLOADING_UP_FILES; |
|||
|
|||
final filePath = await _downloadManager.downloadFirmware( |
|||
downloadUrl: _updatePackageAddress, |
|||
onProgress: (progress) { |
|||
downloadProgress.value = progress; |
|||
}, |
|||
onStatusUpdate: (status) { |
|||
detailedStatus.value = status; |
|||
}, |
|||
); |
|||
File file = File(savePath); |
|||
if (await file.exists()) { |
|||
// Get.snackbar('下载完成', '文件已保存至: $savePath'); |
|||
detailedStatus.value = '下载完成,准备升级...'; |
|||
} else { |
|||
Get.snackbar( |
|||
'错误', |
|||
'文件未找到,请检查写入权限', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.orange[600], |
|||
colorText: Colors.white, |
|||
icon: Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
detailedStatus.value = '下载失败'; |
|||
showDetailedStatus.value = false; |
|||
return; |
|||
} |
|||
|
|||
isDownloading.value = false; |
|||
startUpgrading(savePath); |
|||
return; |
|||
} on DioException catch (e) { |
|||
Get.snackbar( |
|||
'下载失败', |
|||
'错误: ${e.message}', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.orange[600], |
|||
colorText: Colors.white, |
|||
icon: Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
} on FileSystemException catch (e) { |
|||
Get.snackbar( |
|||
'存储失败', |
|||
'无法写入文件: ${e.message}', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.orange[600], |
|||
colorText: Colors.white, |
|||
icon: Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
await _startUpgrading(filePath); |
|||
} catch (e) { |
|||
isDownloading.value = false; |
|||
detailedStatus.value = '下载失败,请检查网络连接'; |
|||
showDetailedStatus.value = false; |
|||
Get.snackbar( |
|||
'下载失败', |
|||
'请检查网络连接: $e', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.orange[600], |
|||
colorText: Colors.white, |
|||
icon: Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
_handleError('下载失败', e.toString()); |
|||
Logger().d('下载失败: $e'); |
|||
} |
|||
} |
|||
|
|||
// 开始升级 |
|||
void startUpgrading(String path) async { |
|||
/// 开始升级 |
|||
/// [filePath] 固件文件路径 |
|||
/// 启动OTA升级过程 |
|||
Future<void> _startUpgrading(String filePath) async { |
|||
detailedStatus.value = '准备设备连接...'; |
|||
await Future.delayed(const Duration(milliseconds: 500)); |
|||
detailedStatus.value = '正在连接设备...'; |
|||
jlota.startUpgrading(address: _address, filePath: path); |
|||
return; |
|||
// isUpgrade.value = false; |
|||
_jlota.startUpgrading(address: _address, filePath: filePath); |
|||
} |
|||
|
|||
// 停止升级 |
|||
void stopUpgrading() async { |
|||
jlota.onOtaClose(); |
|||
return; |
|||
// isUpgrade.value = false; |
|||
/// 停止升级 |
|||
/// 关闭OTA连接 |
|||
void stopUpgrading() { |
|||
_jlota.onOtaClose(); |
|||
} |
|||
|
|||
// 检查设备电量是否满足升级条件 |
|||
bool _checkBatteryLevel() { |
|||
try { |
|||
final deviceInfo = _bleManager.deviceInfo.value; |
|||
|
|||
if (deviceInfo == null) { |
|||
Get.snackbar( |
|||
'电量检查失败', |
|||
'无法获取设备信息,请检查设备连接', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.orange[600], |
|||
colorText: Colors.white, |
|||
icon: Icon( |
|||
Icons.warning_amber, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
return false; |
|||
} |
|||
/// 重试连接 |
|||
/// 清除错误信息并重新加载版本信息 |
|||
void retryConnection() { |
|||
errorMessage.value = ''; |
|||
_loadVersionInfo(); |
|||
} |
|||
|
|||
final leftBattery = deviceInfo.leftEarBattery; |
|||
final rightBattery = deviceInfo.rightEarBattery; |
|||
|
|||
// 检查左右耳电量是否都大于30% |
|||
if (leftBattery <= 30 || rightBattery <= 30) { |
|||
String message = '设备电量不足,无法进行固件升级\n'; |
|||
message += '升级要求:左右耳电量均需大于30%\n'; |
|||
message += '当前电量:左耳 ${leftBattery}%,右耳 ${rightBattery}%\n'; |
|||
message += '请充电后再试'; |
|||
|
|||
Get.dialog( |
|||
AlertDialog( |
|||
title: Text( |
|||
'电量不足', |
|||
style: TextStyle( |
|||
color: Colors.orange[700], |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
content: Text( |
|||
message, |
|||
style: TextStyle( |
|||
color: Colors.orange[600], |
|||
), |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Get.back(), |
|||
style: TextButton.styleFrom( |
|||
foregroundColor: Colors.orange[700], |
|||
), |
|||
child: const Text('确定'), |
|||
), |
|||
], |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
side: BorderSide( |
|||
color: Colors.orange.withValues(alpha: 0.3), |
|||
width: 1, |
|||
), |
|||
), |
|||
backgroundColor: Colors.orange[50], |
|||
), |
|||
); |
|||
return false; |
|||
} |
|||
/// 获取状态描述 |
|||
/// [type] 状态类型 |
|||
/// 返回对应的状态描述文本 |
|||
String getStatusMessage(int type) { |
|||
return OtaStatusManager.getStatusMessage(type); |
|||
} |
|||
|
|||
return true; |
|||
} catch (e) { |
|||
Get.snackbar( |
|||
'电量检查失败', |
|||
'检查设备电量时发生错误:$e', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.orange[600], |
|||
colorText: Colors.white, |
|||
icon: const Icon( |
|||
Icons.warning_amber, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
return false; |
|||
} |
|||
/// 监听OTA事件 |
|||
/// 设置OTA事件流的监听器 |
|||
void _listenToOtaEvents() { |
|||
_jlota.eventStream.listen((event) { |
|||
_handleOtaEvent(event); |
|||
}); |
|||
} |
|||
|
|||
void _listenToEvents() { |
|||
Ota.instance.eventStream.listen((event) { |
|||
if (event.event == 'onProgress' && |
|||
event.type != null && |
|||
event.progress != null) { |
|||
progress.value = event.progress!; |
|||
type.value = event.type!; |
|||
isUpgrade.value = true; // 开始升级 |
|||
detailedStatus.value = |
|||
'升级进度: ${(event.progress! * 100).toStringAsFixed(1)}%'; |
|||
} |
|||
if (event.event == 'onStartOTA') { |
|||
type.value = START_UPGRADING; |
|||
isUpgrade.value = true; // 开始升级 |
|||
/// 处理OTA事件 |
|||
/// [event] OTA事件对象 |
|||
/// 根据不同的事件类型执行相应的处理逻辑 |
|||
void _handleOtaEvent(dynamic event) { |
|||
switch (event.event) { |
|||
case 'onProgress': |
|||
if (event.type != null && event.progress != null) { |
|||
progress.value = event.progress!; |
|||
type.value = event.type!; |
|||
isUpgrade.value = true; |
|||
detailedStatus.value = |
|||
'升级进度: ${(event.progress! * 100).toStringAsFixed(1)}%'; |
|||
} |
|||
break; |
|||
case 'onStartOTA': |
|||
type.value = OtaStatusManager.START_UPGRADING; |
|||
isUpgrade.value = true; |
|||
detailedStatus.value = 'OTA升级已开始...'; |
|||
} |
|||
if (event.event == 'onStopOTA') { |
|||
type.value = UPGRADE_SUCCESS; |
|||
isUpgrade.value = false; |
|||
detailedStatus.value = '升级成功完成'; |
|||
showDetailedStatus.value = false; |
|||
|
|||
// 升级成功,更新版本信息显示 |
|||
currentVersion.value = latestVersion.value; |
|||
isUpdateAvailable.value = false; |
|||
|
|||
// 显示升级成功对话框,引导用户重新配对 |
|||
Get.dialog( |
|||
AlertDialog( |
|||
title: Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.check_circle, |
|||
color: Colors.green[600], |
|||
size: 28, |
|||
), |
|||
SizedBox(width: 12.w), |
|||
Text( |
|||
'升级成功', |
|||
style: TextStyle( |
|||
color: Colors.green[700], |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
content: Text( |
|||
'固件升级已完成!\n\n设备需要重新配对才能正常使用,现在跳转到配对页面吗?', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
height: 1.4, |
|||
), |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () { |
|||
Get.back(); // 关闭对话框 |
|||
}, |
|||
child: Text( |
|||
'稍后配对', |
|||
style: TextStyle(color: Colors.grey[600]), |
|||
), |
|||
), |
|||
ElevatedButton( |
|||
onPressed: () { |
|||
Get.back(); // 关闭对话框 |
|||
Get.toNamed(Routes.pairing); |
|||
}, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.green[600], |
|||
foregroundColor: Colors.white, |
|||
), |
|||
child: Text('立即配对'), |
|||
), |
|||
], |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
), |
|||
barrierDismissible: false, // 防止用户意外关闭 |
|||
); |
|||
} |
|||
if (event.event == 'onError') { |
|||
type.value = UPGRADE_FAILED; |
|||
isUpgrade.value = false; |
|||
detailedStatus.value = '升级失败,错误码:${event.code}'; |
|||
showDetailedStatus.value = false; |
|||
Get.snackbar( |
|||
'升级失败', |
|||
'请检查设备连接或升级包,错误码:${event.code}', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red[600], |
|||
colorText: Colors.white, |
|||
icon: Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
} |
|||
if (event.event == 'NOCONNECTION') { |
|||
type.value = CONNECTION_DISCONNECTION; |
|||
isUpgrade.value = false; |
|||
detailedStatus.value = '设备连接断开'; |
|||
showDetailedStatus.value = false; |
|||
} |
|||
if (event.event == 'CONNECTION_SUCCESS') { |
|||
type.value = CONNECTION_SUCCESS; |
|||
isUpgrade.value = true; // 开始升级 |
|||
detailedStatus.value = '设备连接成功'; |
|||
} |
|||
}); |
|||
break; |
|||
case 'onStopOTA': |
|||
_handleUpgradeSuccess(); |
|||
break; |
|||
case 'onError': |
|||
_handleUpgradeError(event.code); |
|||
break; |
|||
case 'NOCONNECTION': |
|||
_handleConnectionLost(); |
|||
break; |
|||
case 'CONNECTION_SUCCESS': |
|||
_handleConnectionSuccess(); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
/// 处理升级成功 |
|||
/// 更新状态并显示成功对话框 |
|||
void _handleUpgradeSuccess() { |
|||
type.value = OtaStatusManager.UPGRADE_SUCCESS; |
|||
isUpgrade.value = false; |
|||
detailedStatus.value = '升级成功完成'; |
|||
showDetailedStatus.value = false; |
|||
|
|||
// 更新版本信息 |
|||
currentVersion.value = latestVersion.value; |
|||
isUpdateAvailable.value = false; |
|||
|
|||
// 显示成功对话框 |
|||
_uiManager.showUpgradeSuccessDialog(); |
|||
} |
|||
|
|||
/// 处理升级错误 |
|||
/// [errorCode] 错误代码 |
|||
/// 更新状态并显示错误提示 |
|||
void _handleUpgradeError(dynamic errorCode) { |
|||
type.value = OtaStatusManager.UPGRADE_FAILED; |
|||
isUpgrade.value = false; |
|||
detailedStatus.value = '升级失败,错误码:$errorCode'; |
|||
showDetailedStatus.value = false; |
|||
_uiManager.showErrorSnackbar( |
|||
'升级失败', |
|||
'请检查设备连接或升级包,错误码:$errorCode', |
|||
); |
|||
} |
|||
|
|||
/// 处理连接断开 |
|||
/// 更新连接状态 |
|||
void _handleConnectionLost() { |
|||
type.value = OtaStatusManager.CONNECTION_DISCONNECTION; |
|||
isUpgrade.value = false; |
|||
detailedStatus.value = '设备连接断开'; |
|||
showDetailedStatus.value = false; |
|||
} |
|||
|
|||
/// 处理连接成功 |
|||
/// 更新连接状态 |
|||
void _handleConnectionSuccess() { |
|||
type.value = OtaStatusManager.CONNECTION_SUCCESS; |
|||
isUpgrade.value = true; |
|||
detailedStatus.value = '设备连接成功'; |
|||
} |
|||
|
|||
/// 统一错误处理 |
|||
/// [title] 错误标题 |
|||
/// [message] 错误信息 |
|||
/// 设置错误状态并显示错误提示 |
|||
void _handleError(String title, String message) { |
|||
errorMessage.value = message; |
|||
_uiManager.showErrorSnackbar(title, message); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,99 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import '../../../data/services/ble_manager.dart'; |
|||
|
|||
/// 设备检查服务 |
|||
/// 负责检查设备状态,如电量检查等 |
|||
class DeviceChecker { |
|||
final BleManager _bleManager = Get.find<BleManager>(); |
|||
|
|||
/// 检查设备电量是否满足升级条件 |
|||
/// 返回true表示电量充足,可以进行升级 |
|||
/// 返回false表示电量不足,不能进行升级 |
|||
bool checkBatteryLevel() { |
|||
try { |
|||
final deviceInfo = _bleManager.deviceInfo.value; |
|||
|
|||
if (deviceInfo == null) { |
|||
_showBatteryCheckFailedSnackbar('无法获取设备信息,请检查设备连接'); |
|||
return false; |
|||
} |
|||
|
|||
final leftBattery = deviceInfo.leftEarBattery; |
|||
final rightBattery = deviceInfo.rightEarBattery; |
|||
|
|||
// 检查左右耳电量是否都大于30% |
|||
if (leftBattery <= 30 || rightBattery <= 30) { |
|||
_showLowBatteryDialog(leftBattery, rightBattery); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} catch (e) { |
|||
_showBatteryCheckFailedSnackbar('检查设备电量时发生错误:$e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 显示电量不足对话框 |
|||
/// [leftBattery] 左耳电量 |
|||
/// [rightBattery] 右耳电量 |
|||
void _showLowBatteryDialog(int leftBattery, int rightBattery) { |
|||
String message = '设备电量不足,无法进行固件升级\n'; |
|||
message += '升级要求:左右耳电量均需大于30%\n'; |
|||
message += '当前电量:左耳 ${leftBattery}%,右耳 ${rightBattery}%\n'; |
|||
message += '请充电后再试'; |
|||
|
|||
Get.dialog( |
|||
AlertDialog( |
|||
title: Text( |
|||
'电量不足', |
|||
style: TextStyle( |
|||
color: Colors.orange[700], |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
content: Text( |
|||
message, |
|||
style: TextStyle( |
|||
color: Colors.orange[600], |
|||
), |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Get.back(), |
|||
style: TextButton.styleFrom( |
|||
foregroundColor: Colors.orange[700], |
|||
), |
|||
child: const Text('确定'), |
|||
), |
|||
], |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
side: BorderSide( |
|||
color: Colors.orange.withValues(alpha: 0.3), |
|||
width: 1, |
|||
), |
|||
), |
|||
backgroundColor: Colors.orange[50], |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 显示电量检查失败提示 |
|||
/// [message] 错误信息 |
|||
void _showBatteryCheckFailedSnackbar(String message) { |
|||
Get.snackbar( |
|||
'电量检查失败', |
|||
message, |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.orange[600], |
|||
colorText: Colors.white, |
|||
icon: const Icon( |
|||
Icons.warning_amber, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,153 @@ |
|||
import 'dart:io'; |
|||
import 'package:dio/dio.dart'; |
|||
import 'package:dio/io.dart'; |
|||
import 'package:path_provider/path_provider.dart'; |
|||
import 'package:path/path.dart'; |
|||
import 'package:logger/web.dart'; |
|||
|
|||
/// 文件下载管理器 |
|||
/// 负责处理固件包的下载、文件管理等功能 |
|||
class DownloadManager { |
|||
late final Dio _dio; |
|||
|
|||
/// 构造函数 |
|||
/// 初始化Dio实例并配置SSL证书处理 |
|||
DownloadManager() { |
|||
_dio = Dio(BaseOptions( |
|||
connectTimeout: const Duration(seconds: 30), // 增加连接超时时间 |
|||
receiveTimeout: Duration.zero, |
|||
sendTimeout: const Duration(seconds: 30), // 增加发送超时时间 |
|||
headers: {'Accept-Encoding': 'gzip'}, |
|||
)); |
|||
|
|||
// 配置SSL证书验证 |
|||
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { |
|||
final client = HttpClient(); |
|||
|
|||
// 处理SSL证书验证 |
|||
client.badCertificateCallback = |
|||
(X509Certificate cert, String host, int port) { |
|||
// 在生产环境中,应该进行适当的证书验证 |
|||
// 这里为了解决证书过期问题,暂时跳过验证 |
|||
// 建议:在生产环境中实现更安全的证书验证逻辑 |
|||
Logger().w('SSL证书验证跳过: $host:$port'); |
|||
return true; |
|||
}; |
|||
|
|||
return client; |
|||
}; |
|||
} |
|||
|
|||
/// 下载固件包 |
|||
/// [downloadUrl] 下载地址 |
|||
/// [onProgress] 下载进度回调 |
|||
/// [onStatusUpdate] 状态更新回调 |
|||
/// 返回下载文件的本地路径 |
|||
/// 抛出异常如果下载失败 |
|||
Future<String> downloadFirmware({ |
|||
required String downloadUrl, |
|||
required Function(double) onProgress, |
|||
required Function(String) onStatusUpdate, |
|||
}) async { |
|||
onStatusUpdate('准备下载固件包...'); |
|||
|
|||
final fileName = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); |
|||
const directoryName = "ota_upgrades"; |
|||
|
|||
// 获取下载目录 |
|||
onStatusUpdate('正在获取下载目录...'); |
|||
final Directory? downloadsDir = await getApplicationSupportDirectory(); |
|||
if (downloadsDir == null) { |
|||
throw FileSystemException('无法访问下载目录'); |
|||
} |
|||
|
|||
final targetDir = Directory('${downloadsDir.path}/Download/$directoryName'); |
|||
if (!await targetDir.exists()) { |
|||
onStatusUpdate('创建下载目录...'); |
|||
await targetDir.create(recursive: true); |
|||
} |
|||
|
|||
final savePath = '${targetDir.path}/$fileName'; |
|||
|
|||
// 检查是否已存在同名文件 |
|||
onStatusUpdate('检查本地文件...'); |
|||
if (await _checkExistingFile(targetDir, fileName)) { |
|||
onStatusUpdate('文件已存在,准备升级...'); |
|||
return savePath; |
|||
} |
|||
|
|||
// 清理旧文件 |
|||
onStatusUpdate('清理旧文件...'); |
|||
await _cleanOldFiles(targetDir); |
|||
|
|||
// 开始下载 |
|||
onStatusUpdate('开始下载固件包...'); |
|||
Logger().d('文件保存路径: $savePath'); |
|||
|
|||
final options = Options( |
|||
receiveTimeout: Duration.zero, |
|||
followRedirects: true, |
|||
maxRedirects: 5, |
|||
); |
|||
|
|||
await _dio.download( |
|||
downloadUrl, |
|||
savePath, |
|||
options: options, |
|||
onReceiveProgress: (received, total) { |
|||
if (total != -1) { |
|||
final progress = received / total; |
|||
onProgress(progress); |
|||
onStatusUpdate('正在下载固件包... ${(progress * 100).toStringAsFixed(1)}%'); |
|||
} |
|||
}, |
|||
); |
|||
|
|||
// 验证文件是否下载成功 |
|||
final file = File(savePath); |
|||
if (!await file.exists()) { |
|||
throw FileSystemException('文件下载失败,请检查写入权限'); |
|||
} |
|||
|
|||
onStatusUpdate('下载完成,准备升级...'); |
|||
return savePath; |
|||
} |
|||
|
|||
/// 检查是否存在同名文件 |
|||
/// [targetDir] 目标目录 |
|||
/// [fileName] 文件名 |
|||
/// 返回true表示文件已存在 |
|||
Future<bool> _checkExistingFile(Directory targetDir, String fileName) async { |
|||
try { |
|||
final files = await targetDir.list().toList(); |
|||
for (var entity in files) { |
|||
if (await FileSystemEntity.isFile(entity.path)) { |
|||
final name = basename(entity.path); |
|||
if (name == fileName) { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
return false; |
|||
} catch (e) { |
|||
Logger().d('检查本地文件失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 清理旧文件 |
|||
/// [targetDir] 目标目录 |
|||
/// 抛出异常如果清理失败 |
|||
Future<void> _cleanOldFiles(Directory targetDir) async { |
|||
try { |
|||
final files = await targetDir.list().toList(); |
|||
for (var entity in files) { |
|||
if (await FileSystemEntity.isFile(entity.path)) { |
|||
await entity.delete(); |
|||
} |
|||
} |
|||
} catch (e) { |
|||
throw Exception('删除旧文件失败: $e'); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
import 'package:get/get.dart'; |
|||
|
|||
/// OTA升级状态管理器 |
|||
/// 负责管理OTA升级过程中的各种状态常量和状态描述 |
|||
class OtaStatusManager { |
|||
static const int NOT_STARTED = -1; // 未开始 |
|||
static const int VALIDATION_FILE = 0; // 验证文件 |
|||
static const int ONUPGRADE = 1; // 升级中 |
|||
static const int START_UPGRADING = 2; // 开始升级 |
|||
static const int UPGRADE_SUCCESS = 3; // 升级成功 |
|||
static const int UPGRADE_FAILED = 4; // 升级失败 |
|||
static const int CONNECTION_DISCONNECTION = 5; // 连接断开 |
|||
static const int CONNECTION_SUCCESS = 6; // 连接成功 |
|||
static const int DOWNLOADING_UP_FILES = 7; // 下载升级文件 |
|||
|
|||
/// 获取状态描述信息 |
|||
/// [type] 状态类型 |
|||
/// 返回对应的状态描述文本 |
|||
static String getStatusMessage(int type) { |
|||
switch (type) { |
|||
case NOT_STARTED: |
|||
return "未开始".tr; |
|||
case VALIDATION_FILE: |
|||
return "验证文件中".tr; |
|||
case ONUPGRADE: |
|||
return "升级中".tr; |
|||
case START_UPGRADING: |
|||
return "开始升级".tr; |
|||
case UPGRADE_SUCCESS: |
|||
return "升级成功".tr; |
|||
case UPGRADE_FAILED: |
|||
return "升级失败".tr; |
|||
case CONNECTION_DISCONNECTION: |
|||
return "连接断开".tr; |
|||
case CONNECTION_SUCCESS: |
|||
return "连接成功".tr; |
|||
case DOWNLOADING_UP_FILES: |
|||
return "下载升级文件".tr; |
|||
default: |
|||
return "未知状态".tr; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:logger/web.dart'; |
|||
import '/routes/app_routes.dart'; |
|||
|
|||
/// UI交互管理器 |
|||
/// 负责处理所有与用户界面交互相关的功能 |
|||
class UiManager { |
|||
/// 显示更新确认对话框 |
|||
/// [latestVersion] 最新版本号 |
|||
/// [onConfirm] 确认更新的回调函数 |
|||
void showUpdateDialog(String latestVersion, VoidCallback onConfirm) { |
|||
Logger().d('显示更新对话框'); |
|||
Get.dialog( |
|||
AlertDialog( |
|||
title: Text('发现新版本 $latestVersion'), |
|||
content: const Text('是否立即更新?'), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () { |
|||
Logger().d('取消按钮被点击'); |
|||
Get.back(); |
|||
}, |
|||
child: const Text('取消'), |
|||
), |
|||
TextButton( |
|||
onPressed: () { |
|||
Logger().d('更新按钮被点击'); |
|||
Get.back(); |
|||
Future.delayed(const Duration(milliseconds: 300)); |
|||
onConfirm(); |
|||
}, |
|||
child: const Text('更新'), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 显示升级成功对话框 |
|||
/// 引导用户进行设备重新配对 |
|||
void showUpgradeSuccessDialog() { |
|||
Get.dialog( |
|||
AlertDialog( |
|||
title: Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.check_circle, |
|||
color: Colors.green[600], |
|||
size: 28, |
|||
), |
|||
SizedBox(width: 12.w), |
|||
Text( |
|||
'升级成功', |
|||
style: TextStyle( |
|||
color: Colors.green[700], |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
content: const Text( |
|||
'固件升级已完成!\n\n设备需要重新配对才能正常使用,现在跳转到配对页面吗?', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
height: 1.4, |
|||
), |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () { |
|||
Get.back(); |
|||
}, |
|||
child: Text( |
|||
'稍后配对', |
|||
style: TextStyle(color: Colors.grey[600]), |
|||
), |
|||
), |
|||
ElevatedButton( |
|||
onPressed: () { |
|||
Get.back(); |
|||
Get.toNamed(Routes.pairing); |
|||
}, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.green[600], |
|||
foregroundColor: Colors.white, |
|||
), |
|||
child: const Text('立即配对'), |
|||
), |
|||
], |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
), |
|||
barrierDismissible: false, |
|||
); |
|||
} |
|||
|
|||
/// 显示错误提示 |
|||
/// [title] 错误标题 |
|||
/// [message] 错误信息 |
|||
/// [backgroundColor] 背景颜色,可选 |
|||
void showErrorSnackbar(String title, String message, {Color? backgroundColor}) { |
|||
Get.snackbar( |
|||
title, |
|||
message, |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: backgroundColor ?? Colors.red[600], |
|||
colorText: Colors.white, |
|||
icon: const Icon( |
|||
Icons.error_outline, |
|||
color: Colors.white, |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,114 @@ |
|||
import 'package:logger/web.dart'; |
|||
import '../../../data/services/ble_manager.dart'; |
|||
import '../../../data/models/appconfig.dart'; |
|||
import '../../../data/models/appconfig_model.dart'; |
|||
import 'package:get/get.dart'; |
|||
|
|||
/// 版本管理服务 |
|||
/// 负责处理设备版本信息获取、版本比较等功能 |
|||
class VersionManager { |
|||
final BleManager _bleManager = Get.find<BleManager>(); |
|||
final int maxRetryAttempts = 3; |
|||
int currentRetryCount = 0; |
|||
|
|||
/// 获取当前设备版本信息 |
|||
/// 返回包含当前版本、产品ID和设备信息的Map |
|||
/// 抛出异常如果获取失败 |
|||
Future<Map<String, dynamic>> getCurrentVersionInfo() async { |
|||
DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; |
|||
|
|||
// 重试机制获取设备信息 |
|||
while (currentRetryCount < maxRetryAttempts) { |
|||
deviceInfo = _bleManager.deviceInfo.value; |
|||
|
|||
if (deviceInfo != null && |
|||
deviceInfo.firmwareVersion.isNotEmpty && |
|||
deviceInfo.firmwareProductId.isNotEmpty) { |
|||
break; |
|||
} |
|||
|
|||
await _bleManager.fetchDeviceInfo(); |
|||
await Future.delayed(const Duration(seconds: 5)); |
|||
currentRetryCount++; |
|||
} |
|||
|
|||
if (deviceInfo == null || |
|||
deviceInfo.firmwareVersion.isEmpty || |
|||
deviceInfo.firmwareProductId.isEmpty) { |
|||
throw "获取设备信息失败,请检查连接"; |
|||
} |
|||
|
|||
// 验证固件版本格式 |
|||
final firmwareParts = deviceInfo.firmwareVersion.split('.'); |
|||
final firstPart = firmwareParts.sublist(0, 3).join('.'); |
|||
final secondPart = firmwareParts.sublist(3).join('.'); |
|||
|
|||
Logger().d("firstPart: $firstPart, secondPart: $secondPart"); |
|||
|
|||
if (firstPart != secondPart) { |
|||
throw "固件版本格式错误:${deviceInfo.firmwareVersion}"; |
|||
} |
|||
|
|||
return { |
|||
'currentVersion': firstPart, |
|||
'productId': deviceInfo.firmwareProductId, |
|||
'deviceInfo': deviceInfo, |
|||
}; |
|||
} |
|||
|
|||
/// 获取最新版本信息 |
|||
/// 返回包含最新版本、更新描述和更新包地址的Map |
|||
/// 抛出异常如果获取失败 |
|||
Future<Map<String, dynamic>> getLatestVersionInfo() async { |
|||
DBProduct? product = AppConfig.getproduct("DeapSound"); |
|||
|
|||
if (product == null) { |
|||
throw "无法获取产品信息"; |
|||
} |
|||
|
|||
return { |
|||
'latestVersion': product.version ?? "", |
|||
'updateDescription': product.updatedescription ?? "", |
|||
'updatePackageAddress': product.updatepackageaddress ?? "", |
|||
}; |
|||
} |
|||
|
|||
/// 比较版本号,判断是否有新版本可用 |
|||
/// [currentVersion] 当前版本号 |
|||
/// [latestVersion] 最新版本号 |
|||
/// 返回true表示有新版本可用(支持升级和降级) |
|||
bool isNewVersionAvailable(String currentVersion, String latestVersion) { |
|||
try { |
|||
List<int> currentParts = currentVersion |
|||
.split('.') |
|||
.map((e) => int.parse(e.trim())) |
|||
.toList(); |
|||
List<int> latestParts = |
|||
latestVersion.split('.').map((e) => int.parse(e.trim())).toList(); |
|||
|
|||
// 确保两个版本号长度一致,短的用0补齐 |
|||
int maxLength = currentParts.length > latestParts.length |
|||
? currentParts.length |
|||
: latestParts.length; |
|||
|
|||
while (currentParts.length < maxLength) { |
|||
currentParts.add(0); |
|||
} |
|||
while (latestParts.length < maxLength) { |
|||
latestParts.add(0); |
|||
} |
|||
|
|||
// 比较每一段,只要有不同就返回true(支持升级和降级) |
|||
for (int i = 0; i < maxLength; i++) { |
|||
if (latestParts[i] != currentParts[i]) { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} catch (e) { |
|||
Logger().d('版本比较错误: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,534 @@ |
|||
import Foundation |
|||
import AVFoundation |
|||
import MicrosoftCognitiveServicesSpeech |
|||
import os.log |
|||
|
|||
/** |
|||
* 简单音频接收器类,用于处理音频录制和流传输 |
|||
* 对应Android的SimpleAudioReceiver功能 |
|||
*/ |
|||
public class SimpleAudioReceiver: NSObject { |
|||
|
|||
private let tag = "SimpleAudioReceiver" |
|||
private let log = OSLog(subsystem: "com.azure.speech", category: "SimpleAudioReceiver") |
|||
|
|||
/** |
|||
* 音频来源类型 |
|||
*/ |
|||
public enum AudioSourceType { |
|||
/** 使用设备麦克风 */ |
|||
case microphone |
|||
/** 使用外部提供的音频数据 */ |
|||
case external |
|||
} |
|||
|
|||
// MARK: - 音频流类 |
|||
|
|||
public private(set) var pushAudioStream: SPXPushAudioInputStream? |
|||
private let writeQueue = LinkedBlockingQueue<Data>() |
|||
private var audioEngine: AVAudioEngine? |
|||
private var audioFormat: AVAudioFormat? |
|||
private let audioSession = AVAudioSession.sharedInstance() |
|||
private var audioSourceType = AudioSourceType.microphone |
|||
private var audioDataCallback: AudioDataCallback? |
|||
private var isRunning = false |
|||
public var _isWriting = false |
|||
private let bufferSize: Int = 4096 |
|||
private var writeThread: DispatchQueue? |
|||
private var currentRoute: AudioOutputRoute? |
|||
//public var onAudioData: ((Data) -> Void)? |
|||
public var recordfile: RecordFile? |
|||
// 添加对外部类的弱引用 |
|||
private weak var parentHelper: AzureAsrHelper? |
|||
|
|||
// 添加初始化方法,接收外部类引用 |
|||
init(parentHelper: AzureAsrHelper) { |
|||
self.parentHelper = parentHelper |
|||
super.init() |
|||
} |
|||
/** |
|||
* 初始化 |
|||
*/ |
|||
/** |
|||
* 初始化音频录制组件 |
|||
* 包括音频格式、推流、音频引擎等核心组件的初始化 |
|||
*/ |
|||
public func initAudioRecord() { |
|||
print("初始化了") |
|||
audioFormat = getOptimalAudioFormat() |
|||
pushAudioStream = SPXPushAudioInputStream() |
|||
|
|||
// 初始化音频引擎 |
|||
audioEngine = AVAudioEngine() |
|||
|
|||
isRunning = true |
|||
// 创建新的写线程 |
|||
writeThread = DispatchQueue(label: "audio.stream.writer") |
|||
startWriteThread() |
|||
} |
|||
|
|||
/// 获取最佳音频格式 (iOS 通常支持标准采样率) |
|||
public func getOptimalAudioFormat() -> AVAudioFormat? { |
|||
let sampleRate: Double = 16000 // iOS 通常支持 16kHz |
|||
return AVAudioFormat( |
|||
commonFormat: .pcmFormatInt16, |
|||
sampleRate: sampleRate, |
|||
channels: 1, |
|||
interleaved: true |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* 设置音频配置 |
|||
* @param sampleRate 采样率,默认16000 |
|||
* @param channels 声道数,默认1 |
|||
*/ |
|||
/** |
|||
* 设置音频配置参数 |
|||
* @param sampleRate 采样率 |
|||
* @param channels 声道数 |
|||
*/ |
|||
public func setAudioConfig(sampleRate: Int, channels: Int) { |
|||
print( |
|||
"AudioStream设置音频配置: sampleRate=\(sampleRate), channels=\(channels)" |
|||
) |
|||
|
|||
// 如果已经有推流,重新创建 |
|||
if pushAudioStream != nil { |
|||
// 修复:安全解包 SPXAudioStreamFormat |
|||
guard let audioStreamFormat = SPXAudioStreamFormat.init( |
|||
usingPCMWithSampleRate: UInt(sampleRate), |
|||
bitsPerSample: 16, |
|||
channels: UInt(channels) |
|||
) else { |
|||
print("创建音频流格式失败") |
|||
return |
|||
} |
|||
pushAudioStream = SPXPushAudioInputStream(audioFormat: audioStreamFormat) |
|||
} |
|||
|
|||
print("AudioStream音频配置设置完成") |
|||
} |
|||
/** |
|||
* 开始音频输入 |
|||
*/ |
|||
public func startAudioRecord(audioSourceType: AudioSourceType = .microphone, audioDataCallback: AudioDataCallback?) { |
|||
_isWriting=true |
|||
self.audioSourceType = audioSourceType; |
|||
self.audioDataCallback = audioDataCallback |
|||
print("startAudioRecord=audioSourceType\(audioSourceType)") |
|||
switch audioSourceType { |
|||
case .microphone: |
|||
runMicrophoneCapture() |
|||
case .external: |
|||
runExternalCapture() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 开启音频写入线程 |
|||
* 修复:使用userInitiated QoS避免优先级反转 |
|||
*/ |
|||
public func startWriteThread() { |
|||
// 使用userInitiated QoS匹配音频录制线程的优先级 |
|||
writeThread = DispatchQueue(label: "audio.stream.writer", qos: .userInitiated) |
|||
|
|||
writeThread?.async { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
while self.isRunning { |
|||
if !self._isWriting { |
|||
print("startWriteThread 111") |
|||
usleep(10_000) |
|||
continue |
|||
} |
|||
print("startWriteThread 222") |
|||
guard let dataToWrite = self.writeQueue.take() else { continue } |
|||
|
|||
do { |
|||
if let callback = self.audioDataCallback { |
|||
callback.onAudio(dataToWrite) |
|||
} |
|||
try self.pushAudioStream?.write(dataToWrite) |
|||
if self.recordfile != nil { |
|||
print("写入recordfile数据长度: \(dataToWrite.count)") |
|||
recordfile?.saveAudioDataToWav(dataToWrite) |
|||
} |
|||
print("写入数据长度: \(dataToWrite.count)") |
|||
} catch { |
|||
print("推送音频数据失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 向音频流写入音频数据 |
|||
* 仅当音频源设置为external时有效 |
|||
* |
|||
* @param data 音频数据字节数组 |
|||
*/ |
|||
public func saveAudioDataTo(data: Data) { |
|||
// 通过父类引用调用方法 |
|||
if audioSourceType != .external || !(parentHelper?.isContinuousRecognitionActive() ?? false) { |
|||
return |
|||
} |
|||
// print("外部data=\(data)") |
|||
// 放入队列,由写线程写入 |
|||
writeQueue.put(data) |
|||
} |
|||
|
|||
// 私有方法:启动麦克风捕获 |
|||
/** |
|||
* 启动麦克风捕获 |
|||
* 优化:复用已初始化的音频引擎,减少启动延迟 |
|||
*/ |
|||
private func runMicrophoneCapture() { |
|||
do { |
|||
// 【新增】首先配置音频会话 |
|||
try audioSession.setCategory( |
|||
.playAndRecord, |
|||
mode: .measurement, // 使用 measurement 模式获得最佳录音质量 |
|||
options: [.allowBluetooth, .defaultToSpeaker] |
|||
) |
|||
|
|||
// 【新增】请求麦克风权限(如果尚未授权) |
|||
if audioSession.recordPermission != .granted { |
|||
audioSession.requestRecordPermission { granted in |
|||
if !granted { |
|||
print("麦克风权限被拒绝") |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 【新增】激活音频会话(在配置音频引擎之前) |
|||
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) |
|||
|
|||
// 检查音频引擎是否已初始化,避免重复创建 |
|||
if audioEngine == nil { |
|||
audioEngine = AVAudioEngine() |
|||
} |
|||
|
|||
// 如果音频引擎正在运行,先停止 |
|||
if audioEngine?.isRunning == true { |
|||
audioEngine?.stop() |
|||
} |
|||
|
|||
// 获取音频输入节点(麦克风) |
|||
guard let inputNode = audioEngine?.inputNode else { |
|||
throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法获取音频输入节点"]) |
|||
} |
|||
|
|||
// 移除之前的音频处理块,避免重复添加 |
|||
inputNode.removeTap(onBus: 0) |
|||
|
|||
// 获取硬件支持的原始音频格式 |
|||
let hardwareFormat = inputNode.inputFormat(forBus: 0) |
|||
|
|||
// iOS 13+ 启用语音处理 |
|||
if #available(iOS 13.0, *) { |
|||
try inputNode.setVoiceProcessingEnabled(true) |
|||
} |
|||
|
|||
// 检查目标音频格式和转换器是否可用 |
|||
guard let targetFormat = audioFormat, // 外部定义的期望音频格式 |
|||
let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else { |
|||
throw NSError(domain: "AudioSetup", code: 2) |
|||
} |
|||
|
|||
// 在输入节点上安装录音回调 |
|||
inputNode.installTap(onBus: 0, |
|||
bufferSize: UInt32(bufferSize), // 每次回调的缓冲区大小 |
|||
format: hardwareFormat) { // 使用原始硬件格式 |
|||
[weak self] buffer, time in // 弱引用避免循环引用 |
|||
|
|||
// 确保实例存在且正在写入状态 |
|||
guard let self = self, self._isWriting else { return } |
|||
|
|||
// 创建目标格式的音频缓冲区 |
|||
let convertedBuffer = AVAudioPCMBuffer( |
|||
pcmFormat: targetFormat, |
|||
// 计算转换后的帧容量(考虑采样率差异) |
|||
frameCapacity: AVAudioFrameCount( |
|||
targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate |
|||
) |
|||
)! |
|||
|
|||
var error: NSError? |
|||
// 执行音频格式转换 |
|||
let status = converter.convert( |
|||
to: convertedBuffer, |
|||
error: &error, |
|||
withInputFrom: { inNumPackets, outStatus in |
|||
outStatus.pointee = .haveData // 标记有数据可用 |
|||
return buffer // 返回原始音频数据 |
|||
} |
|||
) |
|||
|
|||
// 转换成功且无错误 |
|||
if status == .haveData, error == nil { |
|||
// 将音频缓冲区转换为二进制数据 |
|||
let data = self.audioBufferToData(convertedBuffer) |
|||
// 将数据放入写入队列(后续处理) |
|||
self.writeQueue.put(data) |
|||
} |
|||
} |
|||
|
|||
// 激活音频会话(允许录音) |
|||
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) |
|||
|
|||
try audioEngine?.start() |
|||
} catch { |
|||
print("麦克风启动失败: \(error)") |
|||
// 【新增】添加详细错误处理 |
|||
if let nsError = error as NSError? { |
|||
print("错误域: \(nsError.domain), 错误代码: \(nsError.code)") |
|||
print("错误描述: \(nsError.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
private func audioBufferToData(_ buffer: AVAudioPCMBuffer) -> Data { |
|||
let frameLength = Int(buffer.frameLength) |
|||
let channelCount = 1 |
|||
let dataLength = frameLength * channelCount * MemoryLayout<Int16>.size |
|||
|
|||
// Handle 16-bit integer format |
|||
if let int16Data = buffer.int16ChannelData { |
|||
return Data( |
|||
bytes: int16Data.pointee, |
|||
count: dataLength |
|||
) |
|||
} |
|||
// Handle float format |
|||
else if let floatData = buffer.floatChannelData { |
|||
var int16Array = [Int16](repeating: 0, count: frameLength) |
|||
let floatBuffer = floatData.pointee |
|||
|
|||
for i in 0..<frameLength { |
|||
let sample = floatBuffer[i] |
|||
let clamped = max(-1.0, min(sample, 1.0)) |
|||
let scaled = clamped * Float(Int16.max) |
|||
int16Array[i] = Int16(scaled) |
|||
} |
|||
|
|||
return Data( |
|||
bytes: int16Array, |
|||
count: dataLength |
|||
) |
|||
} |
|||
|
|||
return Data() // Fallback for unsupported formats |
|||
} |
|||
private func runExternalCapture() { |
|||
if audioSourceType == .external { |
|||
//pushAudioData(data: Data()) |
|||
|
|||
audioEngine?.pause() |
|||
//audioEngine?.inputNode.removeTap(onBus: 0) |
|||
} |
|||
} |
|||
/** |
|||
* 继续麦克风捕获 |
|||
*/ |
|||
/** |
|||
* 继续麦克风捕获 |
|||
*/ |
|||
public func resumeRecord() { |
|||
guard _isWriting==false else { return } |
|||
_isWriting = true |
|||
// 启动录音引擎 |
|||
do { |
|||
print("resumeRecord") |
|||
try audioEngine?.start() |
|||
} catch { |
|||
os_log("Failed to start audio engine: %@", log: log, type: .error, error.localizedDescription) |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
public func stopMicrophoneCapture() { |
|||
guard _isWriting==true else { return } |
|||
|
|||
_isWriting=false |
|||
audioEngine?.pause() |
|||
print("stopMicrophoneCapture") |
|||
|
|||
} |
|||
public func releaseAudioResources() { |
|||
print("释放了") |
|||
stopMicrophoneCapture() |
|||
audioEngine?.stop() |
|||
writeThread?.async { |
|||
self.writeQueue.close() |
|||
} |
|||
writeThread = nil |
|||
audioEngine?.inputNode.removeTap(onBus: 0) |
|||
isRunning=false |
|||
audioEngine = nil |
|||
} |
|||
|
|||
// 音频路由管理 |
|||
public enum AudioOutputRoute { |
|||
case speaker |
|||
case receiver |
|||
case bluetooth |
|||
} |
|||
public func setAudioOutputRoute(_ route: AudioOutputRoute) { |
|||
|
|||
do { |
|||
// |
|||
if self._isWriting { |
|||
print("当前正在识别") |
|||
try audioEngine?.pause() |
|||
// 先停用以避免冲突 |
|||
try audioSession.setActive(false) |
|||
|
|||
} |
|||
print("setAudioOutputRoutecurrent,route=\(route)") |
|||
switch route { |
|||
case .speaker: |
|||
print("setAudioOutputRoutecurrent,speaker") |
|||
// 使用扬声器时必须用videoChat模式 |
|||
try audioSession.setCategory( |
|||
.playAndRecord, |
|||
mode: .videoChat, |
|||
options: [.defaultToSpeaker] |
|||
) |
|||
try audioSession.overrideOutputAudioPort(.speaker) |
|||
|
|||
case .receiver: |
|||
// 听筒模式使用voiceChat节省资源 |
|||
try audioSession.setCategory( |
|||
.playAndRecord, |
|||
mode: .voiceChat, |
|||
options: [.allowBluetooth] |
|||
) |
|||
try audioSession.overrideOutputAudioPort(.none) |
|||
|
|||
case .bluetooth: |
|||
// 完整蓝牙设备支持 |
|||
try audioSession.setCategory( |
|||
.playAndRecord, |
|||
mode: .voiceChat, |
|||
options: [.allowBluetooth, .allowBluetoothA2DP] |
|||
) |
|||
try audioSession.overrideOutputAudioPort(.none) |
|||
// 不需要override,系统自动路由 |
|||
} |
|||
self.currentRoute = route |
|||
if self._isWriting { |
|||
|
|||
// 重新激活 |
|||
try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) |
|||
|
|||
try audioEngine?.start() |
|||
// writeThread?.async { |
|||
// self.writeQueue.close() |
|||
// } |
|||
} |
|||
} catch { |
|||
print("路由切换失败: \(error)") |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
/** |
|||
* 音频数据回调接口 |
|||
*/ |
|||
public protocol AudioDataCallback { |
|||
/** |
|||
* 音频数据回调 |
|||
* @param data 音频数据 |
|||
*/ |
|||
func onAudio(_ data: Data) |
|||
} |
|||
|
|||
/** |
|||
* 检查是否正在写入 |
|||
* @return 是否正在写入音频数据 |
|||
*/ |
|||
public func isWriting() -> Bool { |
|||
return _isWriting |
|||
} |
|||
|
|||
} |
|||
// MARK: - iOS版LinkedBlockingQueue实现 |
|||
private class LinkedBlockingQueue<T> { |
|||
private var queue: [T] = [] |
|||
private var isClosed = false |
|||
|
|||
// 使用DispatchSemaphore实现阻塞行为 |
|||
private let availableItems: DispatchSemaphore |
|||
private let queueLock = NSLock() |
|||
|
|||
init() { |
|||
self.availableItems = DispatchSemaphore(value: 0) |
|||
} |
|||
|
|||
/** |
|||
* 阻塞式获取元素(等价于Android的take()) |
|||
* @return 队列中的元素,如果队列已关闭则返回nil |
|||
*/ |
|||
func take() -> T? { |
|||
// 等待可用元素(阻塞直到有元素或队列关闭) |
|||
availableItems.wait() |
|||
|
|||
queueLock.lock() |
|||
defer { queueLock.unlock() } |
|||
|
|||
// 检查队列是否已关闭 |
|||
if isClosed && queue.isEmpty { |
|||
return nil |
|||
} |
|||
|
|||
// 获取第一个元素 |
|||
guard !queue.isEmpty else { |
|||
return nil |
|||
} |
|||
|
|||
return queue.removeFirst() |
|||
} |
|||
|
|||
/** |
|||
* 添加元素(等价于Android的put()) |
|||
* @param item 要添加的元素 |
|||
*/ |
|||
func put(_ item: T) { |
|||
queueLock.lock() |
|||
defer { queueLock.unlock() } |
|||
|
|||
// 检查队列是否已关闭 |
|||
if isClosed { |
|||
return |
|||
} |
|||
|
|||
// 添加元素(无容量限制,与Android一致) |
|||
queue.append(item) |
|||
|
|||
// 通知有新元素可用 |
|||
availableItems.signal() |
|||
} |
|||
|
|||
/** |
|||
* 关闭队列 |
|||
* 关闭后不能再添加新元素,但可以继续取出已有元素 |
|||
*/ |
|||
func close() { |
|||
queueLock.lock() |
|||
defer { queueLock.unlock() } |
|||
|
|||
isClosed = true |
|||
|
|||
// 唤醒所有等待的take()操作 |
|||
for _ in 0..<100 { // 假设最多100个等待者 |
|||
availableItems.signal() |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - 协议定义 |
|||
|
|||
Loading…
Reference in new issue