From 7d0ba4daef130d6b51511ce4af828830006fe796 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Fri, 15 Aug 2025 18:44:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BF=BB=E8=AF=91=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bindings/translation_history_binding.dart | 8 +- .../controllers/record_timer_controller.dart | 93 ++ .../translation_audiohistory_controller.dart | 179 +++ .../controllers/translation_controller.dart | 1085 +++++--------- .../translation_detail_controller.dart | 82 +- .../translation_history_controller.dart | 503 +++++-- .../models/translation_models.dart | 76 + .../views/translation_detail_view.dart | 3 +- .../views/translation_history_view.dart | 6 +- .../translation/views/translation_view.dart | 1 + .../ios/Classes/BleAccessorySetup.swift | 305 ---- .../ble_service/ios/Classes/BleConst.swift | 93 -- .../ble_service/ios/Classes/BleService.swift | 1266 ----------------- .../ios/Classes/BleServicePlugin.h | 4 - .../ios/Classes/BleServicePlugin.m | 12 - .../ios/Classes/SwiftBleServicePlugin.swift | 374 ----- .../ios/Classes/SwiftOpusAudioProcessor.swift | 239 ---- .../ios/Classes/XCFrameworkExample.swift | 1 - 18 files changed, 1081 insertions(+), 3249 deletions(-) create mode 100644 lib/modules/translation/controllers/record_timer_controller.dart create mode 100644 lib/modules/translation/controllers/translation_audiohistory_controller.dart create mode 100644 lib/modules/translation/models/translation_models.dart delete mode 100644 local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift delete mode 100644 local_plugins/ble_service/ios/Classes/BleConst.swift delete mode 100644 local_plugins/ble_service/ios/Classes/BleService.swift delete mode 100644 local_plugins/ble_service/ios/Classes/BleServicePlugin.h delete mode 100644 local_plugins/ble_service/ios/Classes/BleServicePlugin.m delete mode 100644 local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift delete mode 100644 local_plugins/ble_service/ios/Classes/SwiftOpusAudioProcessor.swift delete mode 100644 local_plugins/ble_service/ios/Classes/XCFrameworkExample.swift diff --git a/lib/modules/translation/bindings/translation_history_binding.dart b/lib/modules/translation/bindings/translation_history_binding.dart index ce08077c0..613b525f6 100644 --- a/lib/modules/translation/bindings/translation_history_binding.dart +++ b/lib/modules/translation/bindings/translation_history_binding.dart @@ -1,11 +1,11 @@ import 'package:get/get.dart'; -import '../controllers/translation_history_controller.dart'; +import '../controllers/translation_audiohistory_controller.dart'; class TranslationHistoryBinding extends Bindings { @override void dependencies() { - Get.lazyPut( - () => TranslationHistoryController(), + Get.lazyPut( + () => TranslationAudioHistoryController(), ); } -} \ No newline at end of file +} diff --git a/lib/modules/translation/controllers/record_timer_controller.dart b/lib/modules/translation/controllers/record_timer_controller.dart new file mode 100644 index 000000000..bd779d319 --- /dev/null +++ b/lib/modules/translation/controllers/record_timer_controller.dart @@ -0,0 +1,93 @@ +import 'dart:async'; +import 'package:get/get.dart'; +import '../../../core/utils/logger.dart'; + +/// 录音计时器管理类 +/// 负责管理录音过程中的计时功能 +class RecordTimerController extends GetxController { + static const String TAG = 'RecordTimerController'; + + // 计时器相关状态 + final RxString recordDurationText = '00:00'.obs; + Timer? _recordTimer; + int _seconds = 0; + bool _isTimerPaused = false; + + /// 开始计时器 + /// 从00:00开始计时,每秒更新一次显示 + void startTimer() { + _seconds = 0; + recordDurationText.value = '00:00'; + _recordTimer = Timer.periodic(Duration(seconds: 1), (_) { + _seconds++; + final minutes = (_seconds ~/ 60).toString().padLeft(2, '0'); + final seconds = (_seconds % 60).toString().padLeft(2, '0'); + recordDurationText.value = '$minutes:$seconds'; + }); + Logger.info('录音计时器已开始'); + } + + /// 暂停计时器 + /// 保持当前时间显示,停止计时 + void pauseTimer() { + if (_recordTimer != null && !_isTimerPaused) { + _recordTimer?.cancel(); + _recordTimer = null; + _isTimerPaused = true; + Logger.info('录音计时器已暂停'); + } + } + + /// 恢复计时器 + /// 从暂停的时间点继续计时 + void resumeTimer() { + if (_isTimerPaused) { + _recordTimer = Timer.periodic(Duration(seconds: 1), (_) { + _seconds++; + final minutes = (_seconds ~/ 60).toString().padLeft(2, '0'); + final seconds = (_seconds % 60).toString().padLeft(2, '0'); + recordDurationText.value = '$minutes:$seconds'; + }); + _isTimerPaused = false; + Logger.info('录音计时器已恢复'); + } + } + + /// 重置计时器 + /// 停止计时并重置为00:00 + void resetTimer() { + _recordTimer?.cancel(); + _recordTimer = null; + _seconds = 0; + _isTimerPaused = false; + recordDurationText.value = '00:00'; + Logger.info('录音计时器已重置'); + } + + /// 停止计时器 + /// 完全停止计时器并重置状态 + void stopTimer() { + _recordTimer?.cancel(); + _recordTimer = null; + _isTimerPaused = false; + recordDurationText.value = '00:00'; + _seconds = 0; + Logger.info('录音计时器已停止'); + } + + /// 获取当前计时秒数 + int get currentSeconds => _seconds; + + /// 获取当前是否暂停状态 + bool get isPaused => _isTimerPaused; + + /// 获取当前是否正在计时 + bool get isRunning => _recordTimer != null && !_isTimerPaused; + + /// 释放资源 + void dispose() { + _recordTimer?.cancel(); + _recordTimer = null; + Logger.info('录音计时器管理器已释放'); + } +} diff --git a/lib/modules/translation/controllers/translation_audiohistory_controller.dart b/lib/modules/translation/controllers/translation_audiohistory_controller.dart new file mode 100644 index 000000000..f1d157c72 --- /dev/null +++ b/lib/modules/translation/controllers/translation_audiohistory_controller.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../controllers/translation_controller.dart'; +import 'dart:async'; + +import '../models/translation_models.dart'; // 添加异步支持 + +class TranslationAudioHistoryController extends GetxController { + // 引用主翻译控制器,以访问历史记录数据 + final TranslationController translationController = + Get.find(); + + // 按日期分组的历史记录 + final Rx>> groupedHistory = + Rx>>({}); + + // 排序后的日期列表 + final RxList sortedDates = [].obs; + + // 会话映射表,用于存储每个会话的所有项目 + final Rx>> sessionMap = + Rx>>({}); + + // 加载状态 + final isLoading = true.obs; + + @override + void onInit() { + super.onInit(); + // 确保所有翻译操作都已停止 + translationController.stopAll(); + // 异步加载数据,避免阻塞主线程 + loadHistoryData(); + } + + // 异步加载历史数据 + Future loadHistoryData() async { + isLoading.value = true; + + // 使用 compute 或 Future.microtask 在后台处理数据 + await Future.microtask(() { + _groupHistoryByDate(); + _groupHistoryBySession(); + }); + + isLoading.value = false; + } + + // 按日期对历史记录进行分组 + void _groupHistoryByDate() { + final Map> newGroupedHistory = {}; + + // 获取完整的历史记录,而不仅仅是当前显示的记录 + final List allHistory = + translationController.getAllHistory(); + + // 筛选出每个会话的第一条记录 + final List firstItemsOfSessions = + allHistory.where((item) => item.isFirstInSession).toList(); + + for (var item in firstItemsOfSessions) { + final DateTime date = item.timestamp; + String key; + + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final itemDate = DateTime(date.year, date.month, date.day); + + if (itemDate == today) { + key = 'TODAY'; + } else { + key = DateFormat('yyyy-MM-dd').format(date); + } + + if (!newGroupedHistory.containsKey(key)) { + newGroupedHistory[key] = []; + } + + newGroupedHistory[key]!.add(item); + } + + // 对每个日期组内的项目按时间倒序排序 + newGroupedHistory.forEach((key, items) { + items.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + }); + + // 更新分组后的历史记录 + groupedHistory.value = newGroupedHistory; + + // 按日期排序 + final List dates = newGroupedHistory.keys.toList() + ..sort((a, b) { + if (a == 'TODAY') return -1; + if (b == 'TODAY') return 1; + if (a == '1 DAY AGO') return -1; + if (b == '1 DAY AGO') return 1; + if (a == '2 DAYS AGO') return -1; + if (b == '2 DAYS AGO') return 1; + return b.compareTo(a); // 降序排列其他日期 + }); + + sortedDates.value = dates; + } + + // 按会话ID对历史记录进行分组 + void _groupHistoryBySession() { + final Map> newSessionMap = {}; + + // 获取完整的历史记录进行会话分组 + final List allHistory = + translationController.getAllHistory(); + + for (var item in allHistory) { + if (!newSessionMap.containsKey(item.sessionId)) { + newSessionMap[item.sessionId] = []; + } + + newSessionMap[item.sessionId]!.add(item); + } + + // 对每个会话内的项目按时间升序排序 + newSessionMap.forEach((key, items) { + items.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + }); + + // 更新会话映射表 + sessionMap.value = newSessionMap; + } + + // 获取指定会话的所有翻译项 + List getSessionItems(String sessionId) { + return sessionMap.value[sessionId] ?? []; + } + + // 播放翻译项 + void playTranslationItem(TranslationItem item) { + translationController.playTranslationItem(item); + } + + // 播放指定文本 + void playText(String text) { + translationController.playTranslatedText(text); + } + + // 检查是否有历史记录 + bool get hasHistory => translationController.translationHistory.isNotEmpty; + + // 删除指定会话的历史记录 + Future deleteSessionHistory(String sessionId) async { + try { + // 调用主控制器的删除方法 + translationController.deleteSessionHistory(sessionId); + + // 重新加载和分组历史数据 + await loadHistoryData(); + + // 显示删除成功提示 + Get.snackbar( + '删除成功', + '已删除该会话的所有翻译记录', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], + colorText: Get.isDarkMode ? Colors.white : Colors.black, + duration: const Duration(seconds: 2), + ); + } catch (e) { + // 显示删除失败提示 + Get.snackbar( + '删除失败', + '删除会话记录时发生错误: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red[400], + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + } +} diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index a427784c0..7041f4c6a 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -20,6 +20,9 @@ import '../../../core/utils/logger.dart'; import '../../../data/services/asr_service.dart'; import '../../../data/services/translation_usage_service.dart'; import '../../../data/services/ble_manager.dart'; +import 'record_timer_controller.dart'; +import 'translation_history_controller.dart'; +import '../models/translation_models.dart'; // 切换音频输出设备的枚举 enum AudioOutputDevice { @@ -59,25 +62,11 @@ class TranslationController extends GetxController { bool _audioSourceType = false; //是否开启播放 bool isPlayback = false; - //录音计时器 - final RxString recordDurationText = '00:00'.obs; - //记录计时器 - Timer? _recordTimer; - //秒 - int _seconds = 0; - //暂停状态记录计时器是否暂停 - bool _isTimerPaused = false; + // 存储相关配置 - static const String _historyKey = 'translation_history'; static const String _sourceLanguageKey = 'translation_source_language'; static const String _targetLanguageKey = 'translation_target_language'; - // 【重要改进】区分存储限制和显示限制 - // 存储限制:大幅增加到1000条,充分利用本地存储能力,几乎无限制存储 - static const int _maxStorageItems = 1000; - // 主页面显示限制:只显示最近20条,保证界面性能和用户体验 - static const int _maxDisplayItems = 20; - // 分页加载相关状态 final isLoadingMore = false.obs; // 是否正在加载更多历史记录 final hasMoreHistory = true.obs; // 是否还有更多历史记录 @@ -108,12 +97,26 @@ class TranslationController extends GetxController { // 翻译历史和当前项 String? fTFTranslationResult = ''; - final translationHistory = [].obs; String? currentSessionId; final appDocDir = getApplicationDocumentsDirectory(); var appDir; var dir; + // 使用新的管理器 + late final RecordTimerController _timerManager; + late final TranslationHistoryManager _historyManager; + // 提供访问器 + RxString get recordDurationText => _timerManager.recordDurationText; + RxList get translationHistory => + _historyManager.translationHistory; + ScrollController get scrollController => _historyManager.scrollController; + // 其他 + StreamSubscription? _recognitionSubscription; + StreamSubscription? _voiceInteractionSubscription; + Timer? _translationDebounceTimer; + // 获取支持的语言列表 + Map get supportedLanguages => + _languageManager.getChineseNameToAsrCodeMap(); // 最新的语音识别文本 final latestRecognitionText = ''.obs; @@ -144,81 +147,67 @@ class TranslationController extends GetxController { } } - /// 暂停计时器 - void pauseRecordTimer() { - if (_recordTimer != null && !_isTimerPaused) { - _recordTimer?.cancel(); - _recordTimer = null; - _isTimerPaused = true; - Logger.info('录音计时器已暂停'); + // 更新计时器相关方法 + void pauseRecordTimer() => _timerManager.pauseTimer(); + void resumeRecordTimer() => _timerManager.resumeTimer(); + void resetRecordTimer() => _timerManager.resetTimer(); + + Future setActiveSpeaker(int id) async { + activeSpeaker.value = id; + await _asrService.startContinuousRecognition(_audioSourceType); + + if (activeSpeaker.value != 0) { + await _handleActiveSpeakerOn(id); + } else { + await _handleActiveSpeakerOff(); } } - /// 恢复计时器 - void resumeRecordTimer() { - if (_isTimerPaused) { - _recordTimer = Timer.periodic(Duration(seconds: 1), (_) { - _seconds++; - final minutes = (_seconds ~/ 60).toString().padLeft(2, '0'); - final seconds = (_seconds % 60).toString().padLeft(2, '0'); - recordDurationText.value = '$minutes:$seconds'; - }); - _isTimerPaused = false; - Logger.info('录音计时器已恢复'); + // 拆分出的辅助方法 + Future _handleActiveSpeakerOn(int id) async { + isPlayback = false; + isRecognizing.value = true; + + if (isRecording.value && isRecognizing.value) { + await _handleRecordingStart(); } + + _startAsrActiveTracking(); + lastActiveSpeaker.value = id; } - /// 重置计时器 - void resetRecordTimer() { - _recordTimer?.cancel(); - _recordTimer = null; - _seconds = 0; - _isTimerPaused = false; - recordDurationText.value = '00:00'; - Logger.info('录音计时器已重置'); + Future _handleRecordingStart() async { + if (lasyIsRecording.value != isRecording.value) { + lasyIsRecording.value = isRecording.value; + await startRecording(); + } else { + await _asrService.resumeRecord(); + resumeRecordTimer(); + } } - Future setActiveSpeaker(int id) async { - activeSpeaker.value = id; - await _asrService.startContinuousRecognition(_audioSourceType); - // 监听识别结果 - if (activeSpeaker.value != 0) { - isPlayback = false; - isRecognizing.value = true; - if (isRecording.value && isRecognizing.value) { - if (lasyIsRecording.value != isRecording.value) { - lasyIsRecording.value = isRecording.value; - await startRecording(); - } else { - await _asrService.resumeRecord(); - // 恢复计时器 - resumeRecordTimer(); - } - } + Future _handleActiveSpeakerOff() async { + _stopAsrActiveTracking(); + + if (isRecording.value && isRecognizing.value) { + pauseRecordTimer(); + await _asrService.pauseRecord(); + } - // 开始ASR活跃时长跟踪(面对面模式) - _startAsrActiveTracking(); + await _handleTranslationPlayback(); + } - lastActiveSpeaker.value = id; - } else { - // 停止ASR活跃时长跟踪(面对面模式) - _stopAsrActiveTracking(); - if (isRecording.value && isRecognizing.value) { - // 暂停计时器 - pauseRecordTimer(); - await _asrService.pauseRecord(); - } - if (fTFTranslationResult != '' && !isPlayback) { - isPlayback = true; - await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); - await Future.delayed(const Duration(milliseconds: 1300)); - await playTranslatedText(fTFTranslationResult!); - fTFTranslationResult = ''; - } else if ((translationHistory.isEmpty || - !translationHistory.last.isIntermediate) && - currentMode.value == 'faceToFace') { - _asrService.stopContinuousRecognition(); - } + Future _handleTranslationPlayback() async { + if (fTFTranslationResult != '' && !isPlayback) { + isPlayback = true; + await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); + await Future.delayed(const Duration(milliseconds: 1300)); + await playTranslatedText(fTFTranslationResult!); + fTFTranslationResult = ''; + } else if ((translationHistory.isEmpty || + !translationHistory.last.isIntermediate) && + currentMode.value == 'faceToFace') { + _asrService.stopContinuousRecognition(); } } @@ -238,20 +227,12 @@ class TranslationController extends GetxController { return Rx(null); } - // UI相关 - final ScrollController scrollController = ScrollController(); - - // 其他 - StreamSubscription? _recognitionSubscription; - StreamSubscription? _voiceInteractionSubscription; - Timer? _translationDebounceTimer; - // 获取支持的语言列表 - Map get supportedLanguages => - _languageManager.getChineseNameToAsrCodeMap(); - @override void onInit() { super.onInit(); + // 初始化管理器 + _timerManager = RecordTimerController(); + _historyManager = TranslationHistoryManager(); //_setupEventListeners(); // 预先检查权限,避免在用户交互时阻塞 @@ -281,6 +262,9 @@ class TranslationController extends GetxController { // 初始化服务 _initServices(); + + // 初始化历史管理器 + _historyManager.initialize(); } /// 预先检查权限 @@ -448,22 +432,10 @@ class TranslationController extends GetxController { sourceLanguageCode.value, targetLanguageCode.value ]; -// Future initialize({ -// required String subscriptionKey, -// required String region, -// required List supportedLanguages, -// required String audioSourceType, -// required String translationAccessKey, -// required String translationSecretKey, -// String translationRegion = 'cn-north-1', -// }); - // await _astService.dispose(); + // 重新初始化ASR服务以支持通话音频源 await _astService.initialize(supportedLanguages: callModeLanguages); - // 配置实时翻译参数 - await _configureCallModeTranslation(); - Logger.info('通话模式语音翻译服务初始化完成'); return; } catch (e) { @@ -471,113 +443,6 @@ class TranslationController extends GetxController { } } - // 配置通话模式的翻译参数 - Future _configureCallModeTranslation() async { - try { - // 设置通话模式的特殊配置 - // 1. 更短的识别超时时间,适应通话场景 - // 2. 更高的识别敏感度 - // 3. 噪声抑制优化 - - // 这里可以调用ASR服务的特殊配置方法 - // await _asrService.configureForCallMode( - // endSilenceTimeout: 200, // 更短的静音超时 - // noiseReduction: true, // 启用噪声抑制 - // echoCancellation: true, // 启用回声消除 - // ); - - Logger.info('通话模式翻译参数配置完成'); - } catch (e) { - Logger.error('通话模式翻译参数配置失败: ${e.toString()}'); - } - } - - // // 处理通话模式的翻译结果 - // Future _handleCallModeTranslation(String sourceText) async { - // try { - // // 在通话模式下,翻译结果可能需要特殊处理 - // // 例如:发送到蓝牙设备、显示在特定UI等 - - // final translationResult = await _translationService.translateText( - // text: sourceText, - // sourceLanguageCode: sourceLanguageCode.value, - // targetLanguageCode: targetLanguageCode.value, - // ); - - // if (translationResult != null && translationResult.isNotEmpty) { - // // 通话模式下的特殊处理 - // await _processCallModeTranslationResult(sourceText, translationResult); - // } - // } catch (e) { - // Logger.error('通话模式翻译处理失败: ${e.toString()}'); - // } - // } - - // // 处理通话模式的翻译结果 - // Future _processCallModeTranslationResult(String sourceText, String translatedText) async { - // try { - // // 1. 更新UI显示 - // final newItem = TranslationItem( - // sourceText: sourceText, - // translatedText: translatedText, - // sourceLanguageCode: sourceLanguageCode.value, - // targetLanguageCode: targetLanguageCode.value, - // timestamp: DateTime.now(), - // sessionId: currentSessionId ?? DateTime.now().millisecondsSinceEpoch.toString(), - // isFirstInSession: translationHistory.isEmpty, - // isIntermediate: false, - // ); - - // translationHistory.add(newItem); - // translationHistory.refresh(); - // _scrollToBottom(); - // saveTranslationHistory(); - - // // 2. 通话模式下可能需要将翻译结果发送到蓝牙设备 - // // 或者通过其他方式传输给通话对方 - // await _sendTranslationToCallParty(translatedText); - - // // 3. 记录统计信息 - // _recordCallModeUsageStats(sourceText, translatedText); - - // Logger.info('通话模式翻译结果处理完成: $sourceText -> $translatedText'); - // } catch (e) { - // Logger.error('通话模式翻译结果处理失败: ${e.toString()}'); - // } - // } - - // // 将翻译结果发送给通话对方 - // Future _sendTranslationToCallParty(String translatedText) async { - // try { - // // 这里可以实现将翻译结果发送给通话对方的逻辑 - // // 例如:通过蓝牙、网络等方式 - - // // 示例:通过蓝牙发送 - // await bleManager.sendTranslationResult(translatedText); - - // Logger.info('翻译结果已发送给通话对方: $translatedText'); - // } catch (e) { - // Logger.error('发送翻译结果失败: ${e.toString()}'); - // } - // } - - // // 记录通话模式的使用统计 - // void _recordCallModeUsageStats(String sourceText, String translatedText) { - // try { - // _usageService.recordTranslationApiCall( - // sourceText: sourceText, - // targetText: translatedText, - // sourceLanguage: _languageManager.getChineseNameByAsrCode(sourceLanguageCode.value) ?? '未知', - // targetLanguage: _languageManager.getChineseNameByAsrCode(targetLanguageCode.value) ?? '未知', - // mode: 'call', // 明确标记为通话模式 - // ); - - // Logger.info('通话模式使用统计已记录'); - // } catch (e) { - // Logger.error('记录通话模式统计失败: ${e.toString()}'); - // } - // } - @override void onClose() { stopAll(); @@ -585,10 +450,13 @@ class TranslationController extends GetxController { _recognitionSubscription = null; _voiceInteractionSubscription?.cancel(); _translationDebounceTimer?.cancel(); - scrollController.dispose(); restoreOtherServices(); _asrService.dispose(); _astService.dispose(); + // 释放管理器资源 + _timerManager.dispose(); + _historyManager.dispose(); + super.onClose(); } @@ -607,33 +475,23 @@ class TranslationController extends GetxController { Future startRecording() async { //isRecording.value = true; - _seconds = 0; - recordDurationText.value = '00:00'; - _recordTimer = Timer.periodic(Duration(seconds: 1), (_) { - _seconds++; - final minutes = (_seconds ~/ 60).toString().padLeft(2, '0'); - final seconds = (_seconds % 60).toString().padLeft(2, '0'); - recordDurationText.value = '$minutes:$seconds'; - }); + _timerManager.startTimer(); // 启动实际录音逻辑 // ... final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); await _asrService.enableRecord( "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); - await _astService.enableRecord( - "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav"); + if (currentMode.value == 'call') { + await _astService.enableRecord( + "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav"); + } return; } Future stopRecording() async { - _recordTimer?.cancel(); - _recordTimer = null; - isRecording.value = false; - _isTimerPaused = false; - recordDurationText.value = '00:00'; - _seconds = 0; - + // 停止计时器 + _timerManager.stopTimer(); _asrService.stopRecord(true); lasyIsRecording.value = false; Logger.info('录音已停止,计时器已重置'); @@ -642,71 +500,93 @@ class TranslationController extends GetxController { // 开始语音识别 Future startRecognition() async { Logger.info('开始语音识别'); - // 等待权限请求完成并获取结果 - final bool record = await requestRecordPermission(); - // 如果权限被拒绝,直接返回 + if (!await _checkPermissions()) return; + if (isRecognizing.value) return; + + try { + await _initializeSession(); + await _configureAudioMode(); + await _startAsrService(); + await _finalizeRecognitionStart(); + } catch (e) { + isRecognizing.value = false; + Logger.error('启动语音识别失败: ${e.toString()}'); + } + } + + // 拆分出的辅助方法 + Future _checkPermissions() async { + final bool record = await requestRecordPermission(); if (!record) { Logger.d("Permission", "录音权限被拒绝,无法开始录音"); - return; + return false; } + return true; + } - if (isRecognizing.value) return; + Future _initializeSession() async { + currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); try { - currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); + _usageService.startNewSession(); + _asrSessionStartTime = DateTime.now(); + _asrActiveDuration = Duration.zero; + _isAsrActive = false; + } catch (e) { + Logger.error('开始新会话统计失败: $e'); + } + } - // 开始新的翻译会话统计 - try { - _usageService.startNewSession(); - _asrSessionStartTime = DateTime.now(); // 记录ASR会话开始时间 - _asrActiveDuration = Duration.zero; // 重置ASR活跃时长 - _isAsrActive = false; // 重置ASR活跃状态 - } catch (e) { - Logger.error('开始新会话统计失败: $e'); - } + Future _configureAudioMode() async { + switch (currentMode.value) { + case "audioVideo": + await _configureAudioVideoMode(); + break; + case "call": + await _configureCallMode(); + break; + default: + await _configureDefaultMode(); + } + } - // 开始连续语音识别 - if (currentMode.value == "audioVideo") { - bleManager.openDecoder(); - // 发送ble音乐或者通话远端声音 - _audioSourceType = true; - isTtsEnabled.value = false; // 禁用TTS以避免干扰 - Logger.info('发送ble音乐或者通话远端声音'); - } else if (currentMode.value == "call") { - // 发送ble系统mic和dac(音乐或者通话远端)声音 - bleManager.openA2DPDecoder(); - - //判断是否进入通话状态 - // if (bleManager.isCall) { - _audioSourceType = true; - isTtsEnabled.value = false; - await _astService.startContinuousTranslation(); - // } - - // await _astService.path("${dir.path}/8_mic.wav"); - Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); - } else { - // 开始连续语音识别 - _audioSourceType = false; - isTtsEnabled.value = true; - } + Future _configureAudioVideoMode() async { + bleManager.openDecoder(); + _audioSourceType = true; + isTtsEnabled.value = false; + Logger.info('发送ble音乐或者通话远端声音'); + } - if (bluetoothManager.currentDeviceRx.value != null) { - restoreOriginalAudioState(); - } else { - disableBluetoothAudio(); - } - await _asrService.startContinuousRecognition(_audioSourceType); - // 开始ASR活跃时长计时 - _startAsrActiveTracking(); - isRecognizing.value = true; - if (isRecording.value && isRecognizing.value) { - startRecording(); - } - } catch (e) { - isRecognizing.value = false; - Logger.error('启动语音识别失败: ${e.toString()}'); + Future _configureCallMode() async { + bleManager.openA2DPDecoder(); + _audioSourceType = true; + isTtsEnabled.value = false; + await _astService.startContinuousTranslation(); + Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); + } + + Future _configureDefaultMode() async { + _audioSourceType = false; + isTtsEnabled.value = true; + } + + Future _startAsrService() async { + if (bluetoothManager.currentDeviceRx.value != null) { + restoreOriginalAudioState(); + } else { + disableBluetoothAudio(); + } + + await _asrService.startContinuousRecognition(_audioSourceType); + } + + Future _finalizeRecognitionStart() async { + _startAsrActiveTracking(); + isRecognizing.value = true; + + if (isRecording.value && isRecognizing.value) { + startRecording(); } } @@ -797,20 +677,17 @@ class TranslationController extends GetxController { translationHistory.last.sessionId != currentSessionId, isIntermediate: true, ); - // 添加新项后滚动到底部 + if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { _asrService.stopContinuousRecognition(); } else { - translationHistory.add(newItem); + _historyManager.addTranslationItem(newItem); } - _scrollToBottom(); } else { // 更新当前项 - translationHistory.last.sourceText = text; translationHistory.refresh(); - // 更新后滚动到底部 - _scrollToBottom(); + _historyManager.scrollToBottom(); } // 防抖处理翻译请求 @@ -881,110 +758,13 @@ class TranslationController extends GetxController { String? translationResult; try { - String detectedSourceLanguageCode; - String detectedTargetLanguageCode; - if ((currentMode.value == 'faceToFace' && activeSpeaker.value == 1) || - (currentMode.value == 'faceToFace' && - activeSpeaker.value == 0 && - lastActiveSpeaker.value == 1)) { - detectedLanguageCode = sourceLanguageCode.value; - detectedSourceLanguageCode = sourceLanguageCode.value; - detectedTargetLanguageCode = targetLanguageCode.value; - } else if ((currentMode.value == 'faceToFace' && - activeSpeaker.value == 2) || - (currentMode.value == 'faceToFace' && - activeSpeaker.value == 0 && - lastActiveSpeaker.value == 2)) { - // 根据检测到的语言确定源语言和目标语言 - detectedLanguageCode = targetLanguageCode.value; - detectedSourceLanguageCode = targetLanguageCode.value; - detectedTargetLanguageCode = sourceLanguageCode.value; - } else { - // 根据检测到的语言确定源语言和目标语言 - final bool shouldSwap = - detectedLanguageCode == sourceLanguageCode.value; - detectedSourceLanguageCode = - shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value; - detectedTargetLanguageCode = - shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value; - } - // 调用翻译服务 - translationResult = await _translationService.translateText( - text: sourceText, - sourceLanguageCode: detectedSourceLanguageCode, - targetLanguageCode: detectedTargetLanguageCode, - ); + final languageCodes = _determineLanguageCodes(); + translationResult = await _performTranslation( + sourceText, languageCodes['source']!, languageCodes['target']!); - /// } if (translationResult != null) { - // 查找匹配的项目 - 优先使用timestamp查找 - int index = -1; - - index = translationHistory - .indexWhere((item) => item.timestamp.isAtSameMomentAs(timestamp)); - if (index != -1) { - // 更新历史项 - translationHistory[index].translatedText = translationResult; - translationHistory[index].sourceLanguageCode = - detectedSourceLanguageCode; - translationHistory[index].targetLanguageCode = - detectedTargetLanguageCode; - translationHistory.refresh(); - - // 【改进】只有在最终结果时才保存,避免中间态时的频繁保存 - if (isFinal) { - saveTranslationHistory(); - - // 记录翻译API调用统计 - 优化版(精确计费参数) - try { - final sourceLanguageName = _languageManager - .getChineseNameByAsrCode(detectedSourceLanguageCode) ?? - '未知'; - final targetLanguageName = _languageManager - .getChineseNameByAsrCode(detectedTargetLanguageCode) ?? - '未知'; - - // 使用精确的计费参数:只统计源文本字符数(翻译API的实际计费基础) - _usageService.recordTranslationApiCall( - sourceText: sourceText, // 只有源文本用于翻译API计费 - targetText: translationResult, // 目标文本用于TTS计费 - sourceLanguage: sourceLanguageName, - targetLanguage: targetLanguageName, - mode: currentMode.value, - ); - - Logger.info( - '翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName'); - - // 记录ASR统计(在翻译完成时统计ASR使用情况) - _recordAsrUsageStats(sourceLanguageName); - } catch (e) { - Logger.error('记录翻译统计失败: $e'); - } - } - - // 更新后滚动到底部 - _scrollToBottom(); - // 播放TTS - 只在最终结果时播放 - if (isFinal) { - if (isTtsEnabled.value && currentMode.value != 'faceToFace') { - await playTranslatedText(translationResult); - } else { - fTFTranslationResult = - (fTFTranslationResult ?? '') + translationResult; - print("translationResult=$translationResult"); - if (activeSpeaker.value == 0 && !isPlayback) { - isPlayback = true; - await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); - await Future.delayed(const Duration(milliseconds: 1300)); - await playTranslatedText(fTFTranslationResult!); - fTFTranslationResult = ''; - } - } - } - } else { - Logger.info('未找到匹配的历史项 $sourceText ${timestamp.toIso8601String()}'); - } + await _updateTranslationHistory( + sourceText, translationResult, timestamp, languageCodes, isFinal); } } catch (e) { Logger.error('翻译失败: ${e.toString()}'); @@ -995,6 +775,150 @@ class TranslationController extends GetxController { return translationResult; } + // 拆分出的辅助方法 + Map _determineLanguageCodes() { + String detectedSourceLanguageCode; + String detectedTargetLanguageCode; + + if (_isFaceToFaceSpeaker1()) { + detectedLanguageCode = sourceLanguageCode.value; + detectedSourceLanguageCode = sourceLanguageCode.value; + detectedTargetLanguageCode = targetLanguageCode.value; + } else if (_isFaceToFaceSpeaker2()) { + detectedLanguageCode = targetLanguageCode.value; + detectedSourceLanguageCode = targetLanguageCode.value; + detectedTargetLanguageCode = sourceLanguageCode.value; + } else { + final shouldSwap = detectedLanguageCode == sourceLanguageCode.value; + detectedSourceLanguageCode = + shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value; + detectedTargetLanguageCode = + shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value; + } + + return { + 'source': detectedSourceLanguageCode, + 'target': detectedTargetLanguageCode, + }; + } + + bool _isFaceToFaceSpeaker1() { + return (currentMode.value == 'faceToFace' && activeSpeaker.value == 1) || + (currentMode.value == 'faceToFace' && + activeSpeaker.value == 0 && + lastActiveSpeaker.value == 1); + } + + bool _isFaceToFaceSpeaker2() { + return (currentMode.value == 'faceToFace' && activeSpeaker.value == 2) || + (currentMode.value == 'faceToFace' && + activeSpeaker.value == 0 && + lastActiveSpeaker.value == 2); + } + + Future _performTranslation( + String sourceText, String sourceCode, String targetCode) async { + return await _translationService.translateText( + text: sourceText, + sourceLanguageCode: sourceCode, + targetLanguageCode: targetCode, + ); + } + + Future _updateTranslationHistory( + String sourceText, + String translationResult, + DateTime timestamp, + Map languageCodes, + bool isFinal, + ) async { + final index = translationHistory + .indexWhere((item) => item.timestamp.isAtSameMomentAs(timestamp)); + + if (index != -1) { + await _updateExistingHistoryItem( + index, translationResult, languageCodes, sourceText, isFinal); + } else { + Logger.info('未找到匹配的历史项 $sourceText ${timestamp.toIso8601String()}'); + } + } + + Future _updateExistingHistoryItem( + int index, + String translationResult, + Map languageCodes, + String sourceText, + bool isFinal, + ) async { + // 更新历史项 + translationHistory[index].translatedText = translationResult; + translationHistory[index].sourceLanguageCode = languageCodes['source']!; + translationHistory[index].targetLanguageCode = languageCodes['target']!; + translationHistory.refresh(); + + if (isFinal) { + saveTranslationHistory(); + await _recordTranslationStats( + sourceText, translationResult, languageCodes); + } + + _scrollToBottom(); + + if (isFinal) { + await _handleTtsPlayback(translationResult); + } + } + + Future _recordTranslationStats( + String sourceText, + String translationResult, + Map languageCodes, + ) async { + try { + final sourceLanguageName = + _languageManager.getChineseNameByAsrCode(languageCodes['source']!) ?? + '未知'; + final targetLanguageName = + _languageManager.getChineseNameByAsrCode(languageCodes['target']!) ?? + '未知'; + + _usageService.recordTranslationApiCall( + sourceText: sourceText, + targetText: translationResult, + sourceLanguage: sourceLanguageName, + targetLanguage: targetLanguageName, + mode: currentMode.value, + ); + + Logger.info( + '翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName'); + _recordAsrUsageStats(sourceLanguageName); + } catch (e) { + Logger.error('记录翻译统计失败: $e'); + } + } + + Future _handleTtsPlayback(String translationResult) async { + if (isTtsEnabled.value && currentMode.value != 'faceToFace') { + await playTranslatedText(translationResult); + } else { + await _handleFaceToFaceTts(translationResult); + } + } + + Future _handleFaceToFaceTts(String translationResult) async { + fTFTranslationResult = (fTFTranslationResult ?? '') + translationResult; + print("translationResult=$translationResult"); + + if (activeSpeaker.value == 0 && !isPlayback) { + isPlayback = true; + await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); + await Future.delayed(const Duration(milliseconds: 1300)); + await playTranslatedText(fTFTranslationResult!); + fTFTranslationResult = ''; + } + } + // 播放翻译文本 Future playTranslatedText(String text) async { if (text.isEmpty) return; @@ -1229,77 +1153,10 @@ class TranslationController extends GetxController { } } - // 加载翻译历史 - void loadTranslationHistory() { - final historyJson = _storage.read(_historyKey); - if (historyJson != null) { - try { - final List historyList = jsonDecode(historyJson); - final List allHistory = - historyList.map((item) => TranslationItem.fromJson(item)).toList(); - - // 【注意】存储中的记录都是完成态记录(isIntermediate = false) - // 因为保存时已经过滤掉了中间态记录,确保数据质量 - - // 初始化分页状态 - currentPage.value = 1; - isLoadingMore.value = false; - isLoadingMoreError.value = false; - - // 主页面只显示最近的20条完成态记录,保证界面性能 - if (allHistory.length > _maxDisplayItems) { - translationHistory.value = - allHistory.sublist(allHistory.length - _maxDisplayItems); - // 检查是否还有更多历史记录 - hasMoreHistory.value = true; - } else { - translationHistory.value = allHistory; - hasMoreHistory.value = false; - } - - // 设置滚动监听器 - _setupScrollListener(); - - // 🔑 reverse模式下不需要滚动到底部,ListView会自然显示最新记录 - // 参考Agent模块:使用reverse:true时不需要手动滚动到底部 - } catch (e) { - _storage.remove(_historyKey); - Logger.error('加载翻译历史失败: ${e.toString()}'); - } - } else { - // 没有历史数据时也要初始化分页状态 - hasMoreHistory.value = false; - currentPage.value = 1; - isLoadingMore.value = false; - isLoadingMoreError.value = false; - } - } - - // 保存翻译历史 - 回归原始简单实现 - // 【修复】完全回归原始保存逻辑,只保存当前内存中的完成态记录 - void saveTranslationHistory() { - try { - // 只保存当前内存中的完成态记录,过滤掉中间态记录 - final List finalRecords = - translationHistory.where((item) => !item.isIntermediate).toList(); - - // 应用存储限制,保留最新的1000条完成态记录 - List historyToSave = finalRecords; - if (finalRecords.length > _maxStorageItems) { - historyToSave = - finalRecords.sublist(finalRecords.length - _maxStorageItems); - } + void loadTranslationHistory() => _historyManager.loadTranslationHistory(); - // 保存到本地存储 - final List> historyJson = - historyToSave.map((item) => item.toJson()).toList(); - _storage.write(_historyKey, jsonEncode(historyJson)); - - Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录'); - } catch (e) { - Logger.error('保存翻译历史失败: ${e.toString()}'); - } - } + // 更新历史记录相关方法 + void saveTranslationHistory() => _historyManager.saveTranslationHistory(); // 设置滚动监听器(适配reverse模式) void _setupScrollListener() { @@ -1321,230 +1178,17 @@ class TranslationController extends GetxController { _scrollListenerSetup = true; } - // 加载更多历史记录(分页加载) - Future loadMoreHistory() async { - // 防止重复加载 - if (isLoadingMore.value || !hasMoreHistory.value) { - Logger.d(TAG, - '跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreHistory=${hasMoreHistory.value}'); - return; - } + void clearTranslationHistory() => _historyManager.clearTranslationHistory(); - try { - isLoadingMore.value = true; - isLoadingMoreError.value = false; - - final nextPage = currentPage.value + 1; - Logger.i(TAG, '正在加载第 $nextPage 页翻译历史...'); - - // 从存储中获取所有历史记录 - final historyJson = _storage.read(_historyKey); - if (historyJson != null) { - final List historyList = jsonDecode(historyJson); - final List allHistory = - historyList.map((item) => TranslationItem.fromJson(item)).toList(); - - // 计算分页参数 - final totalItems = allHistory.length; - final totalPages = (totalItems / pageSize).ceil(); - - // 修复分页索引计算:从最新记录向前分页 - // 第1页:最新20条 (index: totalItems-20 到 totalItems) - // 第2页:接下来20条 (index: totalItems-40 到 totalItems-20) - // 第3页:再接下来20条 (index: totalItems-60 到 totalItems-40) - final endIndex = totalItems - ((nextPage - 1) * pageSize); - final startIndex = totalItems - (nextPage * pageSize); - - Logger.d(TAG, - '分页计算: 总记录=$totalItems, 总页数=$totalPages, 当前页=$nextPage, startIndex=$startIndex, endIndex=$endIndex'); - - if (endIndex > 0) { - // 获取当前页的数据,确保索引不越界 - final actualStartIndex = startIndex < 0 ? 0 : startIndex; - final actualEndIndex = endIndex > totalItems ? totalItems : endIndex; - - final pageItems = - allHistory.sublist(actualStartIndex, actualEndIndex); - Logger.d(TAG, - '实际索引: start=$actualStartIndex, end=$actualEndIndex, 获取${pageItems.length}条记录'); - - if (pageItems.isNotEmpty) { - // 🔑 简化延迟,参考Agent模块(300ms vs 1000ms) - await Future.delayed(const Duration(milliseconds: 300)); - - // 🔑 reverse模式下:将新记录插入到现有记录列表的开头 - // 由于reverse显示,用户看到的顺序不会改变,无需复杂的位置保持 - final updatedHistory = [...pageItems, ...translationHistory]; - translationHistory.assignAll(updatedHistory); - - // 更新分页状态 - currentPage.value = nextPage; - // 修复hasMoreHistory判断:当startIndex <= 0时表示已经到达最早的记录 - hasMoreHistory.value = startIndex > 0; - - Logger.i(TAG, - '成功加载 ${pageItems.length} 条历史记录,当前页: $nextPage/$totalPages, 还有更多: ${hasMoreHistory.value}'); - } else { - // 没有更多记录 - hasMoreHistory.value = false; - Logger.i(TAG, '没有更多翻译历史记录'); - } - } else { - // 索引超出范围,没有更多记录 - hasMoreHistory.value = false; - Logger.i(TAG, '已到达历史记录开头'); - } - } else { - // 没有存储数据 - hasMoreHistory.value = false; - Logger.i(TAG, '存储中没有更多历史记录'); - } - } catch (e) { - Logger.e(TAG, '加载更多历史记录失败: $e'); - isLoadingMoreError.value = true; - } finally { - isLoadingMore.value = false; - } - } - - // 获取所有完成态历史记录(供历史页面使用) - // 【修复】简化实现,合并存储和内存中的完成态记录 - List getAllHistory() { - try { - final List result = []; - final Set existingIds = {}; - - // 添加存储的历史记录(存储中的记录都是完成态) - final historyJson = _storage.read(_historyKey); - if (historyJson != null) { - final List historyList = jsonDecode(historyJson); - final List storedHistory = - historyList.map((item) => TranslationItem.fromJson(item)).toList(); - - for (final item in storedHistory) { - final id = - '${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; - if (!existingIds.contains(id)) { - existingIds.add(id); - result.add(item); - } - } - } - - // 添加当前内存中的完成态记录(排除中间态记录) - for (final item in translationHistory) { - if (!item.isIntermediate) { - final id = - '${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; - if (!existingIds.contains(id)) { - existingIds.add(id); - result.add(item); - } - } - } - - // 按时间排序 - result.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - return result; - } catch (e) { - Logger.error('获取完整历史记录失败: ${e.toString()}'); - // 发生错误时,返回当前内存中的完成态记录 - return translationHistory.where((item) => !item.isIntermediate).toList(); - } - } + void deleteSessionHistory(String sessionId) => + _historyManager.deleteSessionHistory(sessionId); - // 清除翻译历史 - void clearTranslationHistory() { - translationHistory.clear(); - _storage.remove(_historyKey); - } - - // 删除指定会话的所有翻译记录 - void deleteSessionHistory(String sessionId) { - try { - // 1. 从内存中删除该会话的所有记录 - final originalCount = translationHistory.length; - translationHistory.removeWhere((item) => item.sessionId == sessionId); - final removedFromMemory = originalCount - translationHistory.length; - - // 2. 从本地存储中删除该会话的记录 - final historyJson = _storage.read(_historyKey); - int removedFromStorage = 0; - - if (historyJson != null) { - final List historyList = jsonDecode(historyJson); - final List storedHistory = - historyList.map((item) => TranslationItem.fromJson(item)).toList(); - - final originalStoredCount = storedHistory.length; - storedHistory.removeWhere((item) => item.sessionId == sessionId); - removedFromStorage = originalStoredCount - storedHistory.length; - - // 重新保存更新后的存储数据 - final List> updatedHistoryJson = - storedHistory.map((item) => item.toJson()).toList(); - _storage.write(_historyKey, jsonEncode(updatedHistoryJson)); - } - - // 3. 刷新UI显示 - translationHistory.refresh(); - - Logger.info( - '删除会话历史成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); - } catch (e) { - Logger.error('删除会话历史失败: ${e.toString()}'); - rethrow; // 重新抛出异常,让调用方处理 - } - } - - // 删除指定的单条翻译记录 - void deleteTranslationItem(String sessionId, DateTime timestamp) { - try { - // 1. 从内存中删除指定的翻译记录 - final originalCount = translationHistory.length; - translationHistory.removeWhere((item) => - item.sessionId == sessionId && - item.timestamp.millisecondsSinceEpoch == - timestamp.millisecondsSinceEpoch); - final removedFromMemory = originalCount - translationHistory.length; - - // 2. 从本地存储中删除指定的翻译记录 - final historyJson = _storage.read(_historyKey); - int removedFromStorage = 0; - - if (historyJson != null) { - final List historyList = jsonDecode(historyJson); - final List storedHistory = - historyList.map((item) => TranslationItem.fromJson(item)).toList(); - - final originalStoredCount = storedHistory.length; - storedHistory.removeWhere((item) => - item.sessionId == sessionId && - item.timestamp.millisecondsSinceEpoch == - timestamp.millisecondsSinceEpoch); - removedFromStorage = originalStoredCount - storedHistory.length; - - // 重新保存更新后的存储数据 - final List> updatedHistoryJson = - storedHistory.map((item) => item.toJson()).toList(); - _storage.write(_historyKey, jsonEncode(updatedHistoryJson)); - } - - // 3. 刷新UI显示 - translationHistory.refresh(); - - Logger.info( - '删除单条翻译记录成功: sessionId=$sessionId, timestamp=${timestamp.toIso8601String()}, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); - } catch (e) { - Logger.error('删除单条翻译记录失败: ${e.toString()}'); - rethrow; // 重新抛出异常,让调用方处理 - } - } - - // 滚动到底部 (公共方法,用于向后兼容) - void scrollToBottom({bool animate = true}) { - _scrollToBottom(animate: animate); - } + void deleteTranslationItem(String sessionId, DateTime timestamp) => + _historyManager.deleteTranslationItem(sessionId, timestamp); + List getAllHistory() => _historyManager.getAllHistory(); + Future loadMoreHistory() => _historyManager.loadMoreHistory(); + void scrollToBottom({bool animate = true}) => + _historyManager.scrollToBottom(animate: animate); /// 开始ASR活跃时长跟踪 void _startAsrActiveTracking() { @@ -1595,74 +1239,3 @@ class TranslationController extends GetxController { } } } - -// 翻译项模型 -class TranslationItem { - String sourceText; - String translatedText; - String sourceLanguageCode; - String targetLanguageCode; - final DateTime timestamp; - String sessionId; - final bool isFirstInSession; - bool isIntermediate; // 是否为中间结果 - - TranslationItem({ - required this.sourceText, - required this.translatedText, - required this.sourceLanguageCode, - required this.targetLanguageCode, - required this.timestamp, - required this.sessionId, - this.isFirstInSession = false, - this.isIntermediate = false, - }); - - factory TranslationItem.fromJson(Map json) { - DateTime timestamp; - try { - timestamp = DateTime.parse(json['timestamp']); - } catch (e) { - timestamp = DateTime.now(); - } - - return TranslationItem( - sourceText: json['sourceText'] ?? '', - translatedText: json['translatedText'] ?? '', - sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN', - targetLanguageCode: json['targetLanguageCode'] ?? 'en-US', - timestamp: timestamp, - sessionId: - json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(), - isFirstInSession: json['isFirstInSession'] ?? false, - isIntermediate: false, // 从存储加载的都是最终结果 - ); - } - - Map toJson() { - return { - 'sourceText': sourceText, - 'translatedText': translatedText, - 'sourceLanguageCode': sourceLanguageCode, - 'targetLanguageCode': targetLanguageCode, - 'timestamp': timestamp.toIso8601String(), - 'sessionId': sessionId, - 'isFirstInSession': isFirstInSession, - // 不保存isIntermediate,因为存储的都是最终结果 - }; - } - - // 获取源语言的中文名称 - String get sourceLanguageName { - final languageManager = Get.find(); - return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ?? - '未知语言'; - } - - // 获取目标语言的中文名称 - String get targetLanguageName { - final languageManager = Get.find(); - return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? - '未知语言'; - } -} diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index 12a586aab..ee283b514 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../../data/services/volcano_ai_service.dart'; import '../controllers/translation_controller.dart'; -import '../controllers/translation_history_controller.dart'; +import 'translation_audiohistory_controller.dart'; +import '../models/translation_models.dart'; // 添加这行导入 import 'package:get_storage/get_storage.dart'; import 'dart:async'; import '../../../core/utils/logger.dart'; @@ -13,25 +14,25 @@ class TranslationDetailController extends GetxController { final VolcanoAIService _aiService = Get.find(); final GetStorage _storage = GetStorage(); late final TranslationUsageService _usageService; - + // 存储键前缀 static const String _summaryKeyPrefix = 'translation_summary_'; - + // 超时设置(秒) static const int _defaultTimeoutDuration = 30; static const int _extendedTimeoutDuration = 60; - + // 状态变量 final selectedTabIndex = 0.obs; final isGeneratingSummary = false.obs; final summaryResult = ''.obs; final isLoading = true.obs; - + // 当前会话信息 late final TranslationItem currentItem; late final List sessionItems; - late final TranslationHistoryController historyController; - + late final TranslationAudioHistoryController historyController; + @override void onInit() { super.onInit(); @@ -59,7 +60,7 @@ class TranslationDetailController extends GetxController { isLoading.value = false; } } - + // 异步初始化方法 Future _asyncInitialize() async { isLoading.value = true; @@ -76,7 +77,7 @@ class TranslationDetailController extends GetxController { isLoading.value = false; } } - + // 加载保存的摘要 void loadSavedSummary() { try { @@ -88,62 +89,64 @@ class TranslationDetailController extends GetxController { Logger.error('加载摘要失败: ${e.toString()}'); } } - + // 保存摘要 void saveSummary(String summary) { if (summary.isEmpty) return; - + try { _storage.write(_getSummaryKey(), summary); } catch (e) { Logger.error('保存摘要失败: ${e.toString()}'); } } - + // 获取摘要的存储键 String _getSummaryKey() => _summaryKeyPrefix + currentItem.sessionId; - + // 切换选项卡 void switchTab(int index) => selectedTabIndex.value = index; - + // 播放翻译项 - void playTranslationItem(TranslationItem item) => historyController.playTranslationItem(item); - + void playTranslationItem(TranslationItem item) => + historyController.playTranslationItem(item); + // 播放文本 void playText(String text) => historyController.playText(text); - - // 获取合并的源文本 - String getCombinedSourceText() => sessionItems.map((e) => e.sourceText).join('\n\n'); - + String getCombinedSourceText() => + sessionItems.map((e) => e.sourceText).join('\n\n'); + // 获取合并的翻译文本 - String getCombinedTranslatedText() => sessionItems.map((e) => e.translatedText).join('\n\n'); - + String getCombinedTranslatedText() => + sessionItems.map((e) => e.translatedText).join('\n\n'); + // 使用延长的超时时间重试提取摘要 Future retryExtractSummaryWithExtendedTimeout(String text) async { return _extractSummaryInternal(text, _extendedTimeoutDuration); } - + // 提取摘要 Future extractSummary(String text) async { return _extractSummaryInternal(text, _defaultTimeoutDuration); } // 内部摘要提取方法,避免代码重复 - Future _extractSummaryInternal(String text, int timeoutDuration) async { + Future _extractSummaryInternal( + String text, int timeoutDuration) async { if (text.isEmpty) { return '无法生成摘要:文本为空'; } isGeneratingSummary.value = true; - + try { // 构建提示词 final messages = [ {'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'}, ]; - + // 系统提示词 const systemPrompt = ''' 你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。 @@ -160,24 +163,26 @@ class TranslationDetailController extends GetxController { 4. 摘要总长度应该是原文的1/3或更短 5. 确保列表中的每个要点都简明扼要,突出重点 '''; - + String result; try { // 使用超时限制 - result = await _aiService.sendMessage( + result = await _aiService + .sendMessage( messages: messages, systemPrompt: systemPrompt, - ).timeout(Duration(seconds: timeoutDuration), onTimeout: () { + ) + .timeout(Duration(seconds: timeoutDuration), onTimeout: () { throw TimeoutException('请求超时', Duration(seconds: timeoutDuration)); }); } on TimeoutException catch (_) { - final timeoutMessage = timeoutDuration == _defaultTimeoutDuration - ? '生成摘要超时,请稍后重试' + final timeoutMessage = timeoutDuration == _defaultTimeoutDuration + ? '生成摘要超时,请稍后重试' : '生成摘要超时,请检查网络连接后重试'; summaryResult.value = timeoutMessage; return summaryResult.value; } - + // 更新结果并保存 if (result.isNotEmpty) { summaryResult.value = result; @@ -201,20 +206,19 @@ class TranslationDetailController extends GetxController { isGeneratingSummary.value = false; } } - + // 删除单条翻译记录 Future deleteTranslationItem(TranslationItem item) async { try { // 调用主控制器的删除方法 - historyController.translationController.deleteTranslationItem( - item.sessionId, - item.timestamp - ); + historyController.translationController + .deleteTranslationItem(item.sessionId, item.timestamp); // 从当前会话项目列表中移除该项目 sessionItems.removeWhere((sessionItem) => sessionItem.sessionId == item.sessionId && - sessionItem.timestamp.millisecondsSinceEpoch == item.timestamp.millisecondsSinceEpoch); + sessionItem.timestamp.millisecondsSinceEpoch == + item.timestamp.millisecondsSinceEpoch); // 重新加载历史控制器的数据 await historyController.loadHistoryData(); @@ -261,4 +265,4 @@ class TranslationDetailController extends GetxController { historyController.translationController.stopAll(); super.onClose(); } -} \ No newline at end of file +} diff --git a/lib/modules/translation/controllers/translation_history_controller.dart b/lib/modules/translation/controllers/translation_history_controller.dart index 3c2184c43..b7e7d15b3 100644 --- a/lib/modules/translation/controllers/translation_history_controller.dart +++ b/lib/modules/translation/controllers/translation_history_controller.dart @@ -1,174 +1,371 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:intl/intl.dart'; -import '../controllers/translation_controller.dart'; -import 'dart:async'; // 添加异步支持 - -class TranslationHistoryController extends GetxController { - // 引用主翻译控制器,以访问历史记录数据 - final TranslationController translationController = Get.find(); - - // 按日期分组的历史记录 - final Rx>> groupedHistory = Rx>>({}); - - // 排序后的日期列表 - final RxList sortedDates = [].obs; - - // 会话映射表,用于存储每个会话的所有项目 - final Rx>> sessionMap = Rx>>({}); - - // 加载状态 - final isLoading = true.obs; - - @override - void onInit() { - super.onInit(); - // 确保所有翻译操作都已停止 - translationController.stopAll(); - // 异步加载数据,避免阻塞主线程 - loadHistoryData(); +import 'package:get_storage/get_storage.dart'; +import '../../../core/utils/logger.dart'; +import '../models/translation_models.dart'; + +/// 翻译历史记录管理类 +/// 负责管理翻译历史的存储、加载、分页等功能 +class TranslationHistoryManager extends GetxController { + static const String TAG = 'TranslationHistoryManager'; + + // 存储相关配置 + static const String _historyKey = 'translation_history'; + static const int _maxStorageItems = 1000; // 存储限制 + static const int _maxDisplayItems = 20; // 显示限制 + + final GetStorage _storage = GetStorage(); + + // 翻译历史数据 + final translationHistory = [].obs; + + // 分页加载相关状态 + final isLoadingMore = false.obs; + final hasMoreHistory = true.obs; + final currentPage = 1.obs; + final pageSize = 20; + final isLoadingMoreError = false.obs; + bool _scrollListenerSetup = false; + + // 滚动控制器 + final ScrollController scrollController = ScrollController(); + + /// 初始化历史管理器 + void initialize() { + loadTranslationHistory(); + _setupScrollListener(); } - - // 异步加载历史数据 - Future loadHistoryData() async { - isLoading.value = true; - - // 使用 compute 或 Future.microtask 在后台处理数据 - await Future.microtask(() { - _groupHistoryByDate(); - _groupHistoryBySession(); - }); - - isLoading.value = false; + + /// 加载翻译历史记录 + /// 初始化时只加载最近的显示条数,提高性能 + void loadTranslationHistory() { + final historyJson = _storage.read(_historyKey); + if (historyJson != null) { + try { + final List historyList = jsonDecode(historyJson); + final List allHistory = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); + + // 初始化分页状态 + currentPage.value = 1; + isLoadingMore.value = false; + isLoadingMoreError.value = false; + + // 主页面只显示最近的记录,保证界面性能 + if (allHistory.length > _maxDisplayItems) { + translationHistory.value = + allHistory.sublist(allHistory.length - _maxDisplayItems); + hasMoreHistory.value = true; + } else { + translationHistory.value = allHistory; + hasMoreHistory.value = false; + } + + Logger.info('翻译历史加载成功: ${translationHistory.length}条记录'); + } catch (e) { + _storage.remove(_historyKey); + Logger.error('加载翻译历史失败: ${e.toString()}'); + } + } else { + // 没有历史数据时初始化分页状态 + hasMoreHistory.value = false; + currentPage.value = 1; + isLoadingMore.value = false; + isLoadingMoreError.value = false; + } } - - // 按日期对历史记录进行分组 - void _groupHistoryByDate() { - final Map> newGroupedHistory = {}; - - // 获取完整的历史记录,而不仅仅是当前显示的记录 - final List allHistory = translationController.getAllHistory(); - - // 筛选出每个会话的第一条记录 - final List firstItemsOfSessions = allHistory - .where((item) => item.isFirstInSession) - .toList(); - - for (var item in firstItemsOfSessions) { - final DateTime date = item.timestamp; - String key; - - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - final itemDate = DateTime(date.year, date.month, date.day); - - if (itemDate == today) { - key = 'TODAY'; - } - else { - key = DateFormat('yyyy-MM-dd').format(date); + + /// 保存翻译历史记录 + /// 只保存完成态记录,过滤中间态记录 + void saveTranslationHistory() { + try { + // 只保存完成态记录 + final List finalRecords = + translationHistory.where((item) => !item.isIntermediate).toList(); + + // 应用存储限制 + List historyToSave = finalRecords; + if (finalRecords.length > _maxStorageItems) { + historyToSave = + finalRecords.sublist(finalRecords.length - _maxStorageItems); } - - if (!newGroupedHistory.containsKey(key)) { - newGroupedHistory[key] = []; + + // 保存到本地存储 + final List> historyJson = + historyToSave.map((item) => item.toJson()).toList(); + _storage.write(_historyKey, jsonEncode(historyJson)); + + Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录'); + } catch (e) { + Logger.error('保存翻译历史失败: ${e.toString()}'); + } + } + + /// 添加新的翻译记录 + /// [item] 要添加的翻译记录 + /// [scrollToBottom] 是否滚动到底部 + void addTranslationItem(TranslationItem item, {bool scrollToBottom = true}) { + translationHistory.add(item); + if (scrollToBottom) { + _scrollToBottom(); + } + } + + /// 更新翻译记录 + /// [timestamp] 记录的时间戳,用于定位记录 + /// [updater] 更新函数 + bool updateTranslationItem( + DateTime timestamp, Function(TranslationItem) updater) { + final index = translationHistory + .indexWhere((item) => item.timestamp.isAtSameMomentAs(timestamp)); + if (index != -1) { + updater(translationHistory[index]); + translationHistory.refresh(); + return true; + } + return false; + } + + /// 加载更多历史记录(分页加载) + Future loadMoreHistory() async { + if (isLoadingMore.value || !hasMoreHistory.value) { + return; + } + + try { + isLoadingMore.value = true; + isLoadingMoreError.value = false; + + final nextPage = currentPage.value + 1; + Logger.i(TAG, '正在加载第 $nextPage 页翻译历史...'); + + // 从存储中获取所有历史记录 + final historyJson = _storage.read(_historyKey); + if (historyJson != null) { + final List historyList = jsonDecode(historyJson); + final List allHistory = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); + + // 计算分页参数 + final totalItems = allHistory.length; + final endIndex = totalItems - ((nextPage - 1) * pageSize); + final startIndex = totalItems - (nextPage * pageSize); + + if (endIndex > 0) { + final actualStartIndex = startIndex < 0 ? 0 : startIndex; + final actualEndIndex = endIndex > totalItems ? totalItems : endIndex; + + final pageItems = + allHistory.sublist(actualStartIndex, actualEndIndex); + + if (pageItems.isNotEmpty) { + await Future.delayed(const Duration(milliseconds: 300)); + + // reverse模式下:将新记录插入到现有记录列表的开头 + final updatedHistory = [...pageItems, ...translationHistory]; + translationHistory.assignAll(updatedHistory); + + // 更新分页状态 + currentPage.value = nextPage; + hasMoreHistory.value = startIndex > 0; + + Logger.i(TAG, '成功加载 ${pageItems.length} 条历史记录'); + } else { + hasMoreHistory.value = false; + } + } else { + hasMoreHistory.value = false; + } + } else { + hasMoreHistory.value = false; } - - newGroupedHistory[key]!.add(item); + } catch (e) { + Logger.e(TAG, '加载更多历史记录失败: $e'); + isLoadingMoreError.value = true; + } finally { + isLoadingMore.value = false; } - - // 对每个日期组内的项目按时间倒序排序 - newGroupedHistory.forEach((key, items) { - items.sort((a, b) => b.timestamp.compareTo(a.timestamp)); - }); - - // 更新分组后的历史记录 - groupedHistory.value = newGroupedHistory; - - // 按日期排序 - final List dates = newGroupedHistory.keys.toList() - ..sort((a, b) { - if (a == 'TODAY') return -1; - if (b == 'TODAY') return 1; - if (a == '1 DAY AGO') return -1; - if (b == '1 DAY AGO') return 1; - if (a == '2 DAYS AGO') return -1; - if (b == '2 DAYS AGO') return 1; - return b.compareTo(a); // 降序排列其他日期 - }); - - sortedDates.value = dates; } - - // 按会话ID对历史记录进行分组 - void _groupHistoryBySession() { - final Map> newSessionMap = {}; - // 获取完整的历史记录进行会话分组 - final List allHistory = translationController.getAllHistory(); + /// 获取所有完成态历史记录 + List getAllHistory() { + try { + final List result = []; + final Set existingIds = {}; + + // 添加存储的历史记录 + final historyJson = _storage.read(_historyKey); + if (historyJson != null) { + final List historyList = jsonDecode(historyJson); + final List storedHistory = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); - for (var item in allHistory) { - if (!newSessionMap.containsKey(item.sessionId)) { - newSessionMap[item.sessionId] = []; + for (final item in storedHistory) { + final id = + '${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; + if (!existingIds.contains(id)) { + existingIds.add(id); + result.add(item); + } + } } - newSessionMap[item.sessionId]!.add(item); + // 添加当前内存中的完成态记录 + for (final item in translationHistory) { + if (!item.isIntermediate) { + final id = + '${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}'; + if (!existingIds.contains(id)) { + existingIds.add(id); + result.add(item); + } + } + } + + // 按时间排序 + result.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + return result; + } catch (e) { + Logger.error('获取完整历史记录失败: ${e.toString()}'); + return translationHistory.where((item) => !item.isIntermediate).toList(); } - - // 对每个会话内的项目按时间升序排序 - newSessionMap.forEach((key, items) { - items.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - }); - - // 更新会话映射表 - sessionMap.value = newSessionMap; - } - - // 获取指定会话的所有翻译项 - List getSessionItems(String sessionId) { - return sessionMap.value[sessionId] ?? []; } - - // 播放翻译项 - void playTranslationItem(TranslationItem item) { - translationController.playTranslationItem(item); + + /// 清除所有翻译历史 + void clearTranslationHistory() { + translationHistory.clear(); + _storage.remove(_historyKey); + Logger.info('翻译历史已清除'); } - - // 播放指定文本 - void playText(String text) { - translationController.playTranslatedText(text); + + /// 删除指定会话的所有翻译记录 + void deleteSessionHistory(String sessionId) { + try { + // 从内存中删除 + final originalCount = translationHistory.length; + translationHistory.removeWhere((item) => item.sessionId == sessionId); + final removedFromMemory = originalCount - translationHistory.length; + + // 从存储中删除 + final historyJson = _storage.read(_historyKey); + int removedFromStorage = 0; + + if (historyJson != null) { + final List historyList = jsonDecode(historyJson); + final List storedHistory = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); + + final originalStoredCount = storedHistory.length; + storedHistory.removeWhere((item) => item.sessionId == sessionId); + removedFromStorage = originalStoredCount - storedHistory.length; + + // 重新保存 + final List> updatedHistoryJson = + storedHistory.map((item) => item.toJson()).toList(); + _storage.write(_historyKey, jsonEncode(updatedHistoryJson)); + } + + translationHistory.refresh(); + + Logger.info( + '删除会话历史成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); + } catch (e) { + Logger.error('删除会话历史失败: ${e.toString()}'); + rethrow; + } } - - // 检查是否有历史记录 - bool get hasHistory => translationController.translationHistory.isNotEmpty; - // 删除指定会话的历史记录 - Future deleteSessionHistory(String sessionId) async { + /// 删除指定的单条翻译记录 + void deleteTranslationItem(String sessionId, DateTime timestamp) { try { - // 调用主控制器的删除方法 - translationController.deleteSessionHistory(sessionId); - - // 重新加载和分组历史数据 - await loadHistoryData(); - - // 显示删除成功提示 - Get.snackbar( - '删除成功', - '已删除该会话的所有翻译记录', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], - colorText: Get.isDarkMode ? Colors.white : Colors.black, - duration: const Duration(seconds: 2), - ); + // 从内存中删除 + final originalCount = translationHistory.length; + translationHistory.removeWhere((item) => + item.sessionId == sessionId && + item.timestamp.millisecondsSinceEpoch == + timestamp.millisecondsSinceEpoch); + final removedFromMemory = originalCount - translationHistory.length; + + // 从存储中删除 + final historyJson = _storage.read(_historyKey); + int removedFromStorage = 0; + + if (historyJson != null) { + final List historyList = jsonDecode(historyJson); + final List storedHistory = + historyList.map((item) => TranslationItem.fromJson(item)).toList(); + + final originalStoredCount = storedHistory.length; + storedHistory.removeWhere((item) => + item.sessionId == sessionId && + item.timestamp.millisecondsSinceEpoch == + timestamp.millisecondsSinceEpoch); + removedFromStorage = originalStoredCount - storedHistory.length; + + // 重新保存 + final List> updatedHistoryJson = + storedHistory.map((item) => item.toJson()).toList(); + _storage.write(_historyKey, jsonEncode(updatedHistoryJson)); + } + + translationHistory.refresh(); + + Logger.info( + '删除单条翻译记录成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条'); } catch (e) { - // 显示删除失败提示 - Get.snackbar( - '删除失败', - '删除会话记录时发生错误: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red[400], - colorText: Colors.white, - duration: const Duration(seconds: 3), - ); + Logger.error('删除单条翻译记录失败: ${e.toString()}'); + rethrow; } } -} \ No newline at end of file + + /// 滚动到底部 + void scrollToBottom({bool animate = true}) { + _scrollToBottom(animate: animate); + } + + /// 设置滚动监听器 + void _setupScrollListener() { + if (_scrollListenerSetup) return; + + scrollController.addListener(() { + // reverse模式下:检查是否滚动到底部 + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 100) { + if (hasMoreHistory.value && !isLoadingMore.value) { + Logger.d(TAG, '触发加载更多翻译历史记录'); + loadMoreHistory(); + } + } + }); + + _scrollListenerSetup = true; + } + + /// 内部滚动到底部方法 + void _scrollToBottom({bool animate = true}) { + if (scrollController.hasClients && translationHistory.isNotEmpty) { + try { + const targetPosition = 0.0; // reverse模式下最新消息在顶部 + + if (animate) { + scrollController.animateTo( + targetPosition, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } else { + scrollController.jumpTo(targetPosition); + } + } catch (e) { + Logger.error('滚动到顶部失败: ${e.toString()}'); + } + } + } + + /// 释放资源 + void dispose() { + scrollController.dispose(); + Logger.info('翻译历史管理器已释放'); + } +} diff --git a/lib/modules/translation/models/translation_models.dart b/lib/modules/translation/models/translation_models.dart new file mode 100644 index 000000000..76acdad24 --- /dev/null +++ b/lib/modules/translation/models/translation_models.dart @@ -0,0 +1,76 @@ +import 'package:get/get.dart'; +import '../../../data/services/language_manager.dart'; + +/// 翻译项模型 +/// 表示一条翻译记录的完整信息 +class TranslationItem { + String sourceText; + String translatedText; + String sourceLanguageCode; + String targetLanguageCode; + final DateTime timestamp; + String sessionId; + final bool isFirstInSession; + bool isIntermediate; // 是否为中间结果 + + TranslationItem({ + required this.sourceText, + required this.translatedText, + required this.sourceLanguageCode, + required this.targetLanguageCode, + required this.timestamp, + required this.sessionId, + this.isFirstInSession = false, + this.isIntermediate = false, + }); + + /// 从JSON创建TranslationItem实例 + factory TranslationItem.fromJson(Map json) { + DateTime timestamp; + try { + timestamp = DateTime.parse(json['timestamp']); + } catch (e) { + timestamp = DateTime.now(); + } + + return TranslationItem( + sourceText: json['sourceText'] ?? '', + translatedText: json['translatedText'] ?? '', + sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN', + targetLanguageCode: json['targetLanguageCode'] ?? 'en-US', + timestamp: timestamp, + sessionId: + json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(), + isFirstInSession: json['isFirstInSession'] ?? false, + isIntermediate: false, // 从存储加载的都是最终结果 + ); + } + + /// 转换为JSON格式 + Map toJson() { + return { + 'sourceText': sourceText, + 'translatedText': translatedText, + 'sourceLanguageCode': sourceLanguageCode, + 'targetLanguageCode': targetLanguageCode, + 'timestamp': timestamp.toIso8601String(), + 'sessionId': sessionId, + 'isFirstInSession': isFirstInSession, + // 不保存isIntermediate,因为存储的都是最终结果 + }; + } + + /// 获取源语言的中文名称 + String get sourceLanguageName { + final languageManager = Get.find(); + return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ?? + '未知语言'; + } + + /// 获取目标语言的中文名称 + String get targetLanguageName { + final languageManager = Get.find(); + return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? + '未知语言'; + } +} \ No newline at end of file diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart index 629348420..e2ec8dfbf 100644 --- a/lib/modules/translation/views/translation_detail_view.dart +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -2,12 +2,13 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/translation_controller.dart'; -import '../controllers/translation_history_controller.dart'; +import '../controllers/translation_audiohistory_controller.dart'; import '../controllers/translation_detail_controller.dart'; import 'package:intl/intl.dart'; import 'package:flutter/services.dart'; import 'package:share_plus/share_plus.dart'; import '../../../core/theme/app_colors.dart'; +import '../models/translation_models.dart'; class TranslationDetailView extends GetView { TranslationDetailView({Key? key}) : super(key: key); diff --git a/lib/modules/translation/views/translation_history_view.dart b/lib/modules/translation/views/translation_history_view.dart index fe43db5b7..54300a92d 100644 --- a/lib/modules/translation/views/translation_history_view.dart +++ b/lib/modules/translation/views/translation_history_view.dart @@ -3,13 +3,15 @@ import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:intl/intl.dart'; import '../controllers/translation_controller.dart'; -import '../controllers/translation_history_controller.dart'; +import '../controllers/translation_audiohistory_controller.dart'; import '../bindings/translation_detail_binding.dart'; +import '../models/translation_models.dart'; import 'translation_detail_view.dart'; import '../../../core/theme/app_colors.dart'; import '../../../data/services/language_manager.dart'; -class TranslationHistoryView extends GetView { +class TranslationHistoryView + extends GetView { const TranslationHistoryView({Key? key}) : super(key: key); @override diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 2998bc6ab..a5d4fbde3 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/translation_controller.dart'; +import '../models/translation_models.dart'; import '../views/translation_history_view.dart'; import '../bindings/translation_history_binding.dart'; import '../../../core/theme/app_colors.dart'; diff --git a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift deleted file mode 100644 index 67ad78c17..000000000 --- a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift +++ /dev/null @@ -1,305 +0,0 @@ -import Foundation -import CoreBluetooth -import os.log -import AVFoundation -import AccessorySetupKit -import UIKit - -class BleAccessorySetup: NSObject { - // MARK: - 属性 - static let shared = BleAccessorySetup() - - private var bleService: BleService? - private var session: ASAccessorySession? - private var isSessionActivated = false - private var isInitializing = false - private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup") - - // 杰理蓝牙设备的公司标识符 - private let deviceCompanyIdentifier: UInt16 = 0xEFAB - private let deviceDisplayName = "语音助理耳机" - - // 用于存储配对设备UUID的UserDefaults键 - private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid" - - typealias DeviceDiscoveredCallback = (UUID) -> Void - private var onDeviceDiscovered: DeviceDiscoveredCallback? - - // MARK: - 初始化 - private override init() { super.init() } - - // MARK: - 公共方法 - func setBleService(_ service: Any) { - self.bleService = service as? BleService - } - - func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) { - self.onDeviceDiscovered = callback - } - - // 处理发现的设备并连接 - func connectToAccessory(uuid: UUID) { - guard let service = bleService else { - return - } - - service.connectToAccessory(uuid: uuid) - } - - func addToReconnectQueue(uuid: UUID) { - guard let service = bleService else { - return - } - - service.addToReconnectQueue(uuid: uuid) - } - - // 从视图控制器显示设备选择器 - 主要入口点 - func showDevicePickerFromViewController(_ viewController: UIViewController) { - initializeSession() - } - - // 清除蓝牙状态(仅用于测试或重置,不会清除系统授权) - func resetBluetoothState() { - if let service = bleService { - service.disconnect() - } - } - - // MARK: - 会话管理 - func initializeSession() { - if isInitializing { - os_log("会话正在初始化中,跳过", log: logger, type: .info) - return - } - - // 如果会话已激活,检查是否有配对设备 - if session != nil && isSessionActivated { - os_log("会话已激活,检查配对设备", log: logger, type: .info) - // 会话已激活,检查是否有配对设备 - if let accessories = session?.accessories, !accessories.isEmpty { - os_log("发现 %d 个已配对设备,尝试连接", log: logger, type: .info, accessories.count) - // 有配对设备,尝试连接 - if let service = bleService { - let _ = service.getCentralManager() - - for accessory in accessories { - if let uuid = accessory.bluetoothIdentifier { - onDeviceDiscovered?(uuid) - connectToAccessory(uuid: uuid) - } - } - } - } else { - os_log("没有已配对设备,显示配对框", log: logger, type: .info) - // 没有配对设备,直接显示配对框 - showDevicePicker() - } - return - } - - os_log("开始初始化新会话", log: logger, type: .info) - isInitializing = true - - do { - session = try ASAccessorySession() - session?.activate(on: .main) { [weak self] event in - guard let self = self else { return } - self.handleSessionEvent(event) - } - } catch { - os_log("会话初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) - handleSessionInitError(error) - } - } - - func resetSession() { - session?.invalidate() - session = nil - isSessionActivated = false - isInitializing = false - } - - // MARK: - 设备选择器显示 - private func showDevicePicker() { - guard let session = session, isSessionActivated else { - os_log("无法显示配对框:会话未激活", log: logger, type: .error) - return - } - - os_log("准备显示设备配对框", log: logger, type: .info) - - let descriptor = ASDiscoveryDescriptor() - descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier) - - let deviceImage = UIImage(systemName: "headphones") ?? UIImage() - let displayItem = ASPickerDisplayItem( - name: deviceDisplayName, - productImage: deviceImage, - descriptor: descriptor - ) - - DispatchQueue.main.async { - os_log("正在显示设备配对框", log: self.logger, type: .info) - session.showPicker(for: [displayItem]) { [weak self] error in - guard let self = self else { return } - - if let error = error { - let nsError = error as NSError - os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) - } else { - os_log("设备选择器完成", log: self.logger, type: .info) - } - } - } - } - - // MARK: - 私有辅助方法 - private func handleSessionEvent(_ event: ASAccessoryEvent) { - switch event.eventType { - case .activated: - os_log("会话已激活", log: logger, type: .info) - handleSessionActivated() - - case .accessoryAdded: - os_log("发现新设备: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown") - handleAccessoryAdded(event.accessory) - - case .accessoryRemoved: - os_log("设备已移除: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown") - handleAccessoryRemoved(event.accessory) - - case .accessoryChanged: - os_log("设备状态已改变", log: logger, type: .info) - break - - case .pickerDidPresent: - os_log("配对框已显示", log: logger, type: .info) - break - - case .pickerDidDismiss: - os_log("配对框已关闭", log: logger, type: .info) - break - - case .pickerSetupFailed: - os_log("选择器设置失败: %{public}@", log: logger, type: .error, - event.error?.localizedDescription ?? "未知错误") - - default: - os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue) - break - } - } - - private func handleSessionActivated() { - isSessionActivated = true - isInitializing = false - - if let accessories = session?.accessories, !accessories.isEmpty { - if let service = bleService { - let _ = service.getCentralManager() - - for accessory in accessories { - if let uuid = accessory.bluetoothIdentifier { - onDeviceDiscovered?(uuid) - connectToAccessory(uuid: uuid) - } - } - } - } else { - showDevicePicker() - } - } - - private func handleAccessoryAdded(_ accessory: ASAccessory?) { - guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else { - return - } - - // 保存配对设备的UUID - savePairedMacAddress(uuid) - - onDeviceDiscovered?(uuid) - connectToAccessory(uuid: uuid) - } - - private func handleAccessoryRemoved(_ accessory: ASAccessory?) { - guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else { - return - } - - if let service = bleService { - service.disconnect() - } - } - - private func handleSessionInitError(_ error: Error) { - isInitializing = false - session = nil - } - - // MARK: - 蓝牙状态检查 - func isBluetoothAvailable() -> Bool { - do { - let audioSession = AVAudioSession.sharedInstance() - let outputs = audioSession.currentRoute.outputs - - for output in outputs { - if output.portType == .bluetoothA2DP || - output.portType == .bluetoothHFP || - output.portType == .bluetoothLE { - return true - } - } - } catch { - os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription) - } - - return true - } - - // MARK: - 清除配对关系 - func clearAssociations() { - resetBluetoothState() - - // 清除保存的配对设备UUID - clearPairedMacAddress() - - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - resetSession() - initializeSession() - } - #endif - } - - // MARK: - 配对设备UUID管理 - - /// 获取已配对设备的UUID字符串 - /// 返回存储的配对设备UUID,如果没有则返回nil - func getPairedMacAddress() -> String? { - let uuidString = UserDefaults.standard.string(forKey: pairedDeviceUUIDKey) - if let uuidString = uuidString, !uuidString.isEmpty { - os_log("获取已配对设备UUID: %{public}@", log: logger, type: .info, uuidString) - return uuidString - } - os_log("未找到已配对设备UUID", log: logger, type: .info) - return nil - } - - /// 保存配对设备的UUID - /// - Parameter uuid: 要保存的设备UUID - func savePairedMacAddress(_ uuid: UUID) { - let uuidString = uuid.uuidString - UserDefaults.standard.set(uuidString, forKey: pairedDeviceUUIDKey) - UserDefaults.standard.synchronize() - os_log("已保存配对设备UUID: %{public}@", log: logger, type: .info, uuidString) - } - - /// 清除已保存的配对设备UUID - private func clearPairedMacAddress() { - UserDefaults.standard.removeObject(forKey: pairedDeviceUUIDKey) - UserDefaults.standard.synchronize() - os_log("已清除配对设备UUID", log: logger, type: .info) - } -} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleConst.swift b/local_plugins/ble_service/ios/Classes/BleConst.swift deleted file mode 100644 index 41d31caa7..000000000 --- a/local_plugins/ble_service/ios/Classes/BleConst.swift +++ /dev/null @@ -1,93 +0,0 @@ -import Foundation -import CoreBluetooth - -/** - * BLE服务常量 - */ -class BleConst { - // ================== 协议相关UUID ================== - /** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */ - static let PRIMARY_SERVICE_UUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") - - /** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ - static let AUDIO_SERVICE_UUID = CBUUID(string: "0000ae00-0000-1000-8000-00805f9b34fb") - - /** 接收音频特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ - static let RECEIVE_AUDIO_CHAR_UUID = CBUUID(string: "0000ae02-0000-1000-8000-00805f9b34fb") - - /** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */ - static let WRITE_CHAR_UUID = CBUUID(string: "0000abc1-0000-1111-2222-123456789abc") - - /** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */ - static let NOTIFY_CHAR_UUID = CBUUID(string: "0000abc2-0000-1111-2222-123456789abc") - - /** 客户端特征配置描述符UUID */ - static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") - - /** BLE扫描Action */ - static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" - - // ================== 连接状态 ================== - static let STATE_DISCONNECTED = 0 - static let STATE_CONNECTING = 1 - static let STATE_CONNECTED = 2 - static let STATE_DISCONNECTING = 3 - static let STATE_ERROR = 4 - - // ================== 广播厂商信息 ================== - /** 厂商ID - 文档中定义为0xABEF */ - static let MANUFACTURER_ID: UInt16 = 0xABEF - /** 客户ID - 文档中定义为0xA001 */ - static let CUSTOMER_ID: UInt16 = 0xA001 - /** 产品ID - 文档中定义为0xA001 */ - static let PRODUCT_ID: UInt16 = 0xA001 - /** 蓝牙状态-未连接 */ - static let BT_STATE_DISCONNECTED: UInt8 = 0x00 - /** 蓝牙状态-已连接 */ - static let BT_STATE_CONNECTED: UInt8 = 0x0C - - // ================== 协议格式 ================== - /** APP请求帧头 */ - static let APP_REQUEST_HEADER: UInt8 = 0xAA - /** 设备响应帧头 */ - static let DEVICE_RESPONSE_HEADER: UInt8 = 0xBB - /** 设备主动上报帧头 */ - static let DEVICE_NOTIFY_HEADER: UInt8 = 0xCC - - // ================== 协议指令 (CMD) ================== - /** 获取耳机版本号 */ - static let CMD_GET_VERSION: UInt8 = 0x01 - /** 获取耳机产品ID */ - static let CMD_GET_PRODUCT_ID: UInt8 = 0x02 - /** 获取耳机颜色ID */ - static let CMD_GET_COLOR_ID: UInt8 = 0x03 - /** 获取耳机电量ID */ - static let CMD_GET_BATTERY_INFO: UInt8 = 0x04 - /** APP控制编解码指令 */ - static let CMD_CONTROL_CODEC: UInt8 = 0x05 - /** 音量增大 */ - static let CMD_VOLUME_UP: UInt8 = 0x06 - /** 音量减小 */ - static let CMD_VOLUME_DOWN: UInt8 = 0x07 - /** A2DP播放 */ - static let CMD_PLAY_A2DP: UInt8 = 0x08 - - /** 唤醒指令 */ - static let CMD_WAKE_UP: UInt8 = 0x11 - - // ================== 编解码控制值 ================== - /** 关闭编解码指令 */ - static let CODEC_CONTROL_CLOSE: UInt8 = 0x00 - /** 音乐或者通话远端声音 */ - static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1 - /** mic和dac(音乐或者通话远端)声音 */ - static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2 - /** 打开编码指令 */ - static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1 - /** 左声道 */ - static let AUDIO_CHANNEL_LEFT: UInt8 = 0x01 - /** 右声道 */ - static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02 - /** 立体声 */ - static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03 -} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleService.swift b/local_plugins/ble_service/ios/Classes/BleService.swift deleted file mode 100644 index 0c7398b7c..000000000 --- a/local_plugins/ble_service/ios/Classes/BleService.swift +++ /dev/null @@ -1,1266 +0,0 @@ -import Foundation -import CoreBluetooth -import os.log -import AVFoundation - -/// BLE服务类 - 专为AccessorySetupKit设备设计,支持自动重连和状态恢复 -/// -/// 核心特性: -/// 1. 外设掉电→上电后自动重连(通过pendingReconnectUUIDs队列) -/// 2. App被系统终止后,外设广播或特征通知时自动恢复连接 -/// 3. 使用CBCentralManagerOptionRestoreIdentifierKey实现系统级状态保留 -/// 4. 支持bluetooth-central后台模式,允许后台BLE操作 -/// 5. willRestoreState恢复系统保留的外设、扫描条件 -/// 6. 统一的重连队列管理,适配AccessorySetupKit工作流程 -@available(iOS 13.0, *) -public class BleService: NSObject { - // MARK: - 单例实现 - public static let shared = BleService() - - // MARK: - 回调代理协议 - public protocol Callback: AnyObject { - // 扫描相关回调 - func onScanResult(devices: [[String: Any]]) - - // 连接状态相关回调 - func onConnectionStateChanged(state: Int) - - // 数据相关回调 - func onAudioDataReceived(data: Data) - - // 唤醒信号回调 - func onWakeupSignalReceived() - - // 设备信息相关回调 - func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) - } - - // MARK: - 属性 - private var centralManager: CBCentralManager? - private var peripheralDevice: CBPeripheral? - private var writeCharacteristic: CBCharacteristic? - private var notifyCharacteristic: CBCharacteristic? - private var audioCharacteristic: CBCharacteristic? - - private var isScanning = false - private var callbacks = [Callback]() - private var connectionState = BleConst.STATE_DISCONNECTED - - - - // 状态恢复相关 - private var restoredPeripherals: [CBPeripheral] = [] - - // 自动重连队列(统一管理所有待连接的设备) - private var pendingReconnectUUIDs = Set() - - private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService") - - // MARK: - Opus音频处理器 - private var opusProcessor: SwiftOpusAudioProcessor? - - // MARK: - 初始化 - private override init() { - super.init() - - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - // iOS 18+下不立即初始化,等待AccessorySetupKit - } else { - initializeCentralManager() - } - #else - initializeCentralManager() - #endif - - // 初始化Opus音频处理器 - initializeOpusProcessor() - } - - // 延迟初始化中央管理器 - private func initializeCentralManager() { - if centralManager != nil { - os_log("🔧 CBCentralManager已存在,跳过初始化", log: logger, type: .debug) - return - } - - os_log("🔧 开始初始化CBCentralManager", log: logger, type: .info) - let options: [String: Any] = [ - CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral", - CBCentralManagerOptionShowPowerAlertKey: true - ] - - centralManager = CBCentralManager(delegate: self, queue: nil, options: options) - os_log("🔧 CBCentralManager初始化完成,等待状态更新", log: logger, type: .debug) - } - - // 初始化Opus处理器 - private func initializeOpusProcessor() { - opusProcessor = SwiftOpusAudioProcessor() - opusProcessor?.setCallback(self) - - // 立即启动Opus解码流,保持整个生命周期内活跃 - startOpusDecoding() - } - - // 启动Opus解码流 - private func startOpusDecoding() { - let opusStarted = opusProcessor?.startDecoding( - sampleRate: 16000, // 设备端Opus编码采样率 - channels: 1, // 单声道 - frameSize: 640 // 40ms at 16kHz = 640 samples - ) ?? false - - if !opusStarted { - os_log("Opus解码流启动失败", log: logger, type: .error) - } - } - - // MARK: - 回调管理 - - /// 添加回调代理 - public func addCallback(_ callback: Callback) -> Bool { - if !callbacks.contains(where: { $0 === callback }) { - callbacks.append(callback) - os_log("已添加BLE回调, 当前回调数: %d", log: logger, type: .debug, callbacks.count) - return true - } - return false - } - - /// 移除回调代理 - public func removeCallback(_ callback: Callback) -> Bool { - if let index = callbacks.firstIndex(where: { $0 === callback }) { - callbacks.remove(at: index) - os_log("已移除BLE回调, 当前回调数: %d", log: logger, type: .debug, callbacks.count) - return true - } - return false - } - - /// 清除所有回调 - public func clearCallbacks() { - callbacks.removeAll() - os_log("已清除所有BLE回调", log: logger, type: .debug) - } - - /// 设置代理(单一代理模式,用于替代addCallback) - public func setDelegate(_ delegate: Callback?) { - callbacks.removeAll() - if let delegate = delegate { - callbacks.append(delegate) - os_log("设置BLE单一代理", log: logger, type: .debug) - } else { - os_log("清除BLE单一代理", log: logger, type: .debug) - } - } - - // MARK: - 公共方法 - - /// 获取中央管理器,如果未初始化则先初始化 - public func getCentralManager() -> CBCentralManager { - if centralManager == nil { - initializeCentralManager() - } - return centralManager! - } - - /// 开始扫描BLE设备 - public func startScan() { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - // iOS 18+不使用传统扫描 - return - } - #endif - - if centralManager == nil { - initializeCentralManager() - } - - guard let manager = centralManager, manager.state == .poweredOn else { - return - } - - if isScanning { - stopScan() - } - - isScanning = true - - let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID] - manager.scanForPeripherals(withServices: serviceUUIDs, options: [ - CBCentralManagerScanOptionAllowDuplicatesKey: false - ]) - - os_log("开始扫描BLE设备...", log: logger, type: .info) - } - - /// 停止扫描 - public func stopScan() { - if isScanning && centralManager != nil { - centralManager?.stopScan() - isScanning = false - os_log("停止BLE扫描", log: logger, type: .info) - } - } - - /// 连接到指定设备 - public func connect(to peripheral: CBPeripheral) { - connectPeripheral(peripheral) - } - - /// 统一的连接方法,支持恢复和重连 - private func connectPeripheral(_ peripheral: CBPeripheral) { - guard let manager = centralManager else { - return - } - - if peripheral.state == .connected { - os_log("设备已连接,恢复会话: %@", log: logger, type: .info, peripheral.identifier.uuidString) - resumeSession(for: peripheral) - return - } - - if peripheral.state == .connecting { - os_log("设备正在连接中: %@", log: logger, type: .info, peripheral.identifier.uuidString) - return - } - - if connectionState == BleConst.STATE_CONNECTED && peripheralDevice?.identifier != peripheral.identifier { - os_log("断开当前设备,连接新设备", log: logger, type: .info) - disconnect() - } - - updateConnectionState(BleConst.STATE_CONNECTING) - - peripheralDevice = peripheral - peripheralDevice?.delegate = self - - manager.connect(peripheral, options: [ - CBConnectPeripheralOptionNotifyOnConnectionKey: true, - CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, - CBConnectPeripheralOptionNotifyOnNotificationKey: true - ]) - - os_log("开始连接到设备: %@", log: logger, type: .info, peripheral.identifier.uuidString) - } - - /// 恢复已连接设备的会话 - private func resumeSession(for peripheral: CBPeripheral) { - peripheralDevice = peripheral - peripheralDevice?.delegate = self - updateConnectionState(BleConst.STATE_CONNECTED) - - if opusProcessor?.isDecoding() != true { - startOpusDecoding() - } - - // 清空特征引用,强制重新发现 - writeCharacteristic = nil - notifyCharacteristic = nil - audioCharacteristic = nil - - os_log("🔍 恢复会话时重新发现服务和特征", log: logger, type: .info) - peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) - - os_log("已恢复设备会话: %@", log: logger, type: .info, peripheral.identifier.uuidString) - } - - /// 断开当前连接 - public func disconnect() { - guard let peripheral = peripheralDevice, - connectionState == BleConst.STATE_CONNECTED else { - return - } - - if centralManager != nil { - updateConnectionState(BleConst.STATE_DISCONNECTING) - centralManager?.cancelPeripheralConnection(peripheral) - os_log("开始断开设备连接", log: logger, type: .info) - } - } - - /// 检查是否已连接 - public func isConnected() -> Bool { - return connectionState == BleConst.STATE_CONNECTED - } - - // MARK: - 回调通知方法 - - /// 通知所有回调扫描结果 - private func notifyScanResult(devices: [[String: Any]]) { - for callback in callbacks { - callback.onScanResult(devices: devices) - } - } - - /// 通知所有回调连接状态变化 - private func notifyConnectionStateChanged(state: Int) { - for callback in callbacks { - callback.onConnectionStateChanged(state: state) - } - } - - /// 通知所有回调接收到音频数据 - private func notifyAudioDataReceived(data: Data) { - for callback in callbacks { - callback.onAudioDataReceived(data: data) - } - } - - /// 通知所有回调接收到唤醒信号 - private func notifyWakeupSignalReceived() { - for callback in callbacks { - callback.onWakeupSignalReceived() - } - } - - /// 通知所有回调接收到设备信息 - private func notifyDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { - for callback in callbacks { - callback.onDeviceInfoReceived(infoType: infoType, infoData: infoData) - } - } - - // MARK: - 设备信息和控制命令 - - /// 获取设备版本号 - func getDeviceVersion() -> Bool { - os_log("获取耳机版本号...", log: logger, type: .info) - let paramData = Data([0xFF]) - return sendCommand(BleConst.CMD_GET_VERSION, data: paramData) - } - - /// 获取产品ID - func getProductId() -> Bool { - os_log("获取耳机产品ID...", log: logger, type: .info) - let paramData = Data([0xFF]) - return sendCommand(BleConst.CMD_GET_PRODUCT_ID, data: paramData) - } - - /// 获取颜色ID - func getColorId() -> Bool { - os_log("获取耳机颜色ID...", log: logger, type: .info) - let paramData = Data([0xFF]) - return sendCommand(BleConst.CMD_GET_COLOR_ID, data: paramData) - } - - /// 获取电池信息 - func getBatteryInfo() -> Bool { - os_log("获取耳机电量信息...", log: logger, type: .info) - let paramData = Data([0xFF]) - return sendCommand(BleConst.CMD_GET_BATTERY_INFO, data: paramData) - } - - // MARK: - 编解码控制 - - /// 打开编码器 - func openEncoder() -> Bool { - os_log("🎙️ 准备打开编码(设备侧)...", log: logger, type: .info) - - let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT]) - let result = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) - - if result { - os_log("✅ 编码器命令发送成功", log: logger, type: .info) - } else { - os_log("❌ 编码器命令发送失败", log: logger, type: .error) - } - - return result - } - - /// 打开解码器 - func openDecoder() -> Bool { - os_log("打开解码...", log: logger, type: .info) - - // 确保Opus解码器处于活跃状态(如果不是则启动) - if opusProcessor?.isDecoding() != true { - startOpusDecoding() - } - - // 发送命令通知设备开始解码 - let paramData = Data([BleConst.CODEC_CONTROL_DECODE_ON, BleConst.AUDIO_CHANNEL_RIGHT]) - return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) - } - - /// 打开A2DP解码器 - func openA2DPDecoder() -> Bool { - os_log("打开A2DP解码...", log: logger, type: .info) - let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_RIGHT]) - return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) - } - - /// 关闭编解码器 - func closeCodec() -> Bool { - os_log("关闭编解码...", log: logger, type: .info) - - // 不停止Opus解码流,只发送命令通知设备关闭编解码 - let paramData = Data([BleConst.CODEC_CONTROL_CLOSE, BleConst.AUDIO_CHANNEL_RIGHT]) - return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) - } - - /// 播放A2DP - func playA2DP() -> Bool { - os_log("切换至A2DP播放模式...", log: logger, type: .info) - return sendCommand(BleConst.CMD_PLAY_A2DP) - } - - /// 音量增大 - func volumeUp() -> Bool { - os_log("设备音量增加...", log: logger, type: .info) - return sendCommand(BleConst.CMD_VOLUME_UP) - } - - /// 音量减小 - func volumeDown() -> Bool { - os_log("设备音量减小...", log: logger, type: .info) - return sendCommand(BleConst.CMD_VOLUME_DOWN) - } - - /// 设置声道模式 - func setChannelMode(_ channelMode: UInt8) -> Bool { - let modeDesc: String - switch channelMode { - case BleConst.AUDIO_CHANNEL_LEFT: - modeDesc = "左声道" - case BleConst.AUDIO_CHANNEL_RIGHT: - modeDesc = "右声道" - case BleConst.AUDIO_CHANNEL_STEREO: - modeDesc = "立体声" - default: - os_log("无效的声道模式: %d", log: logger, type: .error, channelMode) - return false - } - - os_log("设置%@模式...", log: logger, type: .info, modeDesc) - let paramData = Data([0xA1, channelMode]) - return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) - } - - // MARK: - 私有方法 - - /// 更新连接状态并通知回调 - private func updateConnectionState(_ state: Int) { - let oldState = connectionState - connectionState = state - - let stateDesc = { (state: Int) -> String in - switch state { - case BleConst.STATE_DISCONNECTED: return "DISCONNECTED" - case BleConst.STATE_CONNECTING: return "CONNECTING" - case BleConst.STATE_CONNECTED: return "CONNECTED" - case BleConst.STATE_DISCONNECTING: return "DISCONNECTING" - case BleConst.STATE_ERROR: return "ERROR" - default: return "UNKNOWN(\(state))" - } - } - - if oldState != state { - os_log("📱 BLE连接状态变化: %@ → %@", log: logger, type: .info, stateDesc(oldState), stateDesc(state)) - } - - notifyConnectionStateChanged(state: state) - } - - /// 向设备发送命令 - private func sendCommand(_ cmd: UInt8, data: Data = Data()) -> Bool { - guard let characteristic = writeCharacteristic, - connectionState == BleConst.STATE_CONNECTED else { - os_log("发送命令失败: 设备未连接", log: logger, type: .error) - return false - } - - do { - // 命令数据长度 - let cmdLength = UInt8(data.count) - - // 准备完整的命令包:帧头 + 命令ID + 长度 + 数据 + CRC - var cmdPacket = Data() - cmdPacket.append(BleConst.APP_REQUEST_HEADER) // 帧头 - cmdPacket.append(cmd) // 命令类型 - cmdPacket.append(cmdLength) // 命令长度 - - // 添加命令数据(如果有) - if !data.isEmpty { - cmdPacket.append(data) - } - - // 计算CRC校验 - 对除CRC外的所有字节 - let crc = calculateCRC8(data: cmdPacket) - cmdPacket.append(crc) - - // 记录命令详情 - var dataHexString = "" - if !data.isEmpty { - dataHexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") - } - os_log("发送命令: CMD=0x%02X, 长度=%d, 数据=[%@], CRC=0x%02X", - log: logger, type: .debug, cmd, cmdLength, dataHexString, crc) - - // 发送数据(使用无响应写入,与Android端保持一致) - peripheralDevice?.writeValue(cmdPacket, for: characteristic, type: .withoutResponse) - return true - } catch { - os_log("发送命令异常: %@", log: logger, type: .error, error.localizedDescription) - return false - } - } - - /// 计算CRC8/MAXIM校验值(与Android端保持一致) - private func calculateCRC8(data: Data) -> UInt8 { - var crc: Int = 0 - let polynomial: Int = 0x31 - - for byte in data { - crc = crc ^ (Int(byte) & 0xFF) - for _ in 0..<8 { - if (crc & 0x80) != 0 { - crc = ((crc << 1) ^ polynomial) & 0xFF - } else { - crc = (crc << 1) & 0xFF - } - } - } - - return UInt8(crc & 0xFF) - } - - /// 处理收到的音频数据 - private func processAudioData(_ data: Data) { - // 简单过滤:只处理合理大小的音频包 - guard data.count > 0 && data.count <= 1275 else { - return - } - - // 使用Opus处理器处理音频数据 - opusProcessor?.processAudioData(data) - } - - /// 处理接收到的响应数据 - private func handleResponseData(_ data: Data) { - guard data.count >= 4 else { - os_log("响应数据格式错误:数据长度过短", log: logger, type: .error) - return - } - - // 提取帧信息 - let header = data[0] - let cmd = data[1] - let length = data[2] - - // 检查帧头,确保是设备响应 - guard header == BleConst.DEVICE_RESPONSE_HEADER else { - // 如果是设备主动上报 - if header == BleConst.DEVICE_NOTIFY_HEADER { - handleNotificationData(data) - } - return - } - - // 记录响应详情 - var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") - os_log("收到设备响应: %@", log: logger, type: .debug, hexString) - - // 检查数据长度 - if data.count != Int(length) + 4 { // 帧头 + 命令 + 长度 + 数据 + CRC - os_log("设备响应数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count) - return - } - - // 计算CRC校验 - 不包括最后一个字节(CRC) - let payload = data.prefix(data.count - 1) - let calculatedCrc = calculateCRC8(data: payload) - let receivedCrc = data[data.count - 1] - - if calculatedCrc != receivedCrc { - os_log("设备响应CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) - return - } - - // 提取响应数据 - let responseData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data() - - // 根据命令类型处理设备响应 - switch cmd { - case BleConst.CMD_GET_VERSION: - var resultMap: [String: Any] = [ - "success": true, - "command": "versionInfo", - "rawData": responseData - ] - - if !responseData.isEmpty { - if let versionStr = String(data: responseData, encoding: .utf8) { - resultMap["version"] = versionStr - os_log("接收到目标版本号: %@", log: logger, type: .info, versionStr) - } else { - // 如果无法解析为字符串,以点分隔的数字表示版本 - let versionNums = responseData - .map { $0 } - .filter { $0 != 0xFF } // 过滤掉无效字节 - .map { String($0) } - .joined(separator: ".") - resultMap["version"] = versionNums - os_log("接收到目标版本号(数字格式): %@", log: logger, type: .info, versionNums) - } - } - - notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - - case BleConst.CMD_GET_PRODUCT_ID: - var resultMap: [String: Any] = [ - "success": true, - "command": "productId", - "rawData": responseData - ] - - if responseData.count >= 2 { - let productId = UInt16(responseData[0]) << 8 | UInt16(responseData[1]) - resultMap["productId"] = productId - os_log("接收到产品ID: %d", log: logger, type: .info, productId) - } else if !responseData.isEmpty { - // 单字节产品ID - let productId = UInt16(responseData[0]) - resultMap["productId"] = productId - os_log("接收到产品ID: %d", log: logger, type: .info, productId) - } - - notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - - case BleConst.CMD_GET_COLOR_ID: - var resultMap: [String: Any] = [ - "success": true, - "command": "colorInfo", - "rawData": responseData - ] - - if !responseData.isEmpty { - let colorId = Int(responseData[0]) - resultMap["colorId"] = colorId - os_log("接收到耳机颜色ID: %d", log: logger, type: .info, colorId) - } - - notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - - case BleConst.CMD_GET_BATTERY_INFO: - var resultMap: [String: Any] = [ - "success": true, - "command": "batteryInfo", - "rawData": responseData - ] - - if responseData.count >= 3 { - // 处理电池信息 - 左耳、右耳、充电仓 - let leftEarBattery = Int(responseData[0] & 0x7F) - let leftEarCharging = (responseData[0] & 0x80) != 0 - - let rightEarBattery = Int(responseData[1] & 0x7F) - let rightEarCharging = (responseData[1] & 0x80) != 0 - - let caseBattery = Int(responseData[2] & 0x7F) - let caseCharging = (responseData[2] & 0x80) != 0 - - // 添加到结果Map - resultMap["leftEarBattery"] = leftEarBattery - resultMap["leftEarCharging"] = leftEarCharging - resultMap["rightEarBattery"] = rightEarBattery - resultMap["rightEarCharging"] = rightEarCharging - resultMap["caseBattery"] = caseBattery - resultMap["caseCharging"] = caseCharging - resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - - os_log("接收到电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)", - log: logger, type: .info, - leftEarBattery, leftEarCharging ? "充电中" : "未充电", - rightEarBattery, rightEarCharging ? "充电中" : "未充电", - caseBattery, caseCharging ? "充电中" : "未充电") - } else if !responseData.isEmpty { - // 兼容单字节电量 - let batteryLevel = Int(responseData[0]) - resultMap["batteryLevel"] = batteryLevel - os_log("接收到电量信息(简化): %d%%", log: logger, type: .info, batteryLevel) - } - - notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - - case BleConst.CMD_CONTROL_CODEC: - var resultMap: [String: Any] = [ - "success": true, - "command": Int(cmd), - "rawData": responseData - ] - - if responseData.count >= 2 { - let codecStatus = Int(responseData[0]) - let channelMode = Int(responseData[1]) - - // 解码状态描述 - let statusDesc: String - switch codecStatus { - case Int(BleConst.CODEC_CONTROL_CLOSE): - statusDesc = "已关闭编解码" - case Int(BleConst.CODEC_CONTROL_DECODE_ON): - statusDesc = "已打开解码" - case Int(BleConst.CODEC_CONTROL_A2DP_PLAY): - statusDesc = "A2DP播放模式" - case Int(BleConst.CODEC_CONTROL_ENCODE_ON): - statusDesc = "已打开编码" - default: - statusDesc = "未知状态(\(codecStatus))" - } - - // 声道模式描述 - let channelDesc: String - switch channelMode { - case Int(BleConst.AUDIO_CHANNEL_LEFT): - channelDesc = "左声道" - case Int(BleConst.AUDIO_CHANNEL_RIGHT): - channelDesc = "右声道" - case Int(BleConst.AUDIO_CHANNEL_STEREO): - channelDesc = "立体声" - default: - channelDesc = "未知声道(\(channelMode))" - } - - resultMap["codecStatus"] = codecStatus - resultMap["channelMode"] = channelMode - resultMap["statusDesc"] = statusDesc - resultMap["channelDesc"] = channelDesc - - os_log("编解码控制响应: %@, %@", log: logger, type: .info, statusDesc, channelDesc) - } - - notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - - default: - // 对于未知命令,返回原始数据 - let resultMap: [String: Any] = [ - "success": true, - "command": Int(cmd), - "rawData": responseData, - "isUnknown": true - ] - os_log("收到未知命令响应: 0x%02X", log: logger, type: .debug, cmd) - notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) - } - } - - /// 处理设备主动上报的通知数据 - private func handleNotificationData(_ data: Data) { - guard data.count >= 4 else { - os_log("设备主动上报数据格式错误:数据长度过短", log: logger, type: .error) - return - } - - let header = data[0] // 0xCC - let infoType = data[1] - let length = data[2] - - var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") - os_log("收到设备主动上报: %@", log: logger, type: .debug, hexString) - - // 检查数据长度 - if data.count != Int(length) + 4 { // 帧头 + 类型 + 长度 + 数据 + CRC - os_log("设备主动上报数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count) - return - } - - // 计算CRC校验 - 不包括最后一个字节(CRC) - let payload = data.prefix(data.count - 1) - let calculatedCrc = calculateCRC8(data: payload) - let receivedCrc = data[data.count - 1] - - if calculatedCrc != receivedCrc { - os_log("设备主动上报CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) - return - } - - // 提取上报数据 - let notifyData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data() - - // 根据上报类型处理 - switch infoType { - case BleConst.CMD_GET_BATTERY_INFO: - var resultMap: [String: Any] = [ - "success": true, - "command": Int(infoType), - "rawData": notifyData, - "isNotification": true // 标记为主动上报 - ] - - if notifyData.count >= 3 { - // 处理电池信息 - 左耳、右耳、充电仓 - let leftEarBattery = Int(notifyData[0] & 0x7F) - let leftEarCharging = (notifyData[0] & 0x80) != 0 - - let rightEarBattery = Int(notifyData[1] & 0x7F) - let rightEarCharging = (notifyData[1] & 0x80) != 0 - - let caseBattery = Int(notifyData[2] & 0x7F) - let caseCharging = (notifyData[2] & 0x80) != 0 - - // 添加到结果Map - resultMap["leftEarBattery"] = leftEarBattery - resultMap["leftEarCharging"] = leftEarCharging - resultMap["rightEarBattery"] = rightEarBattery - resultMap["rightEarCharging"] = rightEarCharging - resultMap["caseBattery"] = caseBattery - resultMap["caseCharging"] = caseCharging - resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - - os_log("收到主动上报电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)", - log: logger, type: .info, - leftEarBattery, leftEarCharging ? "充电中" : "未充电", - rightEarBattery, rightEarCharging ? "充电中" : "未充电", - caseBattery, caseCharging ? "充电中" : "未充电") - } - - notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap) - - case BleConst.CMD_WAKE_UP: - os_log("📢 收到唤醒指令", log: logger, type: .info) - os_log("🔍 当前状态检查: 连接状态=%d, writeCharacteristic=%@, notifyCharacteristic=%@, audioCharacteristic=%@", - log: logger, type: .debug, - connectionState, - writeCharacteristic != nil ? "✅" : "❌", - notifyCharacteristic != nil ? "✅" : "❌", - audioCharacteristic != nil ? "✅" : "❌") - - // 接收到唤醒信号,打开编码器 (设备侧) - let openResult = openEncoder() - os_log("🎙️ 尝试打开编码器结果: %@", log: logger, type: .info, openResult ? "成功" : "失败") - notifyWakeupSignalReceived() - - default: - // 对于未知上报类型,返回原始数据 - let resultMap: [String: Any] = [ - "success": true, - "command": Int(infoType), - "rawData": notifyData, - "isUnknown": true, - "isNotification": true // 标记为主动上报 - ] - os_log("收到未知主动上报类型: 0x%02X", log: logger, type: .debug, infoType) - notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap) - } - } - - // MARK: - 通知配置 - - /// 设置特征的通知功能(与Android端行为一致) - private func setupNotification(for peripheral: CBPeripheral, characteristic: CBCharacteristic) { - // 检查特征是否支持通知 - guard characteristic.properties.contains(.notify) else { - os_log("⚠️ 特征不支持通知功能: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) - return - } - - os_log("🔧 开始配置特征通知: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) - - // iOS CoreBluetooth会自动处理CLIENT_CHAR_CONFIG_UUID描述符 - // 直接使用setNotifyValue即可,不需要手动写入描述符 - peripheral.setNotifyValue(true, for: characteristic) - - os_log("🔧 特征通知配置完成: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) - } - - /// 清理所有资源 - func cleanup() { - stopScan() - disconnect() - - // 停止Opus解码 - opusProcessor?.cleanup() - opusProcessor = nil - - // 清理状态恢复和重连队列 - restoredPeripherals.removeAll() - pendingReconnectUUIDs.removeAll() - - os_log("BleService资源已清理", log: logger, type: .debug) - } - - /// 清除重连队列 - public func clearReconnectQueue() { - pendingReconnectUUIDs.removeAll() - os_log("已清除重连队列", log: logger, type: .info) - } - - /// 获取当前重连队列状态(用于调试) - public func getReconnectQueueStatus() -> [String: Any] { - return [ - "reconnectQueueCount": pendingReconnectUUIDs.count, - "reconnectQueueUUIDs": Array(pendingReconnectUUIDs).map { $0.uuidString }, - "restoredPeripheralsCount": restoredPeripherals.count, - "connectionState": connectionState, - "centralManagerState": centralManager?.state.rawValue ?? -1 - ] - } - - // MARK: - AccessorySetupKit - - /// 在接收到AccessorySetupKit的accessoryAdded事件后调用 - func connectToAccessory(uuid: UUID) { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("开始连接设备: %{public}@", log: logger, type: .info, uuid.uuidString) - - // 确保CBCentralManager已初始化 - if centralManager == nil { - os_log("CBCentralManager未初始化,开始初始化", log: logger, type: .debug) - initializeCentralManager() - } - - // 添加到重连队列 - addToReconnectQueue(uuid: uuid) - } - #endif - } -} - -// MARK: - SwiftOpusAudioProcessor.AudioDataCallback -@available(iOS 13.0, *) -extension BleService: SwiftOpusAudioProcessor.AudioDataCallback { - /// 接收到解码后的PCM音频数据 - func onAudioDataReceived(data: Data) { - notifyAudioDataReceived(data: data) - } -} - -// MARK: - CBCentralManagerDelegate -@available(iOS 13.0, *) -extension BleService: CBCentralManagerDelegate { - public func centralManagerDidUpdateState(_ central: CBCentralManager) { - os_log("🔧 CBCentralManager状态更新: %d", log: logger, type: .info, central.state.rawValue) - - switch central.state { - case .poweredOn: - os_log("🔧 蓝牙已开启", log: logger, type: .info) - - // 注:AccessorySetupKit应用不需要恢复传统BLE扫描 - - // 恢复外设连接 - if !restoredPeripherals.isEmpty { - os_log("🔄 恢复%d个外设连接", log: logger, type: .info, restoredPeripherals.count) - - for peripheral in restoredPeripherals { - peripheral.delegate = self - - // 从重连队列中移除(避免重复处理) - pendingReconnectUUIDs.remove(peripheral.identifier) - - if peripheral.state == .connected { - os_log("🔄 外设已连接,恢复会话: %{public}@", log: logger, type: .info, - peripheral.identifier.uuidString) - resumeSession(for: peripheral) - } else { - os_log("🔄 外设未连接,开始重连: %{public}@, 状态=%d", log: logger, type: .info, - peripheral.identifier.uuidString, peripheral.state.rawValue) - connectPeripheral(peripheral) - } - } - - // 清空已处理的恢复外设 - restoredPeripherals.removeAll() - } - - // 恢复待重连的设备(只处理不在restoredPeripherals中的设备) - if !pendingReconnectUUIDs.isEmpty { - os_log("🔄 尝试重连%d个待重连设备", log: logger, type: .info, pendingReconnectUUIDs.count) - - let peripherals = central.retrievePeripherals(withIdentifiers: Array(pendingReconnectUUIDs)) - for peripheral in peripherals { - os_log("🔄 重连设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) - connectPeripheral(peripheral) - pendingReconnectUUIDs.remove(peripheral.identifier) - } - } - - case .poweredOff: - os_log("蓝牙已关闭", log: logger, type: .info) - updateConnectionState(BleConst.STATE_DISCONNECTED) - - case .resetting: - os_log("蓝牙正在重置", log: logger, type: .info) - // 无需额外处理,等待状态变更 - - case .unauthorized: - os_log("蓝牙权限未授权", log: logger, type: .error) - updateConnectionState(BleConst.STATE_ERROR) - - case .unsupported: - os_log("设备不支持蓝牙", log: logger, type: .error) - updateConnectionState(BleConst.STATE_ERROR) - - case .unknown: - os_log("蓝牙状态未知", log: logger, type: .info) - // 等待状态变更 - - @unknown default: - os_log("蓝牙未知状态: %d", log: logger, type: .info, central.state.rawValue) - break - } - } - - // MARK: - 重连管理 - - /// 添加设备到重连队列 - public func addToReconnectQueue(uuid: UUID) { - pendingReconnectUUIDs.insert(uuid) - os_log("🔄 设备已加入重连队列: %{public}@", log: logger, type: .info, uuid.uuidString) - - // 立即尝试连接(如果蓝牙已就绪) - tryReconnectDevices() - } - - /// 尝试重连队列中的设备 - private func tryReconnectDevices() { - guard let manager = centralManager, manager.state == .poweredOn else { - os_log("🔄 蓝牙未就绪,延迟重连", log: logger, type: .debug) - return - } - - guard !pendingReconnectUUIDs.isEmpty else { - return - } - - os_log("🔄 尝试重连%d个设备", log: logger, type: .info, pendingReconnectUUIDs.count) - - let peripherals = manager.retrievePeripherals(withIdentifiers: Array(pendingReconnectUUIDs)) - for peripheral in peripherals { - os_log("🔄 重连设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) - connectPeripheral(peripheral) - // 注意:不在这里移除UUID,让didConnect回调来移除 - } - } - - // 原有方法 - public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { - // 将扫描结果转换为字典格式 - var deviceInfo: [String: Any] = [ - "name": peripheral.name ?? "Unknown", - "id": peripheral.identifier.uuidString, - "rssi": RSSI.intValue - ] - - // 添加广播数据 - if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data { - deviceInfo["manufacturerData"] = manufacturerData - } - - if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { - deviceInfo["serviceUUIDs"] = serviceUUIDs.map { $0.uuidString } - } - - // 报告给回调 - notifyScanResult(devices: [deviceInfo]) - } - - public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - os_log("🔗 BLE设备连接成功: %{public}@", log: logger, type: .info, peripheral.name ?? peripheral.identifier.uuidString) - os_log("🔗 连接详情: UUID=%{public}@, 服务数=%d", log: logger, type: .debug, - peripheral.identifier.uuidString, peripheral.services?.count ?? 0) - - // 从重连队列中移除(如果存在) - pendingReconnectUUIDs.remove(peripheral.identifier) - - updateConnectionState(BleConst.STATE_CONNECTED) - - os_log("🔍 开始发现服务...", log: logger, type: .debug) - // 查找服务 - peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) - - // 确保Opus解码器处于活跃状态 - if opusProcessor?.isDecoding() != true { - startOpusDecoding() - } - } - - public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - let errorMsg = error?.localizedDescription ?? "未知错误" - os_log("❌ BLE设备连接失败: %{public}@, 错误: %{public}@", log: logger, type: .error, - peripheral.name ?? peripheral.identifier.uuidString, errorMsg) - os_log("❌ 失败详情: UUID=%{public}@", log: logger, type: .debug, peripheral.identifier.uuidString) - - updateConnectionState(BleConst.STATE_ERROR) - - // 连接失败时,立即发起新的连接 - os_log("🔄 连接失败,立即重新连接: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) - connectPeripheral(peripheral) - } - - public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - if let error = error { - os_log("⚠️ BLE设备意外断开: %{public}@, 错误: %{public}@", log: logger, type: .info, - peripheral.name ?? peripheral.identifier.uuidString, error.localizedDescription) - } else { - os_log("🔌 BLE设备正常断开: %{public}@", log: logger, type: .info, - peripheral.name ?? peripheral.identifier.uuidString) - } - - updateConnectionState(BleConst.STATE_DISCONNECTED) - - // 无论是否意外断开,都立即发起新的连接 - if central.state == .poweredOn { - os_log("🔄 立即重新连接设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) - connectPeripheral(peripheral) - } else { - os_log("🔄 蓝牙未就绪,添加到重连队列: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) - pendingReconnectUUIDs.insert(peripheral.identifier) - } - } - - public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { - os_log("🔄 ===== iOS系统正在恢复CBCentralManager状态 =====", log: logger, type: .info) - os_log("🔄 恢复数据: %{public}@", log: logger, type: .debug, dict) - - // 恢复外设连接 - if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { - restoredPeripherals = peripherals - os_log("🔄 恢复%d个外设连接", log: logger, type: .info, peripherals.count) - - for peripheral in peripherals { - os_log("🔄 恢复外设: %{public}@, 状态=%d", log: logger, type: .debug, - peripheral.name ?? peripheral.identifier.uuidString, peripheral.state.rawValue) - - } - } - } -} - -// MARK: - CBPeripheralDelegate -@available(iOS 13.0, *) -extension BleService: CBPeripheralDelegate { - public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - if let error = error { - os_log("❌ 服务发现失败: %{public}@", log: logger, type: .error, error.localizedDescription) - return - } - - guard let services = peripheral.services else { - os_log("❌ 未发现任何服务", log: logger, type: .error) - return - } - - os_log("✅ 发现%d个服务", log: logger, type: .info, services.count) - - for service in services { - if service.uuid == BleConst.PRIMARY_SERVICE_UUID { - os_log("🔍 发现主服务,开始发现写入和通知特征...", log: logger, type: .debug) - // 主服务,发现写入和通知特征 - peripheral.discoverCharacteristics([BleConst.WRITE_CHAR_UUID, BleConst.NOTIFY_CHAR_UUID], for: service) - } else if service.uuid == BleConst.AUDIO_SERVICE_UUID { - os_log("🔍 发现音频服务,开始发现音频特征...", log: logger, type: .debug) - // 音频服务,发现音频特征 - peripheral.discoverCharacteristics([BleConst.RECEIVE_AUDIO_CHAR_UUID], for: service) - } - } - } - - public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - if let error = error { - os_log("❌ 特征发现失败: %{public}@", log: logger, type: .error, error.localizedDescription) - return - } - - guard let characteristics = service.characteristics else { - os_log("❌ 服务%{public}@未发现任何特征", log: logger, type: .error, service.uuid.uuidString) - return - } - - os_log("✅ 服务%{public}@发现%d个特征", log: logger, type: .info, service.uuid.uuidString, characteristics.count) - - for characteristic in characteristics { - if characteristic.uuid == BleConst.WRITE_CHAR_UUID { - writeCharacteristic = characteristic - os_log("📝 写入特征已设置: %{public}@", log: logger, type: .info, characteristic.uuid.uuidString) - } else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { - notifyCharacteristic = characteristic - os_log("🔔 通知特征已设置,开始发现描述符...", log: logger, type: .info) - // 先发现描述符,再配置通知 - peripheral.discoverDescriptors(for: characteristic) - } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { - audioCharacteristic = characteristic - os_log("🎵 音频特征已设置,开始发现描述符...", log: logger, type: .info) - // 先发现描述符,再配置通知 - peripheral.discoverDescriptors(for: characteristic) - } - } - - // 检查所有必要特征是否已准备就绪 - let isReady = writeCharacteristic != nil && notifyCharacteristic != nil && audioCharacteristic != nil - if isReady { - os_log("🚀 所有BLE特征已准备就绪,可以发送命令", log: logger, type: .info) - } else { - os_log("⏳ 等待更多特征准备: 写入=%@, 通知=%@, 音频=%@", - log: logger, type: .debug, - writeCharacteristic != nil ? "✅" : "❌", - notifyCharacteristic != nil ? "✅" : "❌", - audioCharacteristic != nil ? "✅" : "❌") - } - } - - public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { - if let error = error { - return - } - - guard let data = characteristic.value, !data.isEmpty else { return } - - if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { - // 处理通知数据 - handleResponseData(data) - } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { - // 处理音频数据 - processAudioData(data) - } - } - - public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { - // 写入完成回调,不需要特殊处理 - } - - public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { - // 通知状态更新回调,不需要特殊处理 - } - - public func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) { - if let error = error { - os_log("❌ 描述符发现失败: %{public}@", log: logger, type: .error, error.localizedDescription) - return - } - - os_log("✅ 特征%{public}@发现%d个描述符", log: logger, type: .debug, - characteristic.uuid.uuidString, characteristic.descriptors?.count ?? 0) - - // 对通知特征和音频特征配置通知 - if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID || characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { - setupNotification(for: peripheral, characteristic: characteristic) - } - } - - public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) { - if let error = error { - os_log("❌ 描述符读取失败: %{public}@", log: logger, type: .error, error.localizedDescription) - return - } - - os_log("📖 描述符值已更新: %{public}@", log: logger, type: .debug, descriptor.uuid.uuidString) - } - - public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) { - if let error = error { - os_log("❌ 描述符写入失败: %{public}@", log: logger, type: .error, error.localizedDescription) - return - } - - if descriptor.uuid == BleConst.CLIENT_CHAR_CONFIG_UUID { - os_log("✅ CLIENT_CHAR_CONFIG_UUID描述符写入成功: %{public}@", log: logger, type: .info, - descriptor.characteristic?.uuid.uuidString ?? "未知特征") - } else { - os_log("✅ 描述符写入成功: %{public}@", log: logger, type: .debug, descriptor.uuid.uuidString) - } - } -} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleServicePlugin.h b/local_plugins/ble_service/ios/Classes/BleServicePlugin.h deleted file mode 100644 index af92be7c5..000000000 --- a/local_plugins/ble_service/ios/Classes/BleServicePlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface BleServicePlugin : NSObject -@end \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleServicePlugin.m b/local_plugins/ble_service/ios/Classes/BleServicePlugin.m deleted file mode 100644 index f0a50cd8d..000000000 --- a/local_plugins/ble_service/ios/Classes/BleServicePlugin.m +++ /dev/null @@ -1,12 +0,0 @@ -#import "BleServicePlugin.h" -#if __has_include() -#import -#else -#import "ble_service-Swift.h" -#endif - -@implementation BleServicePlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftBleServicePlugin registerWithRegistrar:registrar]; -} -@end \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift deleted file mode 100644 index c6fca81c8..000000000 --- a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift +++ /dev/null @@ -1,374 +0,0 @@ -import Flutter -import UIKit -import CoreBluetooth -import os - -@available(iOS 13.0, *) -public class SwiftBleServicePlugin: NSObject, FlutterPlugin { - // MARK: - 属性 - private var methodChannel: FlutterMethodChannel - internal var dataEventSink: FlutterEventSink? - internal var statusEventSink: FlutterEventSink? - private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleServicePlugin") - - // 标记是否已初始化所有服务 - private var servicesInitialized = false - - // MARK: - 初始化方法 - init(_ channel: FlutterMethodChannel) { - self.methodChannel = channel - super.init() - - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - - // iOS 18环境下,只设置回调但不主动初始化会话 - let bleAccessorySetup = BleAccessorySetup.shared - - // 在accessoryAdded回调中延迟设置BleService - bleAccessorySetup.setDeviceDiscoveredCallback { [weak self] uuid in - // 确保服务已初始化 - self?.ensureServicesInitialized() - // 然后处理发现的设备 - self?.handleDiscoveredDevice(uuid: uuid) - } - - // 注释掉自动初始化,改为等待用户主动调用startDeviceAssociation - // bleAccessorySetup.initializeSession() - - os_log("iOS 18环境:已设置设备发现回调,等待用户主动调用关联", log: logger, type: .info) - } else { - // 低于iOS 18,直接初始化所有服务 - initializeAllServices() - } - #else - // 非AccessorySetupKit环境,直接初始化所有服务 - initializeAllServices() - #endif - } - - - // 确保所有服务都已初始化 - private func ensureServicesInitialized() { - if !servicesInitialized { - // 设置BLE服务回调 - BleService.shared.addCallback(self) - - // 设置BleService到AccessorySetup - BleAccessorySetup.shared.setBleService(BleService.shared) - - servicesInitialized = true - } - } - - // 初始化所有服务(iOS 17及以下版本的标准行为) - private func initializeAllServices() { - // 设置BLE服务回调 - BleService.shared.addCallback(self) - - // 初始化AccessorySetup,但不自动开始会话 - BleAccessorySetup.shared.setBleService(BleService.shared) - BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in - self?.handleDiscoveredDevice(uuid: uuid) - } - // 移除自动初始化,统一由startDeviceAssociation方法触发 - // BleAccessorySetup.shared.initializeSession() - - os_log("非iOS 18环境:已设置BLE服务但不自动初始化会话", log: logger, type: .info) - servicesInitialized = true - } - - // MARK: - FlutterPlugin Protocol - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger()) - let instance = SwiftBleServicePlugin(channel) - registrar.addMethodCallDelegate(instance, channel: channel) - - // 注册事件通道 - 为每个通道创建独立的handler - let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger()) - let statusEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/status_events", binaryMessenger: registrar.messenger()) - - // 创建独立的StreamHandler - let dataHandler = BleDataStreamHandler(instance) - let statusHandler = BleStatusStreamHandler(instance) - - dataEventChannel.setStreamHandler(dataHandler) - statusEventChannel.setStreamHandler(statusHandler) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - // 检查是否需要初始化服务 - checkAndInitializeServices() - - switch call.method { - case "isConnected": - result(BleService.shared.isConnected()) - - case "getBatteryLevel": - // 获取电池信息,通过回调返回结果 - if BleService.shared.getBatteryInfo() { - result(0) // 通过回调返回实际值 - } else { - result(FlutterError(code: "UNAVAILABLE", - message: "无法获取电池信息", - details: nil)) - } - - case "isCompanionSupported": - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - result(true) - } else { - result(false) - } - #else - result(false) - #endif - - case "getPairedMacAddress": - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - let pairedUUID = BleAccessorySetup.shared.getPairedMacAddress() - result(pairedUUID) - } else { - result(nil) - } - #else - result(nil) - #endif - - - - case "isBleConnected": - result(BleService.shared.isConnected()) - - case "registerBackgroundScan", "unregisterBackgroundScan": - // iOS上禁用,直接返回成功以避免Flutter端报错 - result(true) - - case "getDeviceVersion": - result(BleService.shared.getDeviceVersion()) - - case "getProductId": - result(BleService.shared.getProductId()) - - case "getBatteryInfo": - result(BleService.shared.getBatteryInfo()) - - case "openEncoder": - result(BleService.shared.openEncoder()) - - case "openDecoder": - result(BleService.shared.openDecoder()) - - case "openA2DPDecoder": - result(BleService.shared.openA2DPDecoder()) - - case "closeCodec": - result(BleService.shared.closeCodec()) - - case "playA2DP": - result(BleService.shared.playA2DP()) - - // iOS 18+ AccessorySetupKit相关方法 - case "clearAssociations": - // 清除设备配对关系 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("清除设备配对关系", log: logger, type: .info) - BleAccessorySetup.shared.clearAssociations() - result(true) - } else { - result(true) - } - #else - result(true) - #endif - - case "startDeviceAssociation": - // 用户主动触发设备关联流程 - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - os_log("用户主动触发设备关联流程", log: logger, type: .info) - - // 确保服务已初始化 - ensureServicesInitialized() - - // 重新初始化会话 - BleAccessorySetup.shared.initializeSession() - - // 返回成功,实际配对结果会通过回调通知 - result(true) - } else { - os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info) - result(false) - } - #else - os_log("当前环境不支持AccessorySetupKit", log: logger, type: .info) - result(false) - #endif - - default: - result(FlutterMethodNotImplemented) - } - } - - // MARK: - 私有方法 - // 检查并按需初始化服务 - private func checkAndInitializeServices() { - #if canImport(AccessorySetupKit) && swift(>=5.9) - if #available(iOS 18.0, *) { - ensureServicesInitialized() - } - #endif - } - - // AccessorySetupKit发现设备的处理 - private func handleDiscoveredDevice(uuid: UUID) { - // BleAccessorySetup内部已进行连接,此处不再重复调用 - // os_log("收到设备发现回调: %{public}@", log: logger, type: .info, uuid.uuidString) - // 可以在这里添加设备记录或其他操作 - } - - private func sendEvent(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) { - if let eventSink = eventSink { - DispatchQueue.main.async { - eventSink(data) - } - } - } - - // 获取顶层视图控制器的辅助方法 - private func getTopViewController(_ viewController: UIViewController) -> UIViewController { - if let presented = viewController.presentedViewController { - return getTopViewController(presented) - } - - if let tabBarController = viewController as? UITabBarController, - let selected = tabBarController.selectedViewController { - return getTopViewController(selected) - } - - if let navigationController = viewController as? UINavigationController, - let visibleViewController = navigationController.visibleViewController { - return getTopViewController(visibleViewController) - } - - return viewController - } -} - -// MARK: - BleService.Callback -@available(iOS 13.0, *) -extension SwiftBleServicePlugin: BleService.Callback { - public func onScanResult(devices: [[String: Any]]) { - if let eventSink = statusEventSink { - DispatchQueue.main.async { - eventSink([ - "type": "scanResult", - "devices": devices - ]) - } - } - } - - public func onConnectionStateChanged(state: Int) { - if let eventSink = statusEventSink { - var stateStr = "Unknown" - switch state { - case BleConst.STATE_DISCONNECTED: - stateStr = "Disconnected" - case BleConst.STATE_CONNECTING: - stateStr = "Connecting" - case BleConst.STATE_CONNECTED: - stateStr = "Connected" - case BleConst.STATE_DISCONNECTING: - stateStr = "Disconnecting" - case BleConst.STATE_ERROR: - stateStr = "Connection Error" - default: - stateStr = "Unknown State(\(state))" - } - - DispatchQueue.main.async { - eventSink([ - "type": "connectionStateChanged", - "state": state, - "stateStr": stateStr - ]) - } - } - } - - public func onAudioDataReceived(data: Data) { - if let eventSink = dataEventSink { - DispatchQueue.main.async { - eventSink([ - "type": "audioData", - "data": FlutterStandardTypedData(bytes: data) - ]) - } - } - } - - public func onWakeupSignalReceived() { - if let eventSink = statusEventSink { - DispatchQueue.main.async { - eventSink([ - "type": "wakeup" - ]) - } - } - } - - public func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { - if let eventSink = statusEventSink { - var resultMap = infoData - resultMap["type"] = "deviceInfo" - resultMap["result"] = infoType - - DispatchQueue.main.async { - eventSink(resultMap) - } - } - } -} - -// MARK: - 独立的StreamHandler类 -@available(iOS 13.0, *) -class BleDataStreamHandler: NSObject, FlutterStreamHandler { - private weak var plugin: SwiftBleServicePlugin? - - init(_ plugin: SwiftBleServicePlugin) { - self.plugin = plugin - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - plugin?.dataEventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - plugin?.dataEventSink = nil - return nil - } -} - -@available(iOS 13.0, *) -class BleStatusStreamHandler: NSObject, FlutterStreamHandler { - private weak var plugin: SwiftBleServicePlugin? - - init(_ plugin: SwiftBleServicePlugin) { - self.plugin = plugin - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - plugin?.statusEventSink = events - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - plugin?.statusEventSink = nil - return nil - } -} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/SwiftOpusAudioProcessor.swift b/local_plugins/ble_service/ios/Classes/SwiftOpusAudioProcessor.swift deleted file mode 100644 index fc9c374b0..000000000 --- a/local_plugins/ble_service/ios/Classes/SwiftOpusAudioProcessor.swift +++ /dev/null @@ -1,239 +0,0 @@ -import Foundation -import AVFoundation -import YbridOpus - -@available(iOS 13.0, *) -class SwiftOpusAudioProcessor: NSObject { - - protocol AudioDataCallback: AnyObject { - func onAudioDataReceived(data: Data) - } - - private var isDecodingActive = false - private weak var callback: AudioDataCallback? - - // Opus 解码器相关 - 使用 YbridOpus C API - private var decoder: OpaquePointer? - private var sampleRate: Int32 = 16000 - private var channels: Int32 = 1 - private let maxFrameSize: Int32 = 5760 // 120 ms @48 kHz 的最大值 - - override init() { - super.init() - } - - deinit { - cleanup() - } - - private func initializeOpusDecoder() -> Bool { - if decoder != nil { - return true - } - - var error: Int32 = 0 - decoder = opus_decoder_create(sampleRate, channels, &error) - - guard error == OPUS_OK, decoder != nil else { - return false - } - - return true - } - - private func destroyOpusDecoder() { - if let decoder = decoder { - opus_decoder_destroy(decoder) - self.decoder = nil - } - } - - func setCallback(_ callback: AudioDataCallback?) { - self.callback = callback - } - - func startDecoding(sampleRate: Int32 = 16000, channels: Int32 = 1, frameSize: Int32 = 640) -> Bool { - if isDecodingActive { - return true - } - - self.sampleRate = sampleRate - self.channels = channels - - guard initializeOpusDecoder() else { - return false - } - - isDecodingActive = true - - return true - } - - func stopDecoding() -> Bool { - if !isDecodingActive { - return false - } - - isDecodingActive = false - destroyOpusDecoder() - - return true - } - - func processAudioData(_ data: Data) { - guard isDecodingActive, let decoder = decoder, data.count > 0 else { - return - } - - // 增强的数据包验证 - guard data.count <= 1275 else { - print("音频数据包太大: \(data.count) bytes,跳过处理") - return - } - - // 检查最小数据包大小(Opus最小包通常至少几个字节) - guard data.count >= 3 else { - print("音频数据包太小: \(data.count) bytes,跳过处理") - return - } - - // 简单的 Opus 头部验证(检查是否是有效的 Opus 数据包格式) - let firstByte = data[0] - // Opus packets start with a TOC (Table of Contents) byte - // 检查TOC字节是否在合理范围内 - if firstByte == 0x00 || firstByte == 0xFF { - print("可疑的Opus数据包头部: 0x\(String(format: "%02X", firstByte)),跳过处理") - return - } - - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - self?.decodeOpusPacket(data) - } - } - - // 简化的 Opus 解码方法,重点是容错处理 - private func decodeOpusPacket(_ packet: Data) { - guard let decoder = decoder else { return } - - let maxSamples = Int(maxFrameSize) - var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels)) - - let decodedSamples = packet.withUnsafeBytes { packetPtr in - let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress - return opus_decode( - decoder, - uint8Ptr, - opus_int32(packet.count), - &pcmBuffer, - Int32(maxSamples), - 0 - ) - } - - // 增强的错误处理:检查各种错误情况 - if decodedSamples <= 0 { - let errorCode = decodedSamples - print("Opus解码失败,错误代码: \(errorCode),重新初始化解码器") - - // 根据错误类型进行不同的处理 - if errorCode == -1 { // OPUS_BAD_ARG - print("Opus解码参数错误,执行完全重置") - performCompleteReset() - } else if errorCode == -2 { // OPUS_BUFFER_TOO_SMALL - print("Opus缓冲区太小") - resetDecoder() - } else if errorCode == -3 { // OPUS_INTERNAL_ERROR - print("Opus内部错误,执行完全重置") - performCompleteReset() - } else { - print("Opus其他错误,尝试简单重置") - resetDecoder() - } - return - } - - // 验证解码结果的合理性 - guard decodedSamples <= maxFrameSize else { - print("Opus解码样本数超出预期: \(decodedSamples) > \(maxFrameSize)") - resetDecoder() - return - } - - // 提取解码的音频数据 - let samplesPerChannel = Int(decodedSamples) - let totalSamples = samplesPerChannel * Int(channels) - - // 验证总样本数的合理性 - guard totalSamples <= pcmBuffer.count else { - print("计算的总样本数超出缓冲区: \(totalSamples) > \(pcmBuffer.count)") - resetDecoder() - return - } - - let outputPCM = Array(pcmBuffer[0...size) - - // 回调通知 - DispatchQueue.main.async { [weak self] in - self?.callback?.onAudioDataReceived(data: data) - } - } - - func isDecoding() -> Bool { - return isDecodingActive - } - - func cleanup() { - stopDecoding() - } - - // MARK: - 公共接口 - - /// 获取当前配置信息 - func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) { - return (sampleRate, channels) - } -} \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/XCFrameworkExample.swift b/local_plugins/ble_service/ios/Classes/XCFrameworkExample.swift deleted file mode 100644 index 0519ecba6..000000000 --- a/local_plugins/ble_service/ios/Classes/XCFrameworkExample.swift +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file