diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index fee73821c..75c2457c6 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -19,7 +19,9 @@ abstract class AsrService { /// 开始连续语音识别 /// /// 返回一个包含识别事件的流 - Future> startContinuousRecognition(); + Future> startContinuousRecognition( + bool audioSourceType, + ); /// 停止连续语音识别 Future stopContinuousRecognition(); diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index ce8302c92..4c4b88e99 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -537,6 +537,16 @@ class BleManager extends GetxService { } } + /// 打开解码器 + Future openA2DPDecoder() async { + try { + return await _bleService.openA2DPDecoder(); + } catch (e) { + Logger.error('打开解码器失败: $e'); + return false; + } + } + /// 关闭编解码器 Future closeCodec() async { try { diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index 5318d8d15..98daae281 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -73,4 +73,13 @@ class Api { params: params, ); } + + //app获取设备信息 + static getequipmentinfo([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/user_getequipmentinfo', + params: params, + ); + } } diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index d15038a97..5c87577ee 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -150,7 +150,8 @@ class AzureAsrService extends GetxService implements AsrService { } @override - Future> startContinuousRecognition() async { + Future> startContinuousRecognition( + bool audioSourceType) async { if (!_isInitialized) { await initialize(); } @@ -164,7 +165,9 @@ class AzureAsrService extends GetxService implements AsrService { // 开始连续识别 final bool result = - await _channel.invokeMethod('startContinuousRecognition'); + await _channel.invokeMethod('startContinuousRecognition', { + 'audioSourceType': audioSourceType, + }); if (!result) { _cleanupEventStream(); diff --git a/lib/data/services/speech_impl/flutter_asr_service.dart b/lib/data/services/speech_impl/flutter_asr_service.dart index d1e7fb6d8..438953af8 100644 --- a/lib/data/services/speech_impl/flutter_asr_service.dart +++ b/lib/data/services/speech_impl/flutter_asr_service.dart @@ -363,7 +363,8 @@ class FlutterAsrService extends GetxService implements AsrService { } @override - Future> startContinuousRecognition() async { + Future> startContinuousRecognition( + bool audioSourceType) async { try { if (!_isAvailable.value) { final controller = StreamController.broadcast(); diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index bab3790ef..3ffc4e75e 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -36,7 +36,8 @@ class VolcanoAsrApiService implements AsrService { final int _bitsPerSample = 16; final int _channels = 1; final String _encoding = 'pcm'; - + // 音频输入源 + bool _audioSourceType = false; // 连接相关 WebSocketChannel? _channel; Completer? _connectionCompleter; @@ -113,7 +114,7 @@ class VolcanoAsrApiService implements AsrService { final resultCompleter = Completer(); // 开始连续识别流程 - final stream = await startContinuousRecognition(); + final stream = await startContinuousRecognition(_audioSourceType); // 监听结果 StreamSubscription? subscription; @@ -156,7 +157,8 @@ class VolcanoAsrApiService implements AsrService { } @override - Future> startContinuousRecognition() async { + Future> startContinuousRecognition( + bool audioSourceType) async { if (!_isInitialized) { await initialize(); } diff --git a/lib/data/services/speech_impl/volcano_asr_service.dart b/lib/data/services/speech_impl/volcano_asr_service.dart index 636c2d834..ccb53f447 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_service.dart @@ -265,7 +265,8 @@ class VolcanoAsrService extends GetxService implements AsrService { /// 开始连续语音识别 @override - Future> startContinuousRecognition() async { + Future> startContinuousRecognition( + bool audioSourceType) async { if (!await _ensureInitialized()) { _eventStreamController.add(RecognitionEvent( type: RecognitionEventType.error, @@ -275,9 +276,10 @@ class VolcanoAsrService extends GetxService implements AsrService { } try { - final success = - await _channel.invokeMethod('startContinuousRecognition') ?? - false; + final success = await _channel.invokeMethod('audioSourceType', { + 'audioSourceType': audioSourceType, + }) ?? + false; if (success) { _isContinuousRecognitionActive = true; diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index 1201f7c66..ef6791e3f 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -75,7 +75,8 @@ class XunfeiAsrService extends GetxService implements AsrService { } @override - Future> startContinuousRecognition() async { + Future> startContinuousRecognition( + bool audioSourceType) async { if (!_isInitialized) { await initialize(); } diff --git a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart index e9c9e1410..46dbb2391 100644 --- a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart +++ b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart @@ -9,8 +9,8 @@ import '../../models/events/voice_interaction_event.dart'; import '../../../core/utils/logger.dart'; enum VoiceInteractionState { - idle, // 等待唤醒 - active, // 活跃状态 - 可以同时识别和播放 + idle, // 等待唤醒 + active, // 活跃状态 - 可以同时识别和播放 } class IosVoiceInteractionService extends GetxService { @@ -18,45 +18,47 @@ class IosVoiceInteractionService extends GetxService { final TtsService _ttsService = Get.find(); final VolcanoAIService _aiService = Get.find(); final ChatHistoryService _chatHistoryService = Get.find(); - + // 当前状态 final Rx state = VoiceInteractionState.idle.obs; - + // 是否识别到用户语音 final RxBool isSpeechDetected = false.obs; - + // 是否正在处理AI响应 final RxBool isProcessingAI = false.obs; - + // 是否正在播放TTS final RxBool isSpeaking = false.obs; - + // 对话历史记录 final List> _messageHistory = []; - + // 最大历史记录数 static const int _maxHistorySize = 10; - + // 超时计时器 Timer? _inactivityTimer; - + // 流订阅 StreamSubscription? _recognitionSubscription; - + // 当前识别的文本 String _currentRecognizedText = ''; - + // 音频输入源 + bool _audioSourceType = false; // 事件流控制器 - final StreamController _eventStreamController = + final StreamController _eventStreamController = StreamController.broadcast(); - + // 获取事件流 @override - Stream get eventStream => _eventStreamController.stream; - + Stream get eventStream => + _eventStreamController.stream; + // 标识符,用于存储聊天历史 final String _agentId = 'ios_assistant'; - + /// 初始化服务 @override Future initialize() async { @@ -66,10 +68,10 @@ class IosVoiceInteractionService extends GetxService { } catch (e) { Logger.error('iOS语音交互服务初始化失败: $e'); } - + return this; } - + // 处理唤醒事件 void onWakeup() { _ttsService.speakOnce('我在呢!'); @@ -79,63 +81,61 @@ class IosVoiceInteractionService extends GetxService { _activateInteraction(); } } - + // 激活交互 void _activateInteraction() { // 设置状态为活跃 state.value = VoiceInteractionState.active; - + // 开始连续监听 _startContinuousListening(); - + // 启动不活动计时器 _startInactivityTimer(); - + // 发送识别开始事件 _sendRecognitionStartedEvent(); } - + // 开始连续监听 Future _startContinuousListening() async { // 停止之前的监听 _recognitionSubscription?.cancel(); - + try { // 开始连续识别 - final recognitionStream = await _asrService.startContinuousRecognition(); - + final recognitionStream = + await _asrService.startContinuousRecognition(_audioSourceType); + // 监听识别结果 - _recognitionSubscription = recognitionStream.listen( - _handleRecognitionEvent, - onError: (error) { - Logger.error('语音识别错误: $error'); - _startContinuousListening(); // 尝试重新启动 - } - ); - + _recognitionSubscription = + recognitionStream.listen(_handleRecognitionEvent, onError: (error) { + Logger.error('语音识别错误: $error'); + _startContinuousListening(); // 尝试重新启动 + }); + Logger.info('开始连续语音识别'); } catch (e) { Logger.error('启动语音识别失败: $e'); - } } - + // 处理识别事件 void _handleRecognitionEvent(RecognitionEvent event) { // 更新活动时间 _updateActivityTime(); - + switch (event.type) { case RecognitionEventType.intermediateResult: // 检测用户是否开始说话 if (event.text.isNotEmpty && !isSpeechDetected.value) { isSpeechDetected.value = true; - + // 用户开始讲话时,立即中断当前响应 _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); } break; - + case RecognitionEventType.finalResult: if (event.text.isNotEmpty) { // 最终结果,处理用户输入 @@ -144,16 +144,16 @@ class IosVoiceInteractionService extends GetxService { // 重置语音检测状态 isSpeechDetected.value = false; break; - + case RecognitionEventType.sessionStarted: Logger.info('语音识别会话开始'); break; - + case RecognitionEventType.sessionStopped: Logger.info('语音识别会话结束'); break; - + case RecognitionEventType.error: case RecognitionEventType.canceled: Logger.error('语音识别错误: ${event.error}'); @@ -161,11 +161,11 @@ class IosVoiceInteractionService extends GetxService { break; } } - + // 中断当前响应 void _interruptCurrentResponse(String reason) { Logger.info(reason); - + // 如果正在活跃状态,需要中断当前操作 if (state.value == VoiceInteractionState.active) { // 停止TTS播放 @@ -174,40 +174,40 @@ class IosVoiceInteractionService extends GetxService { isProcessingAI.value = false; } } - + // 处理用户输入 Future _processUserInput(String text) async { Logger.info('处理用户输入: $text'); - + // 设置为处理状态 isProcessingAI.value = true; - + // 添加用户消息到历史记录 final userMessage = {'role': 'user', 'content': text}; _addToHistory(userMessage); - + // 创建用户消息对象 final userMsg = Message( role: 'user', content: text, timestamp: DateTime.now(), ); - + // AI响应处理标志 bool isProcessingCancelled = false; - + try { // 构建消息历史 final messageHistory = _buildMessageHistory(); - + // 调用AI服务获取响应 final responseStream = _aiService.sendMessageStream( messages: messageHistory, systemPrompt: "你是一个智能助手,请简明扼要地回答问题。", ); - + String aiResponse = ''; - + // 等待AI响应 await for (final chunk in responseStream) { // 检查是否被用户打断 @@ -217,49 +217,49 @@ class IosVoiceInteractionService extends GetxService { Logger.info('AI响应生成过程中被用户打断'); break; } - + // 如果状态已改变(可能由其他原因导致),停止处理 if (state.value != VoiceInteractionState.active) { isProcessingCancelled = true; break; } - + aiResponse += chunk; - + // 立即播放当前文本块,实现边生成边播放 _speakStreamResponse(chunk); } - + // 如果处理被取消,不继续后续操作 if (isProcessingCancelled) { isProcessingAI.value = false; return; } - + // 确保完整响应被处理 if (aiResponse.isNotEmpty) { // 将AI响应添加到历史记录 final assistantMessage = {'role': 'assistant', 'content': aiResponse}; _addToHistory(assistantMessage); - + // 创建助手消息对象 final assistantMsg = Message( role: 'assistant', content: aiResponse, timestamp: DateTime.now(), ); - + // 保存聊天历史 await _saveChatHistory(userMsg, assistantMsg); - + // 发送聊天历史事件 _sendChatHistoryEvent(userMsg.content, assistantMsg.content); - + // 刷新TTS流,确保所有文本都被播放 await _ttsService.flushStream(); isSpeaking.value = false; } - + // 处理完成 isProcessingAI.value = false; } catch (e) { @@ -268,59 +268,59 @@ class IosVoiceInteractionService extends GetxService { isProcessingAI.value = false; } } - + // 构建用于AI服务的消息历史 List> _buildMessageHistory() { return List>.from(_messageHistory); } - + // 添加消息到历史记录 void _addToHistory(Map message) { _messageHistory.add(message); - + // 限制历史记录大小 while (_messageHistory.length > _maxHistorySize) { _messageHistory.removeAt(0); } } - + // 播放流式响应 void _speakStreamResponse(String chunk) { if (chunk.isEmpty) return; - + // 更新播放状态 isSpeaking.value = true; - + // 使用流式TTS播放 _ttsService.speakStream(chunk); - + // 更新活动时间 _updateActivityTime(); } - + // 停止TTS void _stopTts() { _ttsService.stop(); isSpeaking.value = false; } - + // 更新最后活动时间 DateTime _lastActivityTime = DateTime.now(); void _updateActivityTime() { _lastActivityTime = DateTime.now(); } - + // 启动不活动计时器(长时间无交互会切换到空闲状态) void _startInactivityTimer() { _cancelInactivityTimer(); - + _inactivityTimer = Timer.periodic(Duration(seconds: 5), (timer) { // 计算空闲时间 final idleTime = DateTime.now().difference(_lastActivityTime).inSeconds; - + // 如果空闲超过30秒,且不在播放或检测到语音,切换到空闲 - if (idleTime > 8 && - !isSpeaking.value && + if (idleTime > 8 && + !isSpeaking.value && !isProcessingAI.value && !isSpeechDetected.value) { _resetToIdle(); @@ -328,37 +328,37 @@ class IosVoiceInteractionService extends GetxService { } }); } - + // 取消不活动计时器 void _cancelInactivityTimer() { _inactivityTimer?.cancel(); _inactivityTimer = null; } - + // 重置到空闲状态 void _resetToIdle() { _ttsService.speakOnce('没有听到声音,暂停对话,双击耳机唤醒!'); _cancelInactivityTimer(); - + // 停止TTS _stopTts(); - + // 停止语音识别 _recognitionSubscription?.cancel(); _asrService.stopContinuousRecognition(); - + // 重置状态 state.value = VoiceInteractionState.idle; isSpeechDetected.value = false; isProcessingAI.value = false; isSpeaking.value = false; - + // 清空历史记录 _messageHistory.clear(); - + Logger.info('语音交互已重置为空闲状态'); } - + // 暂停语音交互 @override void pauseVoiceInteraction() { @@ -366,53 +366,53 @@ class IosVoiceInteractionService extends GetxService { _stopTts(); _recognitionSubscription?.cancel(); _asrService.stopContinuousRecognition(); - + // 重置所有状态 state.value = VoiceInteractionState.idle; isSpeechDetected.value = false; isProcessingAI.value = false; isSpeaking.value = false; - + // 清空历史记录 _messageHistory.clear(); } } - + // 恢复语音交互 void resumeVoiceInteraction() { if (state.value == VoiceInteractionState.idle) { _activateInteraction(); } } - + // 手动停止交互 void stopInteraction() { _resetToIdle(); } - + // 获取当前状态 VoiceInteractionState getCurrentState() { return state.value; } - + // 是否处于活跃状态 @override bool isActive() { return state.value != VoiceInteractionState.idle; } - + // 获取当前对话历史 @override List> getMessageHistory() { return List>.from(_messageHistory); } - + // 获取当前服务ID @override String getServiceId() { return _agentId; } - + // 释放资源 @override void onClose() { @@ -422,25 +422,25 @@ class IosVoiceInteractionService extends GetxService { _eventStreamController.close(); super.onClose(); } - + // 保存聊天历史 Future _saveChatHistory(Message userMsg, Message assistantMsg) async { try { // 加载现有历史记录 final existingMessages = await _chatHistoryService.loadHistory(_agentId); - + // 添加新消息 existingMessages.addAll([userMsg, assistantMsg]); - + // 保存更新后的历史记录 await _chatHistoryService.saveHistory(_agentId, existingMessages); - + Logger.info('保存聊天历史记录成功'); } catch (e) { Logger.error('保存聊天历史记录失败: $e'); } } - + // 发送聊天历史事件 void _sendChatHistoryEvent(String userMessage, String assistantMessage) { final event = ChatHistoryEvent( @@ -449,19 +449,18 @@ class IosVoiceInteractionService extends GetxService { assistantMessage: assistantMessage, timestamp: DateTime.now().millisecondsSinceEpoch, ); - + _eventStreamController.add(event); Logger.info('已发送聊天历史事件'); } - + // 发送识别开始事件 void _sendRecognitionStartedEvent() { final event = RecognitionStartedEvent( timestamp: DateTime.now().millisecondsSinceEpoch, ); - + _eventStreamController.add(event); Logger.info('已发送识别开始事件'); } } - diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 7955486b1..2f7fd5f70 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -78,6 +78,9 @@ class ChatController extends GetxController { // 取消标志 bool _shouldCancelAiResponse = false; + // 音频输入源 + bool _audioSourceType = false; + // 自动停止语音识别计时器 Timer? _autoStopTimer; @@ -184,7 +187,6 @@ class ChatController extends GetxController { // 自动停止计时器会在startVoiceRecognition中启动 } - _getLocation(); player.playerStateStream.listen((state) async { @@ -211,8 +213,6 @@ class ChatController extends GetxController { _amapFlutter.startLocation(); } - - @override void onClose() { Logger.info('ChatController onClose'); @@ -295,7 +295,6 @@ class ChatController extends GetxController { Future sendMessage(String text) async { if (text.trim().isEmpty) return; - // 中断当前响应 _interruptCurrentResponse("发送新消息,中断当前响应"); @@ -558,12 +557,11 @@ class ChatController extends GetxController { Timer(const Duration(milliseconds: 260), () async { if (isDebounce) return; - // 中断当前响应 _interruptCurrentResponse("发送新消息,中断当前响应"); try { final recognitionStream = - await _asrService.startContinuousRecognition(); + await _asrService.startContinuousRecognition(_audioSourceType); await Future.delayed(const Duration(milliseconds: 260)); isDebounce = true; recognizedText.value = ''; @@ -619,11 +617,11 @@ class ChatController extends GetxController { Future startVoiceRecognition() async { if (isVoiceRecognitionActive.value) return; - try { // 启动连续语音识别 // 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据 - final recognitionStream = await _asrService.startContinuousRecognition(); + final recognitionStream = + await _asrService.startContinuousRecognition(_audioSourceType); isVoiceRecognitionActive.value = true; isListening.value = true; diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 01f708c0e..a83e1c312 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -111,7 +111,8 @@ class MeetingRecordController extends GetxController //开始录音 Future _startRecorder() async { - final recognitionStream = await _asrService.startContinuousRecognition(); + final recognitionStream = + await _asrService.startContinuousRecognition(false); _recognitionSubscription = recognitionStream.listen( _handleRecognitionEvent, ); diff --git a/lib/modules/ota_upgrade/bindings/ota_upgrade_binding.dart b/lib/modules/ota_upgrade/bindings/ota_upgrade_binding.dart new file mode 100644 index 000000000..f97146011 --- /dev/null +++ b/lib/modules/ota_upgrade/bindings/ota_upgrade_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/ota_upgrade_controller.dart'; + +class OtaUpgradeBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => OtaUpgradeController()); + } +} diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart new file mode 100644 index 000000000..878a97be6 --- /dev/null +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'package:get_storage/get_storage.dart'; +import '../../../data/services/network/api.dart'; +import '../ota.dart'; +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class OtaUpgradeController extends GetxController { + final Dio _dio = Dio(); + final RxString latestVersion = ''.obs; + final RxString currentVersion = ''.obs; + String updatepackageaddress = ''; + final RxDouble downloadProgress = 0.0.obs; + final RxBool isUpdating = false.obs; + final isUpdateAvailable = true.obs; + + final RxDouble progress = 0.0.obs; + + @override + void onInit() { + super.onInit(); + _getCurrentVersion(); + } + + // 获取当前版本信息 + Future _getCurrentVersion() async { + //发送ble获取固件版本 + var firmwareVersion = "1.1.0"; + currentVersion.value = firmwareVersion; + //发送ble获取产品id + var id = "a2"; + //发送api请求 + final response = await Api.getequipmentinfo({ + "id": id, + }); + if (response['info'] != '') { + print('getLoginToken:进来了'); + // 根据API设计调整 + final resp = OtaConfigResp.fromJson(response); + + print('成功解析: ${resp.id}, ${resp.factory}'); + latestVersion.value = resp.systemversion!; + updatepackageaddress = resp.updatepackageaddress!; + if (_isNewVersionAvailable(resp.systemversion!)) { + //需要升级 + print('需要更新'); + isUpdateAvailable.value = false; + } else { + isUpdateAvailable.value = true; + //已经是最新版本更新ui,按钮隐藏 + } + } else { + throw const FormatException('API 响应缺少 "info" 字段'); + } + } + + // 版本比较方法优化 + bool _isNewVersionAvailable(String latest) { + try { + List currentParts = currentVersion.value + .split('.') + .map((e) => int.parse(e.trim())) + .toList(); + List latestParts = + latest.split('.').map((e) => int.parse(e.trim())).toList(); + + for (int i = 0; i < latestParts.length; i++) { + int current = (i < currentParts.length) ? currentParts[i] : 0; + if (latestParts[i] > current) return true; + if (latestParts[i] < current) return false; + } + return false; + } catch (e) { + print('版本比较错误: $e'); + return false; + } + } + + // 版本更新 + Future checkUpdate() async { + try { + if (latestVersion.value != "") + _showUpdateDialog(latestVersion.value); + else { + Get.snackbar('错误', '获取最新版本信息失败'); + } + } catch (e) { + Get.snackbar('错误', '获取版本信息失败'); + } + } + + // 显示更新对话框 + void _showUpdateDialog(String latest) { + Get.dialog( + AlertDialog( + title: Text('发现新版本 $latest'), + content: Text('是否立即更新?'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('取消'), + ), + TextButton( + onPressed: () { + Get.back(); + startDownload(); + }, + child: Text('更新'), + ), + ], + ), + ); + } + + // 开始下载 + Future startDownload() async { + isUpdating.value = true; + // 查找最后一个 '/' 的位置 + int lastIndex = updatepackageaddress.lastIndexOf('/'); + + // 截取最后一个 '/' 后面的字符串 + String fileName = updatepackageaddress.substring(lastIndex + 1); + String directoryName = "ota_upgrades"; + + final dio = Dio(BaseOptions( + connectTimeout: const Duration(seconds: 5), + receiveTimeout: Duration.zero, + sendTimeout: const Duration(seconds: 5), + headers: {'Accept-Encoding': 'gzip'}, + )); + + Get.snackbar('升级命令已发送', '开始下载...'); + + try { + final Directory? downloadsDir = await getDownloadsDirectory(); + if (downloadsDir == null) { + throw FileSystemException('无法访问下载目录'); + } + + // 创建子目录:Download/APP + final targetDir = Directory('${downloadsDir.path}/$directoryName'); + if (!await targetDir.exists()) { + await targetDir.create(recursive: true); + } + + final savePath = '${targetDir.path}/$fileName'; + print('文件保存路径: $savePath'); // 关键日志 + + final options = Options( + receiveTimeout: Duration.zero, + followRedirects: true, + maxRedirects: 5, + ); + + DateTime lastUpdate = DateTime.now(); + await dio.download( + updatepackageaddress, + savePath, + options: options, + onReceiveProgress: (received, total) { + final now = DateTime.now(); + if (now.difference(lastUpdate).inMilliseconds > 1000) { + lastUpdate = now; + String progressMsg = total != -1 + ? '${(received / total * 100).toStringAsFixed(1)}%' + : '已下载:${(received / 1024 / 1024).toStringAsFixed(1)}MB'; + Get.snackbar( + '升级进度', + progressMsg, + duration: const Duration(seconds: 1), + ); + } + }, + ); + + // 下载完成后验证文件存在性 + File file = File(savePath); + if (await file.exists()) { + Get.snackbar('下载完成', '文件已保存至: $savePath'); // 明确提示路径 + } else { + Get.snackbar('错误', '文件未找到,请检查写入权限'); + } + isUpdating.value = false; + //下载完成,开始升级 + _installApp(savePath); + } on DioException catch (e) { + Get.snackbar('下载失败', '错误: ${e.message}'); + } on FileSystemException catch (e) { + Get.snackbar('存储失败', '无法写入文件: ${e.message}'); + } catch (e) { + Get.snackbar('未知错误', e.toString()); + } + } + + // 开始升级 + void _installApp(String path) async { + // final File file = File(path); + // if (await file.exists()) { + // // OpenResult result = await OpenFile.open(path); + // // if (result.type != ResultType.done) { + // // Get.snackbar('错误', '安装失败: ${result.message}'); + // // } + // } + } +} diff --git a/lib/modules/ota_upgrade/ota.dart b/lib/modules/ota_upgrade/ota.dart new file mode 100644 index 000000000..2c35ff1ea --- /dev/null +++ b/lib/modules/ota_upgrade/ota.dart @@ -0,0 +1,54 @@ +class OtaConfigResp { + // 单例实例 + static OtaConfigResp? _instance; + + // 单例访问器(增加空安全保护) + static OtaConfigResp get instance { + if (_instance == null) { + throw Exception("User must be initialized first"); + } + return _instance!; + } + + // 最终属性(不可变) + final String? id; + final String? factory; + final String? equipmentname; + final String? systemversion; + final String? updatepackageaddress; + // 私有构造函数 + OtaConfigResp._({ + this.id, + this.factory, + this.equipmentname, + this.systemversion, + this.updatepackageaddress, + }); + + factory OtaConfigResp.fromJson(Map json) { + print("数据获取fronTokenJson${json}"); + // 修复1:安全获取嵌套的 user 字段 + final infoData = json['info'] as Map?; // 允许空值 + + // 更新单例实例 + return _instance = OtaConfigResp._( + id: (infoData?['id'] as String?) ?? '', + factory: (infoData?['factory'] as String?) ?? '', + equipmentname: (infoData?['equipmentname'] as String?) ?? '', + systemversion: (infoData?['systemversion'] as String?) ?? '', + updatepackageaddress: + (infoData?['updatepackageaddress'] as String?) ?? '', + ); + } + + // 完整的JSON序列化 + Map toJson() => { + 'user': { + 'id': instance.id, + 'factory': instance.factory, + 'equipmentname': instance.equipmentname, + 'systemversion': instance.systemversion, + 'updatepackageaddress': instance.updatepackageaddress, + } + }; +} diff --git a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart new file mode 100644 index 000000000..bc507d1e9 --- /dev/null +++ b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../controllers/ota_upgrade_controller.dart'; + +class OtaUpgradeView extends GetView { + const OtaUpgradeView({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + // 添加外层Center + child: SingleChildScrollView( + // 添加滚动防止小屏溢出 + child: Padding( + padding: EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, // 水平居中 + mainAxisSize: MainAxisSize.min, // 紧凑布局 + children: [ + // 设备图标 + CircleAvatar( + radius: 60, + backgroundColor: Colors.blue[100], + child: Icon( + Icons.phone_iphone, + size: 60, + color: Colors.blue, + ), + ), + SizedBox(height: 30), + + // 版本信息 + Column( + children: [ + Text( + '当前版本', + style: TextStyle( + fontSize: 18, + color: Colors.grey, + ), + ), + SizedBox(height: 8), + Text( + 'v${controller.currentVersion}', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ], + ), + SizedBox(height: 40), + + // 更新按钮/进度条 + Obx(() { + if (controller.isUpdating.value) { + return Column( + children: [ + SizedBox( + width: 200, // 限制进度条宽度 + child: LinearProgressIndicator( + value: controller.downloadProgress.value, + backgroundColor: Colors.grey[200], + valueColor: + AlwaysStoppedAnimation(Colors.blue), + minHeight: 12, + borderRadius: BorderRadius.circular(10), + ), + ), + SizedBox(height: 20), + Text( + '${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%', + style: TextStyle(color: Colors.grey), + ) + ], + ); + } else { + return ElevatedButton( + onPressed: controller.isUpdateAvailable.value + ? null + : controller.checkUpdate, + style: ElevatedButton.styleFrom( + backgroundColor: controller.isUpdateAvailable.value + ? Colors.grey + : Colors.blue, + foregroundColor: Colors.white, + padding: + EdgeInsets.symmetric(horizontal: 40, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Text( + controller.isUpdateAvailable.value ? '已是最新版本' : '立即更新', + style: TextStyle(fontSize: 16), + ), + ); + } + }), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 66f2d4749..526992dab 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -554,11 +554,32 @@ class SettingsView extends GetView { title: '语音测试', subtitle: '测试语音识别和合成功能,支持多种服务实现', icon: Icons.record_voice_over, - iconBgColor: isDarkMode ? Colors.orange[900]!.withOpacity(0.3) : Colors.orange[100]!, - iconColor: isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, + iconBgColor: isDarkMode + ? Colors.orange[900]!.withOpacity(0.3) + : Colors.orange[100]!, + iconColor: + isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, onTap: () => Get.toNamed(Routes.speechTest), isDarkMode: isDarkMode, ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 测试ble耳机固件版本升级 + _buildSimpleNavigationSetting( + title: 'ota升级', + subtitle: '测试ble耳机固件版本升级', + icon: Icons.record_voice_over, + iconBgColor: isDarkMode + ? Colors.orange[900]!.withOpacity(0.3) + : Colors.orange[100]!, + iconColor: + isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, + onTap: () => Get.toNamed(Routes.OTA_UPGRADE), + isDarkMode: isDarkMode, + ), Divider( height: 1, color: isDarkMode @@ -1190,8 +1211,7 @@ class SettingsView extends GetView { Obx(() { final serviceType = controller.speechServiceType.value; final serviceName = controller.availableSpeechServices - .firstWhere( - (service) => service['type'] == serviceType, + .firstWhere((service) => service['type'] == serviceType, orElse: () => {'name': 'unknown'})['name']; return Container( constraints: BoxConstraints(maxWidth: 120.w), // 限制最大宽度 diff --git a/lib/modules/speech_test/controllers/speech_test_controller.dart b/lib/modules/speech_test/controllers/speech_test_controller.dart index 9274f369f..4a9d195f1 100644 --- a/lib/modules/speech_test/controllers/speech_test_controller.dart +++ b/lib/modules/speech_test/controllers/speech_test_controller.dart @@ -11,47 +11,48 @@ import '../../../core/utils/logger.dart'; class SpeechTestController extends GetxController { // 语音服务工厂 final SpeechFactory _speechFactory = Get.find(); - + // 当前使用的TTS服务 late TtsService _ttsService; - + // 当前使用的ASR服务 late AsrService _asrService; - + // 状态变量 final RxBool isInitialized = false.obs; final RxBool isProcessing = false.obs; final RxBool isListening = false.obs; final RxBool isSpeaking = false.obs; - + // 识别结果 final RxString intermediateResult = ''.obs; // 中间结果 - final RxString finalResult = ''.obs; // 最终结果 + final RxString finalResult = ''.obs; // 最终结果 final RxString recognitionStatus = '准备就绪'.obs; - final RxString detectedLanguage = ''.obs; // 检测到的语言 - + final RxString detectedLanguage = ''.obs; // 检测到的语言 + // TTS事件 final RxString ttsStatus = '准备就绪'.obs; - + // 当前选择的语音服务类型 final Rx selectedServiceType = SpeechServiceType.azure.obs; - + // 音频输入源 + bool _audioSourceType = false; // 连续识别结果流 StreamSubscription? _recognitionSubscription; - + // TTS事件流 StreamSubscription? _ttsEventSubscription; - + // 文本控制器 final TextEditingController textController = TextEditingController(); - + // 连续播放的测试句子 final List testSentences = [ '这是第一句测试语音。', '这是第二句测试语音,语音合成支持连续播放。', '这是第三句测试语音,将在连续模式下播放完毕。' ]; - + @override void onInit() { super.onInit(); @@ -59,23 +60,23 @@ class SpeechTestController extends GetxController { textController.text = '欢迎使用语音测试,这是一段默认的测试文本。您可以修改此文本,然后点击播放按钮测试语音合成效果。'; _initServices(); } - + /// 初始化服务 Future _initServices() async { try { isProcessing.value = true; - + // 获取服务实例 _ttsService = _speechFactory.getTtsService(); _asrService = _speechFactory.getAsrService(); - + // 初始化服务 await _ttsService.initialize(); await _asrService.initialize(); - + // 监听TTS事件 _setupTtsEventListener(); - + isInitialized.value = true; ttsStatus.value = '准备就绪'; recognitionStatus.value = '准备就绪'; @@ -87,12 +88,12 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 设置TTS事件监听 void _setupTtsEventListener() { // 取消旧的订阅 _ttsEventSubscription?.cancel(); - + // 设置新的订阅 _ttsEventSubscription = _ttsService.onEvent.listen((event) { switch (event.type) { @@ -115,18 +116,18 @@ class SpeechTestController extends GetxController { } }); } - + /// 设置ASR事件监听 void _setupAsrEventListener(Stream stream) { // 取消旧的订阅 _recognitionSubscription?.cancel(); - + // 重置识别结果 intermediateResult.value = ''; finalResult.value = ''; detectedLanguage.value = ''; recognitionStatus.value = '正在等待语音输入...'; - + // 设置新的订阅 _recognitionSubscription = stream.listen((event) { switch (event.type) { @@ -134,14 +135,14 @@ class SpeechTestController extends GetxController { isListening.value = true; recognitionStatus.value = '开始聆听...'; break; - + case RecognitionEventType.sessionStopped: isListening.value = false; recognitionStatus.value = '聆听已结束'; // 清空中间结果,保留最终结果 intermediateResult.value = ''; break; - + case RecognitionEventType.intermediateResult: // 更新中间结果,显示为斜体或不同颜色 intermediateResult.value = event.text; @@ -150,7 +151,7 @@ class SpeechTestController extends GetxController { } recognitionStatus.value = '正在识别...'; break; - + case RecognitionEventType.finalResult: // 更新最终结果,显示为正常文本 finalResult.value = event.text; @@ -160,12 +161,12 @@ class SpeechTestController extends GetxController { } recognitionStatus.value = '识别完成'; break; - + case RecognitionEventType.error: isListening.value = false; recognitionStatus.value = '识别错误: ${event.error}'; break; - + case RecognitionEventType.canceled: isListening.value = false; recognitionStatus.value = '识别已取消'; @@ -179,21 +180,21 @@ class SpeechTestController extends GetxController { intermediateResult.value = ''; }); } - + /// 切换语音服务类型 void switchServiceType(SpeechServiceType type) async { if (selectedServiceType.value == type) return; - + try { isProcessing.value = true; - + // 停止当前的所有服务状态 await stopAll(); - + // 切换服务类型 _speechFactory.setServiceType(type); selectedServiceType.value = type; - + // 重新初始化服务 await _initServices(); } catch (e) { @@ -202,40 +203,40 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 停止所有活动 Future stopAll() async { // 停止ASR if (isListening.value) { await stopContinuousRecognition(); } - + // 停止TTS if (isSpeaking.value) { await stopSpeaking(); } - + // 取消所有订阅 await _recognitionSubscription?.cancel(); _recognitionSubscription = null; } - + /// 单次播放文本 Future speakOnce() async { if (!isInitialized.value || isProcessing.value) return; - + final text = textController.text.trim(); if (text.isEmpty) { ttsStatus.value = '请输入要播放的文本'; return; } - + try { isProcessing.value = true; ttsStatus.value = '准备播放...'; - + final result = await _ttsService.speakOnce(text); - + if (!result) { ttsStatus.value = '播放失败'; } @@ -246,15 +247,15 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 连续播放测试 Future speakContinuously() async { if (!isInitialized.value || isProcessing.value) return; - + try { isProcessing.value = true; ttsStatus.value = '准备连续播放...'; - + // 为避免阻塞,使用一个句子接一个句子的方式添加到队列 for (final sentence in testSentences) { if (!await _ttsService.speakStream(sentence)) { @@ -264,10 +265,9 @@ class SpeechTestController extends GetxController { // 给前一个句子一点处理时间 await Future.delayed(const Duration(milliseconds: 50)); } - + // 刷新队列,确保播放完成 await _ttsService.flushStream(); - } catch (e) { ttsStatus.value = '连续播放异常: $e'; Logger.error('连续播放异常: $e'); @@ -275,11 +275,11 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 停止播放 Future stopSpeaking() async { if (!isInitialized.value) return; - + try { await _ttsService.stop(); } catch (e) { @@ -287,39 +287,39 @@ class SpeechTestController extends GetxController { Logger.error('停止播放异常: $e'); } } - + /// 一次性识别 Future recognizeOnce() async { if (!isInitialized.value || isProcessing.value || isListening.value) return; - + try { isProcessing.value = true; recognitionStatus.value = '开始识别...'; - + // 清空之前的结果 intermediateResult.value = ''; finalResult.value = ''; detectedLanguage.value = ''; - + // 显示正在进行的状态 recognitionStatus.value = '正在聆听...'; - + // 执行一次性识别 - 这是同步等待结果的操作 final result = await _asrService.recognizeOnce(); - + if (result.isError) { recognitionStatus.value = '识别错误: ${result.error}'; Logger.error('一次性识别错误: ${result.error}'); } else { // 更新最终结果 finalResult.value = result.text; - + // 更新语言信息 if (result.detectedLanguage.isNotEmpty) { detectedLanguage.value = result.detectedLanguage; Logger.debug('检测到语言: ${result.detectedLanguage}'); } - + recognitionStatus.value = '识别完成'; } } catch (e) { @@ -329,26 +329,26 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 开始连续识别 Future startContinuousRecognition() async { if (!isInitialized.value || isProcessing.value || isListening.value) return; - + try { isProcessing.value = true; - + // 清空之前的结果 intermediateResult.value = ''; finalResult.value = ''; detectedLanguage.value = ''; recognitionStatus.value = '准备连续识别...'; - + // 获取识别事件流 - 这启动了异步识别过程 - final stream = await _asrService.startContinuousRecognition(); - + final stream = + await _asrService.startContinuousRecognition(_audioSourceType); + // 设置事件监听 - 异步处理识别结果 _setupAsrEventListener(stream); - } catch (e) { recognitionStatus.value = '启动连续识别异常: $e'; Logger.error('启动连续识别异常: $e'); @@ -359,19 +359,19 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 停止连续识别 Future stopContinuousRecognition() async { if (!isInitialized.value || !isListening.value) return; - + try { // 首先更新状态,防止重复调用 isListening.value = false; recognitionStatus.value = '正在停止识别...'; - + // 停止识别过程 await _asrService.stopContinuousRecognition(); - + // 更新UI状态 recognitionStatus.value = '连续识别已停止'; intermediateResult.value = ''; @@ -380,16 +380,16 @@ class SpeechTestController extends GetxController { Logger.error('停止连续识别异常: $e'); } } - + // 获取识别结果(用于UI显示) String getDisplayResult() { // 如果有最终结果,优先显示 String displayText = ''; - + if (finalResult.value.isNotEmpty) { displayText = finalResult.value; } - + // 如果有中间结果,添加到最终结果后(可在UI中使用不同样式显示) if (intermediateResult.value.isNotEmpty) { if (displayText.isNotEmpty) { @@ -398,19 +398,19 @@ class SpeechTestController extends GetxController { displayText = '正在识别: ${intermediateResult.value}'; } } - + return displayText; } - + @override void onClose() { // 取消所有订阅 _recognitionSubscription?.cancel(); _ttsEventSubscription?.cancel(); - + // 释放控制器 textController.dispose(); - + super.onClose(); } -} \ No newline at end of file +} diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index fc97056c5..7962d629f 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -9,28 +9,9 @@ import '../../../data/services/language_manager.dart'; import '../../../core/utils/logger.dart'; import '../../../data/models/events/voice_interaction_event.dart'; import '../../../data/services/asr_service.dart'; -import 'package:jieli/jieli.dart'; +import '../../../data/services/ble_manager.dart'; class TranslationController extends GetxController { - late Jieli jieli; - // 事件订阅 - StreamSubscription? _scanSubscription; - StreamSubscription? _connectionSubscription; - StreamSubscription? _wakeupSubscription; - StreamSubscription? _audioSubscription; - StreamSubscription? _commandResultSubscription; - // 状态变量 - var isScanning = false.obs; - var deviceInfo = '未获取'.obs; - var connectStatus = '未连接'.obs; - var currentDevice = Rx(null); - var scannedDevices = RxList([]); - var wakeupReceived = false.obs; - var audioDataSize = 0.obs; - var receivedFrames = 0.obs; - var audioDataReceiving = false.obs; - var volumeCommandResult = ''.obs; - // 服务 final AsrService _asrService = Get.find(); final VolcanoTranslationService _translationService = @@ -38,7 +19,10 @@ class TranslationController extends GetxController { final TtsService _ttsService = Get.find(); final LanguageManager _languageManager = Get.find(); final GetStorage _storage = GetStorage(); - + // 蓝牙服务 + final _bleManager = Get.find(); + // 音频输入源 + bool _audioSourceType = false; // 存储相关 static const String _historyKey = 'translation_history'; static const String _sourceLanguageKey = 'translation_source_language'; @@ -104,7 +88,7 @@ class TranslationController extends GetxController { @override void onInit() { super.onInit(); - jieli = Jieli(); + //_setupEventListeners(); // 加载上次保存的语言设置 _loadSavedLanguages(); @@ -240,19 +224,23 @@ class TranslationController extends GetxController { // // 添加到历史记录 // translationHistory.add(newItem); + // 开始连续语音识别 if (currentMode.value == "audioVideo") { + _bleManager.openDecoder(); // 发送ble音乐或者通话远端声音 - + _audioSourceType = true; Logger.info('发送ble音乐或者通话远端声音'); } else if (currentMode.value == "call") { // 发送ble系统mic和dac(音乐或者通话远端)声音 - + _bleManager.openA2DPDecoder(); + _audioSourceType = true; Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); + } else { + // 开始连续语音识别 + _audioSourceType = false; } - - // 开始连续语音识别 - final recognitionStream = await _asrService.startContinuousRecognition(); - + final recognitionStream = + await _asrService.startContinuousRecognition(_audioSourceType); isRecognizing.value = true; // 监听识别结果 diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 1e7281662..27bfb7f48 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -41,6 +41,8 @@ import '../modules/speech_test/views/speech_test_view.dart'; import '../modules/speech_test/bindings/speech_test_binding.dart'; import '../modules/agent/views/agent_view.dart'; import '../modules/agent/bindings/agent_binding.dart'; +import '../modules/ota_upgrade/views/ota_upgrade_view.dart'; +import '../modules/ota_upgrade/bindings/ota_upgrade_binding.dart'; abstract class AppPages { static final pages = [ @@ -79,6 +81,16 @@ abstract class AppPages { page: () => const ChatView(), binding: ChatBinding(), ), + GetPage( + name: Routes.recording, + page: () => const ChatView(), + binding: ChatBinding(), + ), + GetPage( + name: Routes.OTA_UPGRADE, + page: () => const OtaUpgradeView(), + binding: OtaUpgradeBinding(), + ), GetPage( name: Routes.profile, page: () => const ProfileView(), @@ -144,13 +156,11 @@ abstract class AppPages { page: () => const OpusTestView(), binding: OpusTestBinding(), ), - GetPage( name: Routes.speechTest, page: () => const SpeechTestView(), binding: SpeechTestBinding(), ), - GetPage( name: Routes.agent, page: () => const AgentView(), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 41a7997c1..d44a5616c 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -6,6 +6,8 @@ abstract class Routes { static const home = '/home'; static const explore = '/explore'; static const chat = '/chat'; + static const OTA_UPGRADE = '/ota_upgrade'; + static const recording = '/recording'; static const profile = '/profile'; static const meeting = '/meeting'; static const meetingRecord = '/meetingRecord'; diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 41bba4f7c..1f22c9328 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -288,7 +288,7 @@ object AgentService : CoroutineScope { // 检查录音权限 - if (!isExternalActive||!checkRecordAudioPermission()) { + if (!isExternalActive&&!checkRecordAudioPermission()) { FileLogger.e(TAG, "需要录音权限,请在设置中授予权限") sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) return false diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index 9a97dce58..788f60099 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -172,7 +172,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope { } } "startContinuousRecognition" -> { - val useExternalAudio = call.argument("useExternalAudio") ?: false + val useExternalAudio = call.argument("audioSourceType") ?: false // 确保事件通道已准备好 if (asrEventSink == null) { result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null) @@ -187,6 +187,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope { } else { AzureAsrHelper.AudioSourceType.MICROPHONE } + val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { override fun onResult(text: String, detectedLanguage: String) { sendAsrEvent(mapOf( diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 4ec732c33..1ed8e9dbc 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -999,8 +999,45 @@ object BleService { * 控制编解码 - 打开解码 */ fun openDecoder(): Boolean { + FileLogger.i(TAG, "打开编码 (设备侧)...") + + val opusStarted = startOpusStreamDecoding( + hasHeader = false, // BLE传输的Opus数据通常无文件头 + channel = 1, // 单声道 + sampleRate = 16000,// Opus推荐16kHz或更高 + packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM) + // packetSize for Opus is typically in ms, e.g. 20, 40, 60. + // The library might expect this in a different unit or derive it. + // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms. + ) + + FileLogger.i(TAG, "打开解码...") - return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte())) + return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte(), + + BleConst.AUDIO_CHANNEL_RIGHT.toByte())) + } + /** + * 控制编解码 - 打开解码 + */ + fun openA2DPDecoder(): Boolean { + FileLogger.i(TAG, "打开编码 (设备侧)...") + + val opusStarted = startOpusStreamDecoding( + hasHeader = false, // BLE传输的Opus数据通常无文件头 + channel = 1, // 单声道 + sampleRate = 16000,// Opus推荐16kHz或更高 + packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM) + // packetSize for Opus is typically in ms, e.g. 20, 40, 60. + // The library might expect this in a different unit or derive it. + // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms. + ) + + + FileLogger.i(TAG, "打开解码...") + return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), + + BleConst.AUDIO_CHANNEL_RIGHT.toByte())) } /** diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index 055d791fb..1701f46f1 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -151,6 +151,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { "openDecoder" -> { val success = BleService.openDecoder() result.success(success) + } + "openA2DPDecoder" -> { + val success = BleService.openA2DPDecoder() + result.success(success) } "closeCodec" -> { val success = BleService.closeCodec() diff --git a/local_plugins/ble_service/lib/ble_service.dart b/local_plugins/ble_service/lib/ble_service.dart index f554a8761..b19229f7a 100644 --- a/local_plugins/ble_service/lib/ble_service.dart +++ b/local_plugins/ble_service/lib/ble_service.dart @@ -15,32 +15,35 @@ class BleConnectionState { /// BLE服务插件 class BleService { static final BleService _instance = BleService._internal(); - + /// 获取单例 factory BleService() => _instance; - + // 方法通道 - final MethodChannel _methodChannel = const MethodChannel('com.yunqiinnovation.ble_service'); - + final MethodChannel _methodChannel = + const MethodChannel('com.yunqiinnovation.ble_service'); + // 事件通道 - 简化为只使用数据和状态通道 late final EventChannel _dataEventChannel; late final EventChannel _statusEventChannel; - + // 事件流 late final Stream _dataStream; late final Stream _statusStream; - + // 获取事件流 Stream get onDataReceived => _dataStream; - + // 状态流直接传递原始数据 Stream get onStatusUpdated => _statusStream; - + // 内部构造函数 BleService._internal() { - _dataEventChannel = const EventChannel('com.yunqiinnovation.ble_service/data_events'); - _statusEventChannel = const EventChannel('com.yunqiinnovation.ble_service/status_events'); - + _dataEventChannel = + const EventChannel('com.yunqiinnovation.ble_service/data_events'); + _statusEventChannel = + const EventChannel('com.yunqiinnovation.ble_service/status_events'); + // 初始化事件流 _dataStream = _dataEventChannel.receiveBroadcastStream(); _statusStream = _statusEventChannel.receiveBroadcastStream(); @@ -56,7 +59,7 @@ class BleService { return false; } } - + /// 获取电池电量 Future getBatteryLevel() async { try { @@ -67,18 +70,19 @@ class BleService { return 0; } } - + /// 获取设备版本信息 Future getDeviceVersion() async { try { - final result = await _methodChannel.invokeMethod('getDeviceVersion'); + final result = + await _methodChannel.invokeMethod('getDeviceVersion'); return result ?? false; } catch (e) { print('获取设备版本失败: $e'); return false; } } - + /// 获取电池信息 Future getBatteryInfo() async { try { @@ -89,7 +93,7 @@ class BleService { return false; } } - + /// 打开编码器 Future openEncoder() async { try { @@ -100,7 +104,7 @@ class BleService { return false; } } - + /// 打开解码器 Future openDecoder() async { try { @@ -111,7 +115,18 @@ class BleService { return false; } } - + + /// 打开解码器 + Future openA2DPDecoder() async { + try { + final result = await _methodChannel.invokeMethod('openA2DPDecoder'); + return result ?? false; + } catch (e) { + print('打开解码器失败: $e'); + return false; + } + } + /// 关闭编解码器 Future closeCodec() async { try { @@ -122,7 +137,7 @@ class BleService { return false; } } - + /// 播放A2DP Future playA2DP() async { try { @@ -133,7 +148,7 @@ class BleService { return false; } } - + /// 启动BLE后台服务 /// [config] 配置参数,可包含: /// - scan_interval: 扫描间隔时间(毫秒) @@ -144,14 +159,15 @@ class BleService { /// - device_name: 设备名称 Future startBleService(Map config) async { try { - final result = await _methodChannel.invokeMethod('startBleService', config); + final result = + await _methodChannel.invokeMethod('startBleService', config); return result ?? false; } catch (e) { print('启动BLE后台服务失败: $e'); return false; } } - + /// 停止BLE后台服务 Future stopBleService() async { try { @@ -162,40 +178,42 @@ class BleService { return false; } } - + /// 注册BLE后台扫描 - /// + /// /// 注册后,即使应用被杀死,系统也会在检测到设备广播时唤醒应用 /// 这是确保设备可以在应用后台时被自动连接的关键功能 Future registerBackgroundScan() async { try { - final result = await _methodChannel.invokeMethod('registerBackgroundScan'); + final result = + await _methodChannel.invokeMethod('registerBackgroundScan'); return result ?? false; } catch (e) { print('注册后台扫描失败: $e'); return false; } } - + /// 注销BLE后台扫描 - /// + /// /// 当不再需要后台扫描功能时调用,例如用户主动断开连接时 Future unregisterBackgroundScan() async { try { - final result = await _methodChannel.invokeMethod('unregisterBackgroundScan'); + final result = + await _methodChannel.invokeMethod('unregisterBackgroundScan'); return result ?? false; } catch (e) { print('注销后台扫描失败: $e'); return false; } } - + //======================================================================== // 以下是ChatAgent相关功能 //======================================================================== - + /// 初始化语音交互代理 - /// + /// /// [config] 配置参数,必须包含: /// - azureSpeechKey: Azure语音服务API密钥 /// - azureSpeechRegion: Azure语音服务区域 @@ -204,16 +222,17 @@ class BleService { /// - openaiModel: OpenAI模型名称,默认为'gpt-3.5-turbo' Future initializeChatAgent(Map config) async { try { - final result = await _methodChannel.invokeMethod('initializeChatAgent', config); + final result = await _methodChannel.invokeMethod( + 'initializeChatAgent', config); return result ?? false; } catch (e) { print('初始化语音交互代理失败: $e'); return false; } } - + /// 启动语音识别 - /// + /// /// 用于手动启动语音识别,通常不需要直接调用,因为在唤醒时会自动启动 Future startListening() async { try { @@ -224,7 +243,7 @@ class BleService { return false; } } - + /// 停止语音识别 Future stopListening() async { try { @@ -235,14 +254,16 @@ class BleService { return false; } } - + /// 处理文本输入 - /// + /// /// [text] 用户输入的文本 /// [speakResponse] 是否使用TTS朗读回复,默认为false - Future processTextInput(String text, {bool speakResponse = false}) async { + Future processTextInput(String text, + {bool speakResponse = false}) async { try { - final result = await _methodChannel.invokeMethod('processTextInput', { + final result = + await _methodChannel.invokeMethod('processTextInput', { 'text': text, 'speakResponse': speakResponse, }); @@ -252,7 +273,7 @@ class BleService { return false; } } - + /// 停止TTS播放 Future stopTts() async { try { @@ -263,4 +284,4 @@ class BleService { return false; } } -} \ No newline at end of file +}