diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index f3c13c5fc..1813b0c5a 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -24,7 +24,7 @@ val keystoreProperties =Properties().apply { android { namespace = "com.saitong.voitrans" - compileSdk = 35 + compileSdk = 36 ndkVersion = "28.0.13004108" compileOptions { diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index afa1e8eb0..efdcc4ace 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index af6904e7f..5f55eb578 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -25,8 +25,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") - id("com.android.application") version "8.8.0" apply false - id("com.android.library") version "8.8.0" apply false + id("com.android.application") version "8.9.1" apply false + id("com.android.library") version "8.9.1" apply false id("org.jetbrains.kotlin.android") version "2.1.10" apply false } diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index e84caa5af..3541bb74a 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -540,6 +540,7 @@ const Map enUS = { "loadMoreMessages": "Load more messages...", "loadFailedClickRetry": "Load failed, click to retry", "swipeUpForMore": "Swipe up for more", + "callingTool": "I'm helping you, please wait!", // 正在帮您,请稍等! // 预设提示词 "presetMusic": "Play some music", // Play some music diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 63d24ebbb..cfee20434 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -511,7 +511,7 @@ const Map zhCN = { "loadMoreMessages": "加载更多消息...", "loadFailedClickRetry": "加载失败,点击重试", "swipeUpForMore": "向上滑动查看更多", - "callingTool": "正在调用工具!", + "callingTool": "正在帮您,请稍等!", // 预设提示词 "presetMusic": "来点音乐", // 来点音乐 diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart index 7f7b6bd59..65ea7838e 100644 --- a/lib/data/models/appconfig_model.dart +++ b/lib/data/models/appconfig_model.dart @@ -79,6 +79,7 @@ class DBProduct { final String? productimage; final String scanuuid; final String? version; + final bool? iscontinuouschat; final String? updatedescription; final String? updatepackageaddress; @@ -90,6 +91,7 @@ class DBProduct { required this.productimage, required this.scanuuid, required this.version, + required this.iscontinuouschat, required this.updatedescription, required this.updatepackageaddress, }); diff --git a/lib/data/models/appconfig_model.g.dart b/lib/data/models/appconfig_model.g.dart index e75d17437..f99ba35c6 100644 --- a/lib/data/models/appconfig_model.g.dart +++ b/lib/data/models/appconfig_model.g.dart @@ -79,6 +79,7 @@ DBProduct _$DBProductFromJson(Map json) => DBProduct( productimage: json['productimage'] as String?, scanuuid: json['scanuuid'] as String, version: json['version'] as String?, + iscontinuouschat: json['iscontinuouschat'] as bool?, updatedescription: json['updatedescription'] as String?, updatepackageaddress: json['updatepackageaddress'] as String?, ); @@ -86,5 +87,12 @@ DBProduct _$DBProductFromJson(Map json) => DBProduct( Map _$DBProductToJson(DBProduct instance) => { 'id': instance.id, 'factoryid': instance.factoryid, + 'devicetype': instance.devicetype, + 'productimage': instance.productimage, + 'scanuuid': instance.scanuuid, + 'version': instance.version, + 'iscontinuouschat': instance.iscontinuouschat, 'devicename': instance.devicename, + 'updatedescription': instance.updatedescription, + 'updatepackageaddress': instance.updatepackageaddress, }; diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index ef78d9290..970cf6d5c 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -6,6 +6,7 @@ import 'package:get_storage/get_storage.dart'; import 'package:get/get.dart'; import 'package:ble_service/ble_service.dart'; import 'package:agent_service/agent_service.dart'; +import 'package:voitrans/data/models/appconfig_model.dart'; import '../../data/models/appconfig.dart'; import '../../core/utils/logger.dart'; @@ -639,6 +640,35 @@ class BleManager extends GetxService { } } + String getDeviceName() { + if (deviceInfo.value != null) { + return deviceInfo.value!.name; + } + return ""; + } + + // 加载TTS偏好设置 + Future _loadTtsInterrupe() async { + try { + if (!_storage.hasData("agent_tts_interrupe")) { + // 获取设备类型以确定检查策略 + String deviceName = getDeviceName(); + DBProduct? product = AppConfig.getproduct(deviceName); + if (deviceName != "" && product != null) { + // Logger.d('AgentController', + // 'liwe----------product $deviceName ${product.toJson()}'); + return !product.iscontinuouschat!; + } else { + return false; + } + } else { + return _storage.read('agent_tts_interrupe') ?? false; + } + } catch (e) { + return false; + } + } + /// 启动BLE服务 Future startAgentService() async { try { @@ -668,6 +698,8 @@ class BleManager extends GetxService { // 从Flutter资源包中加载.mcp.json final jsonData = AppConfig.mcpConfig(); Logger.info('启动BLE服务,musicServiceType: ${musicServiceType}'); + final isTtsInterruptEnabled = await _loadTtsInterrupe(); + // 根据APP语言动态配置语音识别语言 List supportedLanguages = []; try { @@ -737,6 +769,7 @@ class BleManager extends GetxService { 'supportedLanguages': supportedLanguages, 'musicServiceType': musicServiceType, "navigationMode": navigationMode, + "isttsinterrupt": isTtsInterruptEnabled ? "true" : "false", "callingTool": "callingTool".tr }; diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 83016d4ef..ad10e5d70 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -6,6 +6,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:uuid/uuid.dart'; import 'package:voitrans/data/models/appconfig.dart'; +import 'package:voitrans/data/models/appconfig_model.dart'; import 'package:voitrans/data/models/song_model.dart'; import 'package:voitrans/data/services/ble_manager.dart'; import 'package:voitrans/data/services/bluetooth_manager.dart'; @@ -40,6 +41,7 @@ enum VoiceInputMode { class AgentController extends GetxController with WidgetsBindingObserver { static AgentController get to => Get.find(); //单例访问模式 + final GetStorage _storage = GetStorage(); // 聊天消息列表 final messages = [].obs; final currsessionid = ''.obs; @@ -77,7 +79,10 @@ class AgentController extends GetxController with WidgetsBindingObserver { final isProcessing = false.obs; // TTS启用状态 final isTtsEnabled = true.obs; - + // TTS播报打断启用状态 + final isTtsInterruptEnabled = false.obs; + // 是否显示工具调用 + final isShowToolInterruptEnabled = false.obs; // 图片处理状态 final isImageProcessing = false.obs; @@ -162,6 +167,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { super.onInit(); _initAppDir(); _loadTtsPreference(); //加载TTS偏好设置 + _loadTtsInterrupe(); //加载TTS播报打断偏好设置 _setupScrollListener(); //设置滚动监听器 _loadChatHistory(); //加载聊天历史记录 _subscribeToEvents(); //订阅代理服务事件 @@ -257,6 +263,50 @@ class AgentController extends GetxController with WidgetsBindingObserver { } } + // 加载TTS偏好设置 + Future _loadTtsInterrupe() async { + try { + if (!_storage.hasData("agent_tts_interrupe")) { + // 获取设备类型以确定检查策略 + String deviceName = getDeviceName(); + DBProduct? product = AppConfig.getproduct(deviceName); + if (deviceName != "" && product != null) { + // Logger.d('AgentController', + // 'liwe----------product $deviceName ${product.toJson()}'); + isTtsInterruptEnabled.value = !product.iscontinuouschat!; + } else { + isTtsInterruptEnabled.value = false; + } + } else { + isTtsInterruptEnabled.value = + _storage.read('agent_tts_interrupe') ?? false; + } + await AgentService.isInterrupt(isTtsInterruptEnabled.value); + } catch (e) { + // 如果读取失败,使用默认值 + Logger.e(TAG, '加载TTS偏好设置失败: $e'); + isTtsInterruptEnabled.value = false; + } + } + + /// 切换TTS播报打断状态 + /// 当启用播报打断时,用户说话会自动停止当前的TTS播放 + Future toggleTtsInterrupt() async { + isTtsInterruptEnabled.toggle(); + await _storage.write('agent_tts_interrupe', isTtsInterruptEnabled.value); + // 保存偏好设置 + try { + await AgentService.isInterrupt(isTtsInterruptEnabled.value); + } catch (e) { + Logger.e(TAG, '保存TTS播报打断偏好设置失败: $e'); + } + } + + /// 当启用显示工具调用效果 + Future toggleShowAIToolInterrupt() async { + isShowToolInterruptEnabled.toggle(); + } + // 设置文本输入监听器 void _setupTextInputListener() { textController.addListener(() { @@ -530,7 +580,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { final sessionid = event.data['sessionid'] ?? ''; final text = event.data['text'] ?? ''; currentText.value = text; // 保留当前文本,以便其他地方使用 - Logger.i(TAG, '识别中间结果: $text'); + Logger.i(TAG, '[$sessionid]识别中间结果: $text'); if (text.isNotEmpty) { // 查找是否有正在识别中的消息 final index = messages.lastIndexWhere( @@ -562,7 +612,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { case AgentServiceEventType.recognitionResult: final sessionid = event.data['sessionid'] ?? ''; final text = event.data['text'] ?? ''; - Logger.i(TAG, '识别最终结果: $text'); + Logger.i(TAG, '[$sessionid]识别最终结果: $text'); // 查找是否有正在识别中的消息 final index = messages.lastIndexWhere( (msg) => msg.sessionid == sessionid && msg.isUser); @@ -2765,4 +2815,11 @@ class AgentController extends GetxController with WidgetsBindingObserver { message.isToolCallsExpanded = isExpanded; messages.refresh(); } + + String getDeviceName() { + if (_bluetoothManager.currentDevice != null) { + return _bluetoothManager.currentDevice!.name; + } + return ""; + } } diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 3f4dd0dd1..d24352ca6 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -172,6 +172,73 @@ class _AgentViewState extends State with WidgetsBindingObserver { ), color: isDarkMode ? Colors.grey[800] : Colors.white, itemBuilder: (context) => [ + // 播报打断开关选项 + PopupMenuItem( + child: Obx(() => Row( + children: [ + Icon( + controller.isShowToolInterruptEnabled.value + ? Icons.build_circle + : Icons.build_circle_outlined, + color: controller.isShowToolInterruptEnabled.value + ? primaryColor + : (isDarkMode ? Colors.grey[400] : Colors.grey), + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + //'ttsInterruptToggle'.tr, + "显示工具", + style: TextStyle(fontSize: 14.sp), + ), + const Spacer(), + Switch( + value: controller.isShowToolInterruptEnabled.value, + onChanged: (value) => + controller.toggleShowAIToolInterrupt(), + activeColor: primaryColor, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + ], + )), + onTap: () => controller.toggleTtsInterrupt(), + ), + + // 播报打断开关选项 + PopupMenuItem( + child: Obx(() => Row( + children: [ + Icon( + controller.isTtsInterruptEnabled.value + ? Icons.voice_over_off + : Icons.voice_over_off_outlined, + color: controller.isTtsInterruptEnabled.value + ? primaryColor + : (isDarkMode ? Colors.grey[400] : Colors.grey), + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + //'ttsInterruptToggle'.tr, + "播报打断", + style: TextStyle(fontSize: 14.sp), + ), + const Spacer(), + Switch( + value: controller.isTtsInterruptEnabled.value, + onChanged: (value) => + controller.toggleTtsInterrupt(), + activeColor: primaryColor, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + ], + )), + onTap: () => controller.toggleTtsInterrupt(), + ), + + // 清除聊天记录选项 PopupMenuItem( child: Row( children: [ diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index bf2b04983..e8078f13b 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -84,7 +84,8 @@ class MessageBubble extends GetView { ], ), child: Padding( - padding: (!isUser && + padding: (controller.isShowToolInterruptEnabled.value && + !isUser && functionCalls != null && functionCalls!.isNotEmpty) ? const EdgeInsets.only(top: 30) @@ -93,7 +94,8 @@ class MessageBubble extends GetView { ), ), //工具调用流程显示(仅在AI消息且有functionCalls时显示) - if (!isUser && + if (controller.isShowToolInterruptEnabled.value && + !isUser && functionCalls != null && functionCalls!.isNotEmpty) _buildFunctionCallsWidget(context), @@ -112,7 +114,7 @@ class MessageBubble extends GetView { Text( _formatTimestamp(timestamp), style: TextStyle( - fontSize: 14, + fontSize: 8, color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[400] : Colors.grey[500], @@ -183,12 +185,20 @@ class MessageBubble extends GetView { ); } - if (!isUser && message.isEmpty && functionCalls == null && meta == null) { + if (!isUser && + message.isEmpty && + (functionCalls == null || + !controller.isShowToolInterruptEnabled.value) && + meta == null) { return _buildTypingIndicator(); } // 如果只有工具调用而没有文本内容,显示一个占位符确保气泡有足够宽度 - if (!isUser && message.isEmpty && functionCalls != null && meta == null) { + if (!isUser && + message.isEmpty && + functionCalls != null && + controller.isShowToolInterruptEnabled.value && + meta == null) { return Container( constraints: const BoxConstraints( minWidth: 200, // 确保最小宽度 diff --git a/lib/modules/opus_test/controllers/opus_test_controller.dart b/lib/modules/opus_test/controllers/opus_test_controller.dart index 81ee98093..b22cc4b5e 100644 --- a/lib/modules/opus_test/controllers/opus_test_controller.dart +++ b/lib/modules/opus_test/controllers/opus_test_controller.dart @@ -140,7 +140,7 @@ class OpusTestController extends GetxController { for (var entity in files) { if (entity is File) { final path = entity.path.toLowerCase(); - if (path.endsWith('.opus')) { + if (path.endsWith('.opus') || path.endsWith('.txt')) { final fileSize = await entity.length(); externalFiles.add(PlatformFile( name: entity.path.split('/').last, diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 8b1d2e43b..3347794bd 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -672,9 +672,10 @@ class PairingController extends GetxController with WidgetsBindingObserver { _verifiedDeviceAddress?.toUpperCase()) { // 已有关联,检查BLE连接状态 Logger.i(_tag, '设备已关联,检查BLE连接状态'); + print('fdp--connectble--pairedMacAddress: $pairedMacAddress'); final currentBleState = await _bleManager.getConnectionState(); Logger.i(_tag, '验证时BLE状态: $currentBleState'); - + print('fdp--connectble--currentBleState: $currentBleState'); if (currentBleState == 2) { // BleConnectionState.connected Logger.i(_tag, 'BLE已连接,直接完成配对'); @@ -700,21 +701,24 @@ class PairingController extends GetxController with WidgetsBindingObserver { return; } - Logger.i(_tag, '开始BLE设备关联: $_verifiedDeviceName:$_verifiedDeviceAddress'); - - // 调用BLE管理器的关联方法 - final success = await _bleManager.startDeviceAssociation( - deviceNamePattern: _verifiedDeviceName!, - macAddress: _verifiedDeviceAddress!, - ); + // Logger.i(_tag, '开始BLE设备关联: $_verifiedDeviceName:$_verifiedDeviceAddress'); + if (Platform.isIOS) { + // 调用BLE管理器的关联方法 + final success = await _bleManager.startDeviceAssociation( + deviceNamePattern: _verifiedDeviceName!, + macAddress: _verifiedDeviceAddress!, + ); - if (success) { - Logger.i(_tag, 'BLE设备关联成功'); - // 关联成功后,转到等待BLE连接状态 - _transition(PairingState.waitingBle); + if (success) { + Logger.i(_tag, 'BLE设备关联成功'); + //关联成功后,转到等待BLE连接状态 + _transition(PairingState.waitingBle); + } else { + _setError(ErrorType.associationFailed, + 'bleDeviceAssociationFailed'.tr); //BLE设备关联失败 + } } else { - _setError(ErrorType.associationFailed, - 'bleDeviceAssociationFailed'.tr); //BLE设备关联失败 + _transition(PairingState.waitingBle); } } catch (e) { Logger.e(_tag, 'BLE设备关联异常: $e'); @@ -803,13 +807,12 @@ class PairingController extends GetxController with WidgetsBindingObserver { } } else { //未绑定设备 - _bleManager.clearPairedDevices(); + _bleManager.clearPairedDevices(); Logger.i(_tag, '验证时BLE状态: $currentBleState'); if (currentBleState == 2) { //已连接 清理 // BleConnectionState.connected Logger.i(_tag, 'BLE已连接,断开并清理缓存'); - _bleManager.disconnect(); } } @@ -940,6 +943,11 @@ class PairingController extends GetxController with WidgetsBindingObserver { } } } + final currentBleState = await _bleManager.getConnectionState(); + if (currentBleState == 2) { + _bleManager.clearPairedDevices(); + _bleManager.disconnect(); + } await Future.delayed(Duration(seconds: 1)); } _bleManager.stopScan(); 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 aeb97e695..0ed92e073 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 @@ -2,6 +2,7 @@ package com.yunqiinnovation.agent_service import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.os.Handler import android.os.Looper import org.json.JSONArray @@ -62,6 +63,12 @@ interface AgentServiceListener { */ object AgentService : CoroutineScope { private val TAG = "AgentService" + + // SharedPreferences 相关常量 + private const val PREF_NAME = "agent_service_prefs" + private const val KEY_IS_INTERRUPT = "is_interrupt" + + private var _isInterrupt = AtomicBoolean(false) private var azureSpeechKey: String = "" private var azureSpeechRegion: String = "" @@ -196,8 +203,16 @@ object AgentService : CoroutineScope { serverurl = config["serverurl"]?.toString() ?: ""; usertoken = config["usertoken"]?.toString() ?: ""; callingTool = config["callingTool"]?.toString() ?: ""; + // 初始化上下文和服务 this.context = appContext.applicationContext + + // 从 SharedPreferences 读取 _isInterrupt 状态 + loadIsInterruptFromCache() + if (config["isttsinterrupt"] != null){ + _isInterrupt.set(config["isttsinterrupt"]?.toString() == "true") + } + this.chatApiService = ChatApiService(context) this.chatStorageHelper = ChatStorageHelper(context) this.audioPlayer = AudioPlayer(context) @@ -367,6 +382,9 @@ object AgentService : CoroutineScope { sendEvent("playback_started", mapOf("status" to "playback_started")) // 确保气泡音已停止,避免与TTS播放重叠 audioPlayer?.stopAudio() + if(_isInterrupt.get()){ + BleService.closeCodec() + } // 发送AI说话开始广播 val intent = Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply { @@ -390,6 +408,9 @@ object AgentService : CoroutineScope { setPackage(context.packageName) } context.sendBroadcast(intent) + if(currentRecognitionMode == "ble_wakeup"&&_isInterrupt.get()){ + BleService.openB1Encoder() + } if(isplaymusic){ isplaymusic = false Log.e(TAG, "QQ 音乐恢复播放") @@ -521,7 +542,21 @@ object AgentService : CoroutineScope { startIdleCheckForMode(currentRecognitionMode) } } - + /** + * 切换中断状态 + * 如果当前为false,则设置为true;如果当前为true,则设置为false + * @return 切换后的状态值 + */ + fun isInterrupt(isInterrupt: Boolean): Boolean { + // val currentValue = _isInterrupt.get() + // val newValue = !currentValue + _isInterrupt.set(isInterrupt) + + // 保存到 SharedPreferences + saveIsInterruptToCache(isInterrupt) + + return isInterrupt + } /** * 开始语音识别 * @param isExternalActive 是否使用外部音频源(蓝牙) @@ -617,6 +652,7 @@ object AgentService : CoroutineScope { sendEvent( "recognizing", mapOf( + "sessionid" to sessionid, "text" to recognizing, "language" to detectedLanguage ) @@ -1013,6 +1049,7 @@ object AgentService : CoroutineScope { override fun onComplete(sessionid: String) { try { if (sessionid == currsessionId) { + audioPlayer?.stopAudio() } // 视情况决定是否朗读回复 @@ -1090,10 +1127,11 @@ object AgentService : CoroutineScope { if (name == "exit_interaction") { BleService.closeCodec() stopRecognition() + }else if (name == "qqmusic_pause" || name == "music_pause"){ + isplaymusic = false } Log.d(TAG, "liwei--------------- AI Call onFunctionCall iscallingTool:$iscallingTool callingTool:$callingTool") if (iscallingTool) { - ttsService?.speakStream(callingTool) iscallingTool = false } @@ -2544,4 +2582,35 @@ object AgentService : CoroutineScope { } } } + + /** + * 从 SharedPreferences 加载 _isInterrupt 状态 + */ + private fun loadIsInterruptFromCache() { + try { + if (::context.isInitialized) { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + val cachedValue = prefs.getBoolean(KEY_IS_INTERRUPT, false) // 默认值为 false + _isInterrupt.set(cachedValue) + Log.d(TAG, "从缓存加载 _isInterrupt 状态: $cachedValue") + } + } catch (e: Exception) { + Log.e(TAG, "加载 _isInterrupt 缓存状态时发生异常: ${e.message}", e) + } + } + + /** + * 保存 _isInterrupt 状态到 SharedPreferences + */ + private fun saveIsInterruptToCache(isInterrupt: Boolean) { + try { + if (::context.isInitialized) { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + prefs.edit().putBoolean(KEY_IS_INTERRUPT, isInterrupt).apply() + Log.d(TAG, "保存 _isInterrupt 状态到缓存: $isInterrupt") + } + } catch (e: Exception) { + Log.e(TAG, "保存 _isInterrupt 缓存状态时发生异常: ${e.message}", e) + } + } } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index cccdacf62..39edf4c8d 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -77,6 +77,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["usertoken"]?.toString()?.let { intent.putExtra("usertoken", it) } config["musicServiceType"]?.toString()?.let { intent.putExtra("musicServiceType", it) } config["navigationMode"]?.toString()?.let { intent.putExtra("navigationMode", it) } + config["isttsinterrupt"]?.toString()?.let { intent.putExtra("isttsinterrupt", it) } config["callingTool"]?.toString()?.let { intent.putExtra("callingTool", it) } @@ -110,6 +111,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream "stopAgentService" -> { try { + Log.d(TAG, "停止AgentService成功") val intent = Intent(context, BleMediaService::class.java) context.stopService(intent) result.success(true) @@ -129,6 +131,16 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("START_ERROR", "启动对话失败: ${e.message}", null) } } + "isInterrupt" -> { + try { + val isinterrupt = call.argument("isinterrupt") + val _isInrrupt = isinterrupt?.let { AgentService.isInterrupt(isinterrupt) } + result.success(_isInrrupt) + } catch (e: Exception) { + Log.e(TAG, "检查是否打断失败", e) + result.error("IS_INTERRUPT_ERROR", "检查是否打断失败: ${e.message}", null) + } + } "recognizeCallback" -> { val isStarted = AgentService.recognizeCallback() result.success(isStarted) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt index 03dee6109..4d3f782c5 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt @@ -66,6 +66,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { return false } Log.d(TAG, "手动启动语音识别: ") + AgentService.interruptCurrentResponse() return AgentService.startRecognition(true, "ble_wakeup") } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index f12454569..e824eadcf 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -21,6 +21,15 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import com.yunqiinnovation.ble_service.BleConst import com.yunqiinnovation.ble_service.BleService +import com.yunqiinnovation.ble_service.BleCompanionManager +// 添加蓝牙相关导入 +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.content.BroadcastReceiver +import android.content.IntentFilter +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat /** * BLE代理服务 @@ -28,6 +37,7 @@ import com.yunqiinnovation.ble_service.BleService * 1. 语音代理初始化和管理 * 2. BLE设备连接管理 * 3. 后台扫描结果处理 + * 4. 蓝牙设备连接监听 */ class BleMediaService : Service(), CoroutineScope { @@ -37,6 +47,9 @@ class BleMediaService : Service(), CoroutineScope { private const val NOTIFICATION_ID = 1002 private const val PREF_NAME = "deep_voice_config" + // 目标蓝牙设备名称 + private const val TARGET_DEVICE_NAME = "DeapSound" + // ChatAgent配置参数键 private const val KEY_AZURE_SPEECH_KEY = "azureSpeechKey" private const val KEY_AZURE_SPEECH_REGION = "azureSpeechRegion" @@ -76,6 +89,309 @@ class BleMediaService : Service(), CoroutineScope { // 当前启动Intent private var currentIntent: Intent? = null + + // 蓝牙相关组件 + private var bluetoothAdapter: BluetoothAdapter? = null + private var bluetoothReceiver: BroadcastReceiver? = null + // 添加标志位跟踪广播接收器注册状态 + private var isBluetoothReceiverRegistered = false + // 添加定期检查蓝牙状态的协程作业 + private var bluetoothCheckJob: kotlinx.coroutines.Job? = null +/** + * BLE伴侣设备管理器 + * 用于管理设备配对和后台扫描 + */ +private var companionManager: BleCompanionManager? = null + /** + * 蓝牙设备连接状态广播接收器 + * 监听蓝牙设备的连接和断开事件 + */ + private fun createBluetoothReceiver(): BroadcastReceiver { + return object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + BluetoothDevice.ACTION_ACL_CONNECTED -> { + val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + device?.let { handleDeviceConnected(it) } + } + BluetoothDevice.ACTION_ACL_DISCONNECTED -> { + val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + device?.let { handleDeviceDisconnected(it) } + } + BluetoothAdapter.ACTION_STATE_CHANGED -> { + val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) + handleBluetoothStateChanged(state) + } + } + } + } + } + + /** + * 处理蓝牙设备连接事件 + * @param device 连接的蓝牙设备 + */ + private fun handleDeviceConnected(device: BluetoothDevice) { + val deviceName = device.name ?: "Unknown" + val deviceAddress = device.address ?: "Unknown" + + Log.i(TAG, "蓝牙设备已连接: $deviceName ($deviceAddress)") + + // 检查是否是目标设备 + if (deviceName.equals(TARGET_DEVICE_NAME, ignoreCase = true)) { + Log.i(TAG, "检测到目标设备 '$TARGET_DEVICE_NAME' 连接!") + Log.i(TAG, "设备信息:") + Log.i(TAG, " - 设备名称: $deviceName") + Log.i(TAG, " - 设备地址: $deviceAddress") + Log.i(TAG, " - 设备类型: ${getDeviceTypeString(device.type)}") + Log.i(TAG, " - 绑定状态: ${getBondStateString(device.bondState)}") + + // 可以在这里添加更多的处理逻辑,比如自动连接BLE服务等 + onTargetDeviceConnected(device) + } + } + + /** + * 处理蓝牙设备断开事件 + * @param device 断开的蓝牙设备 + */ + private fun handleDeviceDisconnected(device: BluetoothDevice) { + val deviceName = device.name ?: "Unknown" + val deviceAddress = device.address ?: "Unknown" + + Log.i(TAG, "蓝牙设备已断开: $deviceName ($deviceAddress)") + + if (deviceName.equals(TARGET_DEVICE_NAME, ignoreCase = true)) { + Log.i(TAG, "目标设备 '$TARGET_DEVICE_NAME' 已断开连接") + onTargetDeviceDisconnected(device) + } + } + + /** + * 处理蓝牙适配器状态变化 + * @param state 蓝牙适配器状态 + */ + private fun handleBluetoothStateChanged(state: Int) { + when (state) { + BluetoothAdapter.STATE_OFF -> { + Log.w(TAG, "蓝牙已关闭") + //companionManager?.unregisterBackgroundScan() + // 蓝牙关闭时,注销设备连接监听器(保留状态监听器) + unregisterDeviceConnectionReceiver() + } + BluetoothAdapter.STATE_ON -> { + Log.i(TAG, "蓝牙已开启") + // 蓝牙开启后,重新注册设备连接监听器 + registerDeviceConnectionReceiver() + // 检查已连接的设备 + checkAlreadyConnectedDevices() + } + BluetoothAdapter.STATE_TURNING_OFF -> { + Log.i(TAG, "蓝牙正在关闭...") + } + BluetoothAdapter.STATE_TURNING_ON -> { + Log.i(TAG, "蓝牙正在开启...") + } + } + } + + /** + * 目标设备连接时的处理逻辑 + * @param device 连接的目标设备 + */ + private fun onTargetDeviceConnected(device: BluetoothDevice) { + // 在这里可以添加目标设备连接后的特殊处理逻辑 + // 比如自动启动某些功能、发送通知等 + Log.i(TAG, "目标设备 '${device.name}' 连接成功,执行相关处理逻辑") + } + + /** + * 目标设备断开时的处理逻辑 + * @param device 断开的目标设备 + */ + private fun onTargetDeviceDisconnected(device: BluetoothDevice) { + // 在这里可以添加目标设备断开后的处理逻辑 + Log.i(TAG, "目标设备 '${device.name}' 断开连接,执行清理逻辑") + } + + /** + * 获取设备类型字符串 + * @param type 设备类型 + * @return 设备类型描述 + */ + private fun getDeviceTypeString(type: Int): String { + return when (type) { + BluetoothDevice.DEVICE_TYPE_CLASSIC -> "经典蓝牙" + BluetoothDevice.DEVICE_TYPE_LE -> "低功耗蓝牙(BLE)" + BluetoothDevice.DEVICE_TYPE_DUAL -> "双模蓝牙" + else -> "未知类型" + } + } + + /** + * 获取绑定状态字符串 + * @param bondState 绑定状态 + * @return 绑定状态描述 + */ + private fun getBondStateString(bondState: Int): String { + return when (bondState) { + BluetoothDevice.BOND_NONE -> "未绑定" + BluetoothDevice.BOND_BONDING -> "正在绑定" + BluetoothDevice.BOND_BONDED -> "已绑定" + else -> "未知状态" + } + } + + /** + * 检查已连接的蓝牙设备 + * 在服务启动或蓝牙开启时调用,检查是否有目标设备已经连接 + */ + private fun checkAlreadyConnectedDevices() { + try { + bluetoothAdapter?.let { adapter -> + if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_CONNECT) + == PackageManager.PERMISSION_GRANTED) { + + val bondedDevices = adapter.bondedDevices + Log.i(TAG, "检查已绑定的设备数量: ${bondedDevices?.size ?: 0}") + + bondedDevices?.forEach { device -> + val deviceName = device.name ?: "Unknown" + Log.d(TAG, "已绑定设备: $deviceName (${device.address})") + + if (deviceName.equals(TARGET_DEVICE_NAME, ignoreCase = true)) { + Log.i(TAG, "发现已绑定的目标设备: $deviceName") + val pairedUuid = companionManager?.getPairedUuid() + val pairedMac = companionManager?.getPairedMacAddress() + companionManager?.registerBackgroundScan(pairedMac, pairedUuid) + // 注意:这里只能检查绑定状态,无法直接判断连接状态 + // 实际连接状态需要通过广播接收器监听 + } + } + } else { + Log.w(TAG, "缺少蓝牙连接权限,无法检查已连接设备") + } + } + } catch (e: Exception) { + Log.e(TAG, "检查已连接设备时发生异常", e) + } + } + + /** + * 初始化蓝牙监听器 + * 设置蓝牙适配器和注册广播接收器 + */ + private fun initializeBluetoothListener() { + try { + // 获取蓝牙管理器和适配器 + val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager + bluetoothAdapter = bluetoothManager?.adapter + + if (bluetoothAdapter == null) { + Log.w(TAG, "设备不支持蓝牙功能") + return + } + + // 创建蓝牙广播接收器 + if (bluetoothReceiver == null) { + bluetoothReceiver = createBluetoothReceiver() + } + + // 首先注册蓝牙状态变化监听器(始终保持注册) + registerBluetoothStateReceiver() + + // 如果蓝牙已开启,注册设备连接监听器 + if (bluetoothAdapter!!.isEnabled) { + Log.i(TAG, "蓝牙已开启,注册设备连接监听器") + registerDeviceConnectionReceiver() + // 检查已连接的设备 + checkAlreadyConnectedDevices() + } else { + Log.w(TAG, "蓝牙未开启,仅注册状态监听器") + } + + } catch (e: Exception) { + Log.e(TAG, "初始化蓝牙监听器时发生异常", e) + } + } + + /** + * 注册蓝牙状态变化监听器 + * 这个监听器始终保持注册状态,用于监听蓝牙开关状态 + */ + private fun registerBluetoothStateReceiver() { + try { + if (!isBluetoothReceiverRegistered && bluetoothReceiver != null) { + val stateFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) + registerReceiver(bluetoothReceiver, stateFilter) + isBluetoothReceiverRegistered = true + Log.i(TAG, "蓝牙状态监听器注册成功") + } + } catch (e: Exception) { + Log.e(TAG, "注册蓝牙状态监听器时发生异常", e) + } + } + + /** + * 注册设备连接监听器 + * 仅在蓝牙开启时注册,用于监听设备连接和断开事件 + */ + private fun registerDeviceConnectionReceiver() { + try { + if (isBluetoothReceiverRegistered && bluetoothReceiver != null) { + // 先注销现有的接收器 + unregisterReceiver(bluetoothReceiver!!) + + // 重新注册包含所有事件的接收器 + val fullFilter = IntentFilter().apply { + addAction(BluetoothDevice.ACTION_ACL_CONNECTED) + addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED) + addAction(BluetoothAdapter.ACTION_STATE_CHANGED) + } + + registerReceiver(bluetoothReceiver, fullFilter) + Log.i(TAG, "设备连接监听器注册成功") + } + } catch (e: Exception) { + Log.e(TAG, "注册设备连接监听器时发生异常", e) + } + } + + /** + * 注销设备连接监听器 + * 蓝牙关闭时调用,但保留状态监听器 + */ + private fun unregisterDeviceConnectionReceiver() { + try { + if (isBluetoothReceiverRegistered && bluetoothReceiver != null) { + // 注销当前接收器 + unregisterReceiver(bluetoothReceiver!!) + + // 重新注册仅包含状态变化的接收器 + val stateFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) + registerReceiver(bluetoothReceiver, stateFilter) + Log.i(TAG, "设备连接监听器已注销,保留状态监听器") + } + } catch (e: Exception) { + Log.e(TAG, "注销设备连接监听器时发生异常", e) + } + } + + /** + * 完全注销蓝牙广播接收器 + * 仅在服务销毁时调用 + */ + private fun unregisterBluetoothReceiver() { + try { + if (isBluetoothReceiverRegistered && bluetoothReceiver != null) { + unregisterReceiver(bluetoothReceiver!!) + isBluetoothReceiverRegistered = false + Log.i(TAG, "蓝牙广播接收器已完全注销") + } + } catch (e: Exception) { + Log.e(TAG, "注销蓝牙广播接收器时发生异常", e) + } + } override fun onCreate() { super.onCreate() @@ -84,10 +400,80 @@ class BleMediaService : Service(), CoroutineScope { // 设置服务状态 isRunning.set(true) startForeground(NOTIFICATION_ID, createNotification()) + // 初始化BLE伴侣设备管理器 - 使用单例模式获取实例 + companionManager = BleCompanionManager.getInstance(this) + // 初始化蓝牙监听器 + initializeBluetoothListener() + + // 启动定期蓝牙状态检查 + startBluetoothStatusCheck() + Log.i(TAG, "BLE代理服务已启动") } - + + /** + * 启动定期蓝牙状态检查 + * 每30秒检查一次蓝牙状态,确保监听器正常工作 + */ + private fun startBluetoothStatusCheck() { + bluetoothCheckJob = launch { + while (isRunning.get()) { + try { + kotlinx.coroutines.delay(30000) // 30秒检查一次 + checkAndRestoreBluetoothListener() + } catch (e: Exception) { + Log.e(TAG, "蓝牙状态检查时发生异常", e) + } + } + } + Log.i(TAG, "蓝牙状态定期检查已启动") + } + + /** + * 检查并恢复蓝牙监听器 + * 确保在蓝牙开启时监听器正常工作 + */ + private fun checkAndRestoreBluetoothListener() { + try { + bluetoothAdapter?.let { adapter -> + val isBluetoothEnabled = adapter.isEnabled + Log.d(TAG, "蓝牙状态检查: 蓝牙${if (isBluetoothEnabled) "已开启" else "已关闭"}, 接收器${if (isBluetoothReceiverRegistered) "已注册" else "未注册"}") + + if (isBluetoothEnabled && isBluetoothReceiverRegistered) { + // 蓝牙开启且接收器已注册,检查是否需要重新注册设备连接监听器 + // 这里可以通过尝试获取当前注册的IntentFilter来判断 + // 但Android没有直接的API,所以我们采用重新注册的方式确保监听器有效 + Log.d(TAG, "重新确认设备连接监听器注册状态") + registerDeviceConnectionReceiver() + } else if (isBluetoothEnabled && !isBluetoothReceiverRegistered) { + // 蓝牙开启但接收器未注册,重新初始化 + Log.w(TAG, "检测到蓝牙已开启但监听器未注册,重新初始化") + initializeBluetoothListener() + } else if (!isBluetoothEnabled && isBluetoothReceiverRegistered) { + // 蓝牙关闭但接收器仍注册,保持状态监听器 + Log.d(TAG, "蓝牙已关闭,确保仅保留状态监听器") + unregisterDeviceConnectionReceiver() + } + } ?: run { + // 蓝牙适配器为空,重新初始化 + Log.w(TAG, "蓝牙适配器为空,重新初始化监听器") + initializeBluetoothListener() + } + } catch (e: Exception) { + Log.e(TAG, "检查和恢复蓝牙监听器时发生异常", e) + } + } + + /** + * 停止蓝牙状态检查 + */ + private fun stopBluetoothStatusCheck() { + bluetoothCheckJob?.cancel() + bluetoothCheckJob = null + Log.i(TAG, "蓝牙状态定期检查已停止") + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.i(TAG, "BleAgentService.onStartCommand: ${intent?.action}") @@ -185,6 +571,8 @@ class BleMediaService : Service(), CoroutineScope { it.getStringExtra(KEY_MUSICSERVICE_TYPE)?.let { value -> config[KEY_MUSICSERVICE_TYPE] = value } it.getStringExtra(APP_NAVIGATION_MODE)?.let { value -> config[APP_NAVIGATION_MODE] = value } it.getStringExtra(APP_CALLING_TOOL)?.let { value -> config[APP_CALLING_TOOL] = value } + it.getStringExtra("isttsinterrupt")?.let { value -> config["isttsinterrupt"] = value } + // 提取 supportedLanguages 参数 it.getStringArrayListExtra(KEY_SUPPORTED_LANGUAGES)?.let { languages -> if (languages.isNotEmpty()) { @@ -272,6 +660,14 @@ class BleMediaService : Service(), CoroutineScope { override fun onDestroy() { Log.i(TAG, "BLE代理服务正在销毁...") + // 停止蓝牙状态检查 + stopBluetoothStatusCheck() + + // 完全注销蓝牙广播接收器 + unregisterBluetoothReceiver() + bluetoothReceiver = null + bluetoothAdapter = null + // 取消所有协程 cancel() @@ -338,4 +734,4 @@ class BleMediaService : Service(), CoroutineScope { return builder.build() } -} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 01976fc8e..a0561af62 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -27,6 +27,10 @@ class AgentServiceImpl: NSObject { static let shared = AgentServiceImpl() + // UserDefaults 相关常量 + private let PREF_NAME = "agent_service_prefs" + private let KEY_IS_INTERRUPT = "is_interrupt" + private var listeners = [AgentServiceListener]() private let listenersLock = NSLock() @@ -76,11 +80,15 @@ class AgentServiceImpl: NSObject { //是否保留最终结果 private var isKeepResult = true; + // 新增:是否在识别过程中被中断 + public var isInterrupt: Bool = true + var currsessionId: String = "" var language: String = "" private var serverurl = "" private var usertoken = "" + private let timerQueue = DispatchQueue(label: "com.example.idleTimer.queue") private var idleTimer: DispatchSourceTimer? private let maxIdleSeconds: TimeInterval = 10 var micCapture: MicrophoneCapture! @@ -174,6 +182,14 @@ class AgentServiceImpl: NSObject { self.callingTool = callingTool } + //加载 isInterrupt 状态 + loadIsInterruptFromCache() + if let isttsinterrupt = config["isttsinterrupt"] as? String { + if (isttsinterrupt == "true"){ + self.isInterrupt = true + } + } + if let serverurl = config["serverurl"] as? String { self.serverurl = serverurl } @@ -248,6 +264,7 @@ class AgentServiceImpl: NSObject { if isInitialized { loadChatHistory() + } LocationService.shared.getCurrentLocation() { location in // self.locationInfo = location @@ -357,41 +374,62 @@ class AgentServiceImpl: NSObject { } private func startIdleCheck(idleSeconds: TimeInterval = 10) { - stopIdleCheck() - - guard isRecognizing else { return } - - os_log("启动空闲检测,超时时间: %.0f秒,模式: %{public}@", log: logger, type: .info, idleSeconds, currentRecognitionMode) - - // 使用全局队列而不是主队列,避免后台挂起问题 - let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility)) - idleTimer = timer - - timer.schedule(deadline: .now() + idleSeconds) - timer.setEventHandler { [weak self] in - // 使用更安全的方式检查self是否存在 - DispatchQueue.main.async { [weak self] in + // 2. 所有对idleTimer的操作都通过串行队列执行,避免竞态条件 + timerQueue.async { + // 先停止旧的定时器(在串行队列中执行,确保线程安全) + self.stopIdleCheck() + + guard self.isRecognizing else { return } + + os_log("启动空闲检测,超时时间: %.0f秒,模式: %{public}@", + log: self.logger, + type: .info, + idleSeconds, + self.currentRecognitionMode) + + // 使用全局队列作为定时器回调队列(避免后台挂起) + let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility)) + + timer.schedule(deadline: .now() + idleSeconds) + timer.setEventHandler { [weak self] in guard let self = self else { return } - - if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming { - os_log("空闲检测触发自动停止,模式: %{public}@", log: self.logger, type: .info, self.currentRecognitionMode) - // 关闭编码器 (设备侧) - BleService.shared.closeCodec() - self.stopRecognition() - self.sendEvent(name: "auto_stop", data: [ - "reason": "idle_timeout", - "seconds": idleSeconds, - "mode": self.currentRecognitionMode - ]) + + // 回调在全局队列,涉及UI或实例变量操作时切换到主线程 + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + // 检查状态(确保在主线程访问这些属性,避免UI相关竞态) + if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming { + os_log("空闲检测触发自动停止,模式: %{public}@", + log: self.logger, + type: .info, + self.currentRecognitionMode) + + // 关闭编码器(设备侧操作,若BleService线程安全可直接调用,否则需加锁) + BleService.shared.closeCodec() + + self.stopRecognition() + self.sendEvent(name: "auto_stop", data: [ + "reason": "idle_timeout", + "seconds": idleSeconds, + "mode": self.currentRecognitionMode + ]) + } } } + + timer.resume() + // 3. 在串行队列中安全赋值 + self.idleTimer = timer } - timer.resume() } private func stopIdleCheck() { - idleTimer?.cancel() - idleTimer = nil + timerQueue.async { + self.idleTimer?.cancel() + self.idleTimer = nil // 清空引用 + } + } private func restartIdleCheck() { @@ -520,11 +558,11 @@ class AgentServiceImpl: NSObject { if !isRecognizing || !isInitialized { return false } - guard let audioStream = azureAsrHelper?.audioStream else { - os_log("AI音频流未初始化", log: logger,type: .error) - return false -} -audioStream.saveAudioDataTo(data: audioData) + guard let audioStream = azureAsrHelper?.audioStream else { + // os_log("AI音频流未初始化", log: logger,type: .error) + return false + } + audioStream.saveAudioDataTo(data: audioData) return true } @@ -654,6 +692,15 @@ audioStream.saveAudioDataTo(data: audioData) messages.append(userMessage) os_log("添加用户消息", log: logger, type: .info) + // 保存用户消息 + self.chatStorageHelper.saveMessage( + agentId: self.agentId, + sessionId: sessionid, + message: displayText, + sender: "user", + metadata:"" + ) + addToHistoryMessages(chatApiService.createUserMessage(content: displayText)) currsessionId = sessionid let callback = ChatApiStreamCallback( @@ -778,16 +825,9 @@ audioStream.saveAudioDataTo(data: audioData) */ internal func saveChatMessage(sessionId:String,userMessage: String, assistantMessage: String, metadata: String = "",userMetadata: String) { DispatchQueue.global(qos: .utility).async { - // 保存用户消息 - let userMessageId = self.chatStorageHelper.saveMessage( - agentId: self.agentId, - sessionId: sessionId, - message: userMessage, - sender: "user", - metadata: userMetadata - ) + - if userMessageId != -1 { +// if userMessageId != -1 { if !assistantMessage.isEmpty || !metadata.isEmpty { // 保存AI回复 let assistantMessageId = self.chatStorageHelper.saveMessage( @@ -801,9 +841,9 @@ audioStream.saveAudioDataTo(data: audioData) os_log("保存助手消息失败", log: self.logger, type: .error) } } - } else { - os_log("保存用户消息失败", log: self.logger, type: .error) - } +// } else { +// os_log("保存用户消息失败", log: self.logger, type: .error) +// } } } @@ -814,6 +854,7 @@ internal func autoHandleFunctionCallResult(toolName: String, functionCallResult: guard let contextValue = functionCallResult["context"] else { return [ "meta": "", + "iscard":false, "broadcast": true, "notreply": false, ] @@ -881,9 +922,9 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) - metadata["isinternal"] = isinternal } else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" { processQQMusicPlayList(cardQQMusiclist) - } else if toolName == "maps_weather" { + } else if toolName == "map_weather" { iscard = true - }else if toolName == "map_weather" { + }else if toolName == "maps_weather" { iscard = true } metadata["iscard"] = iscard @@ -897,6 +938,7 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) - return [ "meta": metaStr, + "iscard":iscard, "broadcast": broadcast, "notreply": notreply ] @@ -1670,7 +1712,10 @@ class ChatApiStreamCallback: StreamCallback { // 停止等待音效 agentService.audioPlayer?.stopAwaitSound() } - + os_log("onComplete agentService.isInterrupt:%{public}@", log: agentService.logger, type: .info, String(agentService.isInterrupt)) + if(agentService.isInterrupt){ + BleService.shared.closeCodec() + } if speakResponse && reply && broadcast && sessionid == agentService.currsessionId{ agentService.azureTtsHelper?.flushStream(sessionid:sessionid) @@ -1749,10 +1794,10 @@ class ChatApiStreamCallback: StreamCallback { print("onFunctionCall=functionCall\(functionCall)") if (name == "exit_interaction") { - // 关闭编码器 (设备侧) - BleService.shared.closeCodec() - agentService.stopRecognition() - } + // 关闭编码器 (设备侧) + BleService.shared.closeCodec() + agentService.stopRecognition() + } try agentService.sendEvent(name: "function_call", data: [ "sessionid":sessionid, @@ -1782,11 +1827,15 @@ class ChatApiStreamCallback: StreamCallback { // agentService.audioPlayer?.stopCallingSound() let resultmap = agentService.autoHandleFunctionCallResult(toolName:name,functionCallResult:functionCallResult) - metadata = resultmap["meta"] as? String ?? "" broadcast = resultmap["broadcast"] as? Bool ?? true reply = !(resultmap["notreply"] as? Bool ?? true) - iscard = !(resultmap["iscard"] as? Bool ?? false) + iscard = resultmap["iscard"] as? Bool ?? false + os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{iscard}@", + log: agentService.logger, + type: .info, + "\(broadcast)", "\(reply)", "\(iscard)") if (iscard){ + metadata = resultmap["meta"] as? String ?? "" agentService.sendEvent(name: "function_call_result", data: [ "sessionid":sessionid, "function_name": name, @@ -1794,10 +1843,13 @@ class ChatApiStreamCallback: StreamCallback { "result": functionCallResult, "meta":metadata, ]) - os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{metadata}@", - log: agentService.logger, - type: .info, - "\(broadcast)", "\(reply)", "\(metadata)") + }else{ + agentService.sendEvent(name: "function_call_result", data: [ + "sessionid":sessionid, + "function_name": name, + "function_call": functionCall, + "result": functionCallResult, + ]) } }catch{ @@ -1825,7 +1877,7 @@ extension AgentServiceImpl: TtsEventListener { restartIdleCheck() sendEvent(name: "tts_started", data: ["status": "started"]) audioPlayer?.stopAwaitSound() - + case .synthesisCompleted: isSpeaking = false restartIdleCheck() @@ -1835,8 +1887,14 @@ extension AgentServiceImpl: TtsEventListener { restartIdleCheck() sendEvent(name: "playback_started", data: ["status": "playback_started"]) MusicService.shared.reduceVolume() + if(isInterrupt){ //播报开始。打断唤醒输入 + BleService.shared.closeCodec() + } case .playbackCompleted: restartIdleCheck() + if(isInterrupt){//播报结束。打开解码器 + BleService.shared.openB1Encoder() + } sendEvent(name: "playback_completed", data: ["status": "playback_completed"]) MusicService.shared.restoreVolume() case .synthesisCanceled: @@ -2147,7 +2205,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { isStartingRecognition = false // 使用模式感知的空闲检测启动方法 startIdleCheckForMode(mode: currentRecognitionMode) - if (currentRecognitionMode == "ble_wakeup") { + if (currentRecognitionMode == "ble_wakeup") { audioPlayer?.playStartSound() } @@ -2196,4 +2254,25 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { stopRequestedDuringStart = false stopIdleCheck() } + + // MARK: - isInterrupt 缓存方法 + + /** + * 从 UserDefaults 加载 isInterrupt 状态 + */ + private func loadIsInterruptFromCache() { + let userDefaults = UserDefaults.standard + let cachedValue = userDefaults.bool(forKey: KEY_IS_INTERRUPT) // 默认值为 false + isInterrupt = cachedValue + os_log("从缓存加载 isInterrupt 状态: %{public}@", log: logger, type: .info, String(isInterrupt)) + } + + /** + * 保存 isInterrupt 状态到 UserDefaults + */ + func saveIsInterruptToCache(_ isInterrupt: Bool) { + let userDefaults = UserDefaults.standard + userDefaults.set(isInterrupt, forKey: KEY_IS_INTERRUPT) + os_log("保存 isInterrupt 状态到缓存: %{public}@", log: logger, type: .info, String(isInterrupt)) + } } diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift index 08864a87f..0877ab63e 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift @@ -78,7 +78,17 @@ public class AgentServicePlugin: NSObject, FlutterPlugin { let mode = (call.arguments as? [String: Any])?["mode"] as? String ?? "normal" impl.startRecognition(mode: mode) result(true) - + case "isInterrupt": + if let args = call.arguments as? [String: Any], + let isInterrupt = args["isinterrupt"] as? Bool { + impl.isInterrupt = isInterrupt + // 保存到 UserDefaults + impl.saveIsInterruptToCache(isInterrupt) + result(impl.isInterrupt) + } else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "isinterrupt 参数缺失或类型错误", details: nil)) + } + case "recognizeCallback": impl.recognizeCallback() result(true) diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index 502c4c9f1..689a17c6c 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -266,6 +266,17 @@ class AgentService { } } + static Future isInterrupt(bool isInterrupt) async { + try { + final bool result = await _channel.invokeMethod('isInterrupt', { + 'isinterrupt': isInterrupt, + }); + return result; + } on PlatformException catch (e) { + throw AgentServiceException(e.code, e.message ?? '检查是否打断失败', e.details); + } + } + /// 设备识别器 /// /// 启动语音识别,开始监听用户语音输入 diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 7d43d01c3..2f0989c62 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -347,6 +347,7 @@ class AzureAsrHelper(private val context: Context) { currentLanguage } callback.onResult(currsessionid,event.result.text, detectedLanguage) + currsessionid = UUID.randomUUID().toString() } } ) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index 09ac0dd40..261825b98 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -671,9 +671,9 @@ class AzureTtsHelper(private val context: Context) : ITtsService { try { synthesizer?.StopSpeakingAsync() - synthesizer?.close() streamBuffer.clear() isSpeaking = false + recreateSynthesizer() return true } catch (e: Exception) { FileLogger.e(TAG, "停止语音合成失败: ${e.message}") diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index ca31912cf..8fe11e856 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -801,6 +801,7 @@ public class AzureAsrHelper: NSObject { // 在主线程回调结果 DispatchQueue.main.async { callback.onResult(sessionid: self.currsessionId,result.text ?? "", detectedLanguage) + self.currsessionId = UUID().uuidString } } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index 6bf1caab1..f63488d10 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -376,7 +376,7 @@ private func sendAstEvent(_ event: [String: Any]) { // 安全解包版本 guard let audioStream = azureAsrHelper.audioStream else { - os_log("音频流未初始化", type: .error) + // os_log("音频流未初始化", type: .error) return } print("liwei----------外部音频推送:\(audioBytes.data.count)") @@ -986,7 +986,7 @@ extension AzureSpeechPlugin: BleService.Callback { } // 安全解包版本 guard let audioStream = azureAsrHelper.audioStream else { - os_log("音频流未初始化", type: .error) + // os_log("音频流未初始化", type: .error) return } azureAsrHelper.audioStream?.saveAudioDataTo(data: data) @@ -1030,7 +1030,7 @@ extension AzureSpeechPlugin: BleService.Callback { // 安全解包版本 guard let audioStream = azureAsrHelper.audioStream else { - os_log("音频流未初始化", type: .error) + // os_log("音频流未初始化", type: .error) return } azureAsrHelper.audioStream?.saveAudioDataTo(data: rightBuffer) diff --git a/local_plugins/ble_service/android/libs/jl_audio_decode_V2.0.0_20005bat.aar b/local_plugins/ble_service/android/libs/jl_audio_decode_V2.0.0_20005bat.aar new file mode 100644 index 000000000..0ec76c882 Binary files /dev/null and b/local_plugins/ble_service/android/libs/jl_audio_decode_V2.0.0_20005bat.aar differ diff --git a/local_plugins/ble_service/android/libs/jl_audio_decode_V2.1.0_20013-release.aar b/local_plugins/ble_service/android/libs/jl_audio_decode_V2.1.0_20013-release.aar new file mode 100644 index 000000000..84091918b Binary files /dev/null and b/local_plugins/ble_service/android/libs/jl_audio_decode_V2.1.0_20013-release.aar differ diff --git a/local_plugins/ble_service/android/src/main/AndroidManifest.xml b/local_plugins/ble_service/android/src/main/AndroidManifest.xml index f5763d6e5..186a87c7d 100644 --- a/local_plugins/ble_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/ble_service/android/src/main/AndroidManifest.xml @@ -12,6 +12,9 @@ + + + @@ -47,6 +50,18 @@ + + + + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt new file mode 100644 index 000000000..44bb4f630 --- /dev/null +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt @@ -0,0 +1,700 @@ +package com.yunqiinnovation.ble_service + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCharacteristic +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.annotation.RequiresPermission +import java.util.* + +/** + * BLE指令发送器类:负责处理蓝牙设备的指令发送、接收和队列管理 + * 功能包括: + * 1. 指令队列管理 + * 2. 指令封装和CRC校验 + * 3. 指令发送和重试机制 + * 4. 响应超时处理 + * 5. 设备响应和通知处理 + */ +class BleCommandSender { + + companion object { + private const val TAG = "BleCommandSender" + private const val REPLY_TIMEOUT_MS = 1000L // 响应超时时间1秒 + } + + // ====================================================================================================== + // 成员变量 + // ====================================================================================================== + + /** 指令队列 */ + private val commandQueue: Queue = LinkedList() + + /** 是否正在等待回复 */ + var isWaitingReply = true + private set + + /** 回复的指令类型 */ + var replyCommandType = 0xff + private set + + /** 响应超时处理器 */ + private val replyTimeoutHandler = Handler(Looper.getMainLooper()) + + /** 响应超时回调 */ + private val replyTimeoutRunnable = Runnable { + if (!isWaitingReply) { + Log.w(TAG, "命令响应超时,自动重置 isWaitingReply 为 true") + isWaitingReply = true + processNextCommand() + } + } + + /** 指令发送状态回调接口 */ + interface CommandSenderCallback { + /** + * 检查连接状态 + * @return 是否已连接 + */ + fun isConnected(): Boolean + + /** + * 获取写入特征 + * @return 写入特征对象 + */ + fun getWriteCharacteristic(): BluetoothGattCharacteristic? + + /** + * 获取GATT连接 + * @return GATT连接对象 + */ + fun getBluetoothGatt(): BluetoothGatt? + + /** + * 通知设备信息接收 + * @param infoType 信息类型 + * @param infoData 信息数据 + */ + fun notifyDeviceInfoReceived(infoType: Int, infoData: Map) + + /** + * 通知唤醒信号接收 + */ + fun notifyWakeupSignalReceived() + + /** + * 获取录音文件对象 + * @return 录音文件对象 + */ + fun getRecordFile(): RecordingFile? + + /** + * 获取重新压缩录音文件对象 + * @return 重新压缩录音文件对象 + */ + fun getRecordFile1(): RecordingFile? + } + + private var callback: CommandSenderCallback? = null + + // ====================================================================================================== + // 公共方法 + // ====================================================================================================== + + /** + * 设置回调接口 + * @param callback 回调接口实现 + */ + fun setCallback(callback: CommandSenderCallback) { + this.callback = callback + } + + /** + * 发送命令到设备 - 通用方法 + * @param cmdType 命令类型 + * @param cmdData 命令数据 + * @return 是否成功发送 + */ + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { + val callback = this.callback ?: run { + Log.e(TAG, "发送命令失败: 回调接口未设置") + return false + } + + if (!callback.isConnected()) { + Log.e(TAG, "发送命令失败: 设备未连接") + return false + } + + try { + // 创建命令包 + val cmdPacket = createCommandPacket(cmdType, cmdData) + + // 记录命令详情 + logCommandDetails(cmdType, cmdData, cmdPacket) + + // 获取写入特征 + val writeChar = callback.getWriteCharacteristic() ?: run { + Log.e(TAG, "发送命令失败: 写入特征未找到") + return false + } + + // 设置数据并发送 + writeChar.value = cmdPacket + + return if (isWaitingReply) { + // 直接发送 + val gatt = callback.getBluetoothGatt() + val isSuccess = gatt?.writeCharacteristic(writeChar) == true + if (isSuccess) { + isWaitingReply = false + startReplyTimeout() + true + } else { + Log.e(TAG, "发送命令失败: 写入特征失败") + false + } + } else { + // 加入队列等待发送 + commandQueue.offer(cmdPacket) + Log.d(TAG, "命令已加入队列,当前队列长度: ${commandQueue.size}") + true + } + + } catch (e: Exception) { + Log.e(TAG, "发送命令异常: ${e.message}", e) + return false + } + } + + /** + * 处理命令响应,重置等待状态并处理下一个命令 + * @param responseCommandType 响应的命令类型 + */ + fun onCommandResponse(responseCommandType: Int) { + isWaitingReply = true + replyCommandType = responseCommandType + replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable) + processNextCommand() + } + + /** + * 处理设备响应数据 - 从BleService移动过来 + * @param data 响应数据 + */ + @SuppressLint("StaticFieldLeak") + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + fun processDeviceResponse(data: ByteArray) { + Log.d( + TAG, + "收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" + ) + if (data.size < 3) { + Log.e(TAG, "设备响应数据格式错误:数据长度过短") + return + } + + val header = data[0] // 0xBB + val cmdType = data[1].toInt() and 0xFF + + // 判断命令失败响应(长度为0) + if (data.size == 4 && data[2].toInt() and 0xFF == 0) { + Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") + // 创建失败结果Map + val resultMap = mapOf( + "success" to false, + "command" to cmdType, + "rawData" to byteArrayOf() + ) + callback?.notifyDeviceInfoReceived(cmdType, resultMap) + // 通知指令发送器处理响应 + onCommandResponse(cmdType) + return + } + + // 正常响应处理 + val length = data[2].toInt() and 0xFF + + // 检查数据长度 + if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC + Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") + return + } + + // 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 + val payload = data.sliceArray(0 until data.size - 1) + val calculatedCrc = calculateCrc8Maxim(payload, payload.size) + val receivedCrc = data[data.size - 1].toInt() and 0xFF + + if (calculatedCrc.toInt() and 0xFF != receivedCrc) { + Log.e( + TAG, + "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" + ) + return + } + + // 提取响应数据 + val responseData = if (length > 0) ByteArray(length) else ByteArray(0) + if (length > 0) { + System.arraycopy(data, 3, responseData, 0, length) + } + + Log.d( + TAG, + "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ + responseData.joinToString(", ") { + "0x${ + (it.toInt() and 0xFF).toString(16) + }" + } + }" + ) + // 通知指令发送器处理响应 + onCommandResponse(cmdType) + // 根据命令类型处理不同的响应 + when (cmdType) { + BleConst.CMD_GET_VERSION -> { + val resultMap = mutableMapOf( + "success" to true, + "command" to "versionInfo", + "rawData" to responseData + ) + + if (responseData.isNotEmpty()) { + // 转换为字符串前,确认是否是有效的ASCII字符 + val version = + responseData + .map { it.toInt() and 0xFF } // 转为无符号整数 (0-255) + .dropWhile { it == 0xFF } // 仅丢弃开头的连续 255 + .joinToString(".") { it.toString() } + resultMap["version"] = version + Log.i(TAG, "接收到目标版本号: $version") + } + + callback?.notifyDeviceInfoReceived(cmdType, resultMap) + } + + BleConst.CMD_GET_PRODUCT_ID -> { + val resultMap = mutableMapOf( + "success" to true, + "command" to "productId", + "rawData" to responseData + ) + + if (responseData.isNotEmpty()) { + // 转换为字符串前,确认是否是有效的ASCII字符 + val productId = + responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } + resultMap["productId"] = productId + Log.i(TAG, "接收到产品ID: $productId") + } + + callback?.notifyDeviceInfoReceived(cmdType, resultMap) + } + + BleConst.CMD_GET_COLOR_ID -> { + val resultMap = mutableMapOf( + "success" to true, + "command" to "colorInfo", + "rawData" to responseData + ) + + if (responseData.isNotEmpty()) { + val colorId = (responseData[0].toInt() and 0xFF).toString() + resultMap["colorId"] = colorId + Log.i(TAG, "接收到耳机颜色ID: $colorId") + } + + callback?.notifyDeviceInfoReceived(cmdType, resultMap) + } + + BleConst.CMD_GET_BATTERY_INFO -> { + val resultMap = mutableMapOf( + "success" to true, + "command" to "batteryInfo", + "rawData" to responseData + ) + + if (responseData.isNotEmpty() && responseData.size >= 3) { + // 处理电池信息 - 左耳、右耳、充电仓 + val leftEarBattery = responseData[0].toInt() and 0x7F + val leftEarCharging = (responseData[0].toInt() and 0x80) != 0 + + val rightEarBattery = + if (responseData.size > 1) responseData[1].toInt() and 0x7F else 0 + val rightEarCharging = + if (responseData.size > 1) (responseData[1].toInt() and 0x80) != 0 else false + + val caseBattery = + if (responseData.size > 2) responseData[2].toInt() and 0x7F else 0 + val caseCharging = + if (responseData.size > 2) (responseData[2].toInt() and 0x80) != 0 else false + + // 添加到结果Map + resultMap["leftEarBattery"] = leftEarBattery + resultMap["leftEarCharging"] = leftEarCharging + resultMap["rightEarBattery"] = rightEarBattery + resultMap["rightEarCharging"] = rightEarCharging + resultMap["caseBattery"] = caseBattery + resultMap["caseCharging"] = caseCharging + resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 + + Log.i( + TAG, "接收到电量信息: 左耳=${leftEarBattery}% " + + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + + "(${if (caseCharging) "充电中" else "未充电"})" + ) + } + + callback?.notifyDeviceInfoReceived(cmdType, resultMap) + } + + BleConst.CMD_CONTROL_CODEC -> { + val resultMap = mutableMapOf( + "success" to true, + "command" to cmdType, + "rawData" to responseData + ) + + if (responseData.isNotEmpty() && responseData.size >= 2) { + val codecStatus = responseData[0].toInt() and 0xFF + val channelMode = responseData[1].toInt() and 0xFF + + // 解码状态描述 + val statusDesc = when (codecStatus) { + BleConst.CODEC_CONTROL_CLOSE -> "已关闭编解码" + BleConst.CODEC_CONTROL_DECODE_ON -> "已打开解码" + BleConst.CODEC_CONTROL_A2DP_PLAY -> "A2DP播放模式" + BleConst.CODEC_CONTROL_CALL_RECORD_PLAY -> "通话记录播放模式" + BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码" + else -> "未知状态($codecStatus)" + } + + // 处理录音文件 + if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON || + codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY || + codecStatus == BleConst.CODEC_CONTROL_CALL_RECORD_PLAY || + codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON + ) { + callback?.getRecordFile1()?.closeFile() + callback?.getRecordFile1()?.creatingFiles() + callback?.getRecordFile()?.closeFile() + callback?.getRecordFile()?.creatingFiles() + } else if (codecStatus == BleConst.CODEC_CONTROL_CLOSE) { + callback?.getRecordFile()?.closeFile() + callback?.getRecordFile1()?.closeFile() + } + + Log.d(TAG, "收到设备响应,channelMode: $channelMode") + + // 声道模式描述 + val channelDesc = when (channelMode) { + BleConst.AUDIO_CHANNEL_LEFT -> "左声道" + BleConst.AUDIO_CHANNEL_RIGHT -> "右声道" + BleConst.AUDIO_CHANNEL_STEREO -> "立体声" + else -> "未知声道($channelMode)" + } + + resultMap["codecStatus"] = codecStatus + resultMap["channelMode"] = channelMode + resultMap["statusDesc"] = statusDesc + resultMap["channelDesc"] = channelDesc + + Log.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") + } else if (responseData.isNotEmpty() && responseData.size == 1) { + val codecStatus = responseData[0].toInt() and 0xFF + if (codecStatus == 0x00) { + Log.d(TAG, "收到设备响应:失败,codecStatus: $codecStatus") + val resultMap = mutableMapOf( + "success" to true, + "command" to "Error", + "rawData" to responseData + ) + } + } + + callback?.notifyDeviceInfoReceived(cmdType, resultMap) + } + + else -> { + // 对于未知命令,返回原始数据 + val resultMap = mapOf( + "success" to true, + "command" to cmdType, + "rawData" to responseData, + "isUnknown" to true + ) + Log.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}") + callback?.notifyDeviceInfoReceived(cmdType, resultMap) + } + } + } + + /** + * 处理设备主动上报数据 - 帧头为0xCC + * 将可见性修改为internal,允许同包内的BleService类访问 + */ + fun processDeviceNotification(data: ByteArray) { + Log.d( + TAG, + "收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" + ) + if (data.size < 3) { + Log.e(TAG, "设备主动上报数据格式错误:数据长度过短") + return + } + + val header = data[0] // 0xCC + val infoType = data[1].toInt() and 0xFF + val length = data[2].toInt() and 0xFF + + // 检查数据长度 + if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC + Log.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") + return + } + + // 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 + val payload = data.sliceArray(0 until data.size - 1) + val calculatedCrc = calculateCrc8Maxim(payload, payload.size) + val receivedCrc = data[data.size - 1].toInt() and 0xFF + + if (calculatedCrc.toInt() and 0xFF != receivedCrc) { + Log.e( + TAG, + "设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" + ) + return + } + + // 提取上报数据 + val notifyData = if (length > 0) ByteArray(length) else ByteArray(0) + if (length > 0) { + System.arraycopy(data, 3, notifyData, 0, length) + } + + Log.d( + TAG, + "收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${ + notifyData.joinToString(", ") { + "0x${ + (it.toInt() and 0xFF).toString(16) + }" + } + }" + ) + + // 根据上报类型处理 + when (infoType) { + BleConst.CMD_GET_BATTERY_INFO -> { // 电量信息 + val resultMap = mutableMapOf( + "success" to true, + "command" to infoType, + "rawData" to notifyData, + "isNotification" to true // 标记为主动上报 + ) + + if (notifyData.isNotEmpty() && notifyData.size >= 3) { + // 处理电池信息 - 左耳、右耳、充电仓 + val leftEarBattery = notifyData[0].toInt() and 0x7F + val leftEarCharging = (notifyData[0].toInt() and 0x80) != 0 + + val rightEarBattery = + if (notifyData.size > 1) notifyData[1].toInt() and 0x7F else 0 + val rightEarCharging = + if (notifyData.size > 1) (notifyData[1].toInt() and 0x80) != 0 else false + + val caseBattery = if (notifyData.size > 2) notifyData[2].toInt() and 0x7F else 0 + val caseCharging = + if (notifyData.size > 2) (notifyData[2].toInt() and 0x80) != 0 else false + + + // 添加到结果Map + resultMap["leftEarBattery"] = leftEarBattery + resultMap["leftEarCharging"] = leftEarCharging + resultMap["rightEarBattery"] = rightEarBattery + resultMap["rightEarCharging"] = rightEarCharging + resultMap["caseBattery"] = caseBattery + resultMap["caseCharging"] = caseCharging + resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 + + Log.i( + TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " + + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + + "(${if (caseCharging) "充电中" else "未充电"})" + ) + } + + callback?.notifyDeviceInfoReceived(infoType, resultMap) + } + + BleConst.CMD_WAKE_UP -> { + Log.i(TAG, "收到唤醒指令") + // 接收到唤醒信号,打开编码器 (设备侧) + + callback?.notifyWakeupSignalReceived() + + } + // 可以在这里添加其他主动上报类型的处理 + else -> { + // 对于未知上报类型,返回原始数据 + val resultMap = mapOf( + "success" to true, + "command" to infoType, + "rawData" to notifyData, + "isUnknown" to true, + "isNotification" to true // 标记为主动上报 + ) + Log.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}") + callback?.notifyDeviceInfoReceived(infoType, resultMap) + } + } + } + + + /** + * 清空命令队列 + */ + fun clearCommandQueue() { + commandQueue.clear() + Log.d(TAG, "命令队列已清空") + } + + /** + * 获取当前队列长度 + * @return 队列中待发送的命令数量 + */ + fun getQueueSize(): Int = commandQueue.size + + /** + * 重置发送状态 + */ + fun reset() { + isWaitingReply = true + replyCommandType = 0xff + replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable) + clearCommandQueue() + Log.d(TAG, "指令发送器已重置") + } + + // ====================================================================================================== + // 私有方法 + // ====================================================================================================== + + /** + * 创建命令包 + * @param cmdType 命令类型 + * @param cmdData 命令数据 + * @return 完整的命令包(包含帧头、命令、长度、数据、CRC) + */ + private fun createCommandPacket(cmdType: Byte, cmdData: ByteArray): ByteArray { + val cmdLength = cmdData.size + val cmdPacket = ByteArray(4 + cmdLength) // 帧头 + 命令 + 长度 + 数据 + CRC + + // 帧头 + cmdPacket[0] = BleConst.APP_REQUEST_HEADER + // 命令类型 + cmdPacket[1] = cmdType + // 命令长度 + cmdPacket[2] = cmdLength.toByte() + + // 填充命令数据(如果有) + if (cmdLength > 0) { + System.arraycopy(cmdData, 0, cmdPacket, 3, cmdLength) + } + + // 计算CRC - 对帧头到数据的所有字节进行校验 (不包括CRC自身) + val crcPayload = cmdPacket.sliceArray(0 until cmdPacket.size - 1) + val crc = calculateCrc8Maxim(crcPayload, crcPayload.size) + + // 设置CRC + cmdPacket[3 + cmdLength] = crc + + return cmdPacket + } + + /** + * 记录命令详情 + * @param cmdType 命令类型 + * @param cmdData 命令数据 + * @param cmdPacket 完整命令包 + */ + private fun logCommandDetails(cmdType: Byte, cmdData: ByteArray, cmdPacket: ByteArray) { + val crc = cmdPacket[cmdPacket.size - 1] + Log.d( + TAG, + "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=${cmdData.size}, " + + "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + + "CRC=0x${(crc.toInt() and 0xFF).toString(16)}" + ) + } + + /** + * 处理队列中的下一个命令 + */ + private fun processNextCommand() { + if (commandQueue.isEmpty()) { + return + } + + val callback = this.callback ?: return + val nextCmd = commandQueue.poll() ?: return + + val writeChar = callback.getWriteCharacteristic() ?: run { + Log.e(TAG, "处理下一个命令失败: 写入特征未找到") + return + } + + val gatt = callback.getBluetoothGatt() ?: run { + Log.e(TAG, "处理下一个命令失败: GATT连接未找到") + return + } + + writeChar.value = nextCmd + val isSuccess = gatt.writeCharacteristic(writeChar) + if (isSuccess) { + isWaitingReply = false + startReplyTimeout() + Log.d(TAG, "发送队列中的下一个命令,剩余队列长度: ${commandQueue.size}") + } else { + Log.e(TAG, "发送队列中的命令失败") + } + } + + /** + * 启动响应超时计时器 + */ + private fun startReplyTimeout() { + replyTimeoutHandler.postDelayed(replyTimeoutRunnable, REPLY_TIMEOUT_MS) + } + + /** + * 计算CRC8/MAXIM校验值 + * @param data 要校验的数据 + * @param length 数据长度 + * @return CRC8校验值 + */ + private fun calculateCrc8Maxim(data: ByteArray, length: Int): Byte { + var crc = 0 + val polynomial = 0x31 + + for (i in 0 until length) { + crc = crc xor (data[i].toInt() and 0xFF) + for (j in 0 until 8) { + if (crc and 0x80 != 0) { + crc = ((crc shl 1) xor polynomial) and 0xFF + } else { + crc = (crc shl 1) and 0xFF + } + } + } + return crc.toByte() + } +} \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt index 52f7e2acd..e02fe6f52 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt @@ -22,10 +22,12 @@ import java.util.UUID import java.util.regex.Pattern /** - * BleCompanionManager + * BleCompanionManager - 单例模式 * 用于管理BLE设备的配对和关联 + * + * 使用单例模式确保全局只有一个实例,避免重复初始化和资源浪费 */ -class BleCompanionManager(private val context: Context) { +class BleCompanionManager private constructor(private val context: Context) { private val TAG = "BleCompanionManager" // 常量定义 @@ -34,6 +36,44 @@ class BleCompanionManager(private val context: Context) { private const val PREF_NAME = "ble_companion_prefs" private const val KEY_ASSOCIATION_IDS = "association_ids" private const val KEY_PAIRED_MAC = "paired_mac" + private const val KEY_PAIRED_UUID = "paired_uuid" + + // 单例实例 + @Volatile + private var INSTANCE: BleCompanionManager? = null + + /** + * 获取BleCompanionManager单例实例 + * 使用双重检查锁定模式确保线程安全 + * + * @param context 应用上下文,建议使用ApplicationContext + * @return BleCompanionManager实例 + */ + fun getInstance(context: Context): BleCompanionManager { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: BleCompanionManager(context.applicationContext).also { INSTANCE = it } + } + } + + /** + * 获取已初始化的实例(如果存在) + * + * @return BleCompanionManager实例,如果未初始化则返回null + */ + fun getInstanceIfInitialized(): BleCompanionManager? { + return INSTANCE + } + + /** + * 销毁单例实例 + * 主要用于测试或特殊情况下的清理 + */ + fun destroyInstance() { + synchronized(this) { + INSTANCE?.cleanup() + INSTANCE = null + } + } } // 会话保存 @@ -44,6 +84,36 @@ class BleCompanionManager(private val context: Context) { // 用于验证的临时MAC地址 private var pendingMacAddress: String? = null + // 创建PendingIntent + private val pendingIntent by lazy { + val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) + .setClass(context, BleWakeReceiver::class.java) + + PendingIntent.getBroadcast( + context, + 0, + scanIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + ) + } + + /** + * 清理资源 + * 在销毁实例时调用 + */ + private fun cleanup() { + try { + unregisterBackgroundScan() + pendingMacAddress = null + } catch (e: Exception) { + Log.e(TAG, "清理资源时发生错误: ${e.message}", e) + } + } + /** * 获取CompanionDeviceManager服务(使用Activity上下文) */ @@ -63,313 +133,344 @@ class BleCompanionManager(private val context: Context) { } /** - * 获取已关联的设备MAC地址 + * 获取已连接的设备MAC地址 */ fun getPairedMacAddress(): String? { return sharedPreferences.getString(KEY_PAIRED_MAC, null) } - - /** - * 保存已关联的设备MAC地址 + /** + * 保存连接的设备UUID */ - fun savePairedMacAddress(macAddress: String) { - sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply() - Log.i(TAG, "已保存配对设备MAC: $macAddress") - } - - /** - * 清除已关联的设备MAC地址 - */ - fun clearPairedMacAddress() { - sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply() - Log.i(TAG, "已清除配对设备MAC") - } - /** - * 检查是否有已关联的设备 + * 保存连接的设备UUID列表 + * 将UUID列表转换为JSON字符串进行存储 */ - @RequiresApi(Build.VERSION_CODES.O) - fun hasAssociations(): Boolean { - val associationIds = getAssociationIds() - return associationIds.isNotEmpty() - } - - /** - * 获取所有关联ID - */ - @RequiresApi(Build.VERSION_CODES.O) - fun getAssociationIds(): List { - // 使用默认上下文获取CompanionDeviceManager - val companionDeviceManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager - } else { - null + fun savePairedUuid(uuids: List) { + if (uuids.isNotEmpty()) { + // 将列表转换为JSON字符串格式进行存储 + val uuidsJson = uuids.joinToString(",") { "\"$it\"" } + val jsonString = "[$uuidsJson]" + sharedPreferences.edit().putString(KEY_PAIRED_UUID, jsonString).apply() + Log.i(TAG, "已保存连接设备UUID列表: $uuids") } - return getAssociationIds(companionDeviceManager) } - + /** - * 获取所有关联ID(使用指定的CompanionDeviceManager) + * 获取已连接的设备UUID列表 + * 从JSON字符串解析为UUID列表 */ - @RequiresApi(Build.VERSION_CODES.O) - private fun getAssociationIds(companionDeviceManager: CompanionDeviceManager?): List { - if (companionDeviceManager == null) { - return emptyList() - } - - // 注意: allAssociations 仅在API 33 (Android 13)及以上版本可用 - // 在早期版本中,使用associations (返回的是String数组) - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + fun getPairedUuid(): List { + val jsonString = sharedPreferences.getString(KEY_PAIRED_UUID, null) + return if (!jsonString.isNullOrEmpty()) { try { - // 由于AssociationInfo类在API 30+引入,使用反射获取更安全 - val method = CompanionDeviceManager::class.java.getMethod("getAllAssociations") - @Suppress("UNCHECKED_CAST") - val associations = method.invoke(companionDeviceManager) as? List - associations?.map { association -> - // 尝试获取AssociationInfo.id - val idField = association.javaClass.getField("id") - idField.get(association).toString() - } ?: emptyList() + // 简单的JSON解析,移除方括号和引号,然后分割 + jsonString.trim('[', ']') + .split(",") + .map { it.trim().trim('"') } + .filter { it.isNotEmpty() } } catch (e: Exception) { - Log.e(TAG, "获取关联ID失败: ${e.message}", e) + Log.e(TAG, "解析UUID列表失败: ${e.message}") emptyList() } } else { - // API 26-32 使用 associations 方法 - companionDeviceManager.associations?.toList() ?: emptyList() + emptyList() } } + + + /** + * 保存连接的设备MAC地址 + */ + fun savePairedMacAddress(macAddress: String) { + sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply() + Log.i(TAG, "已保存连接设备MAC: $macAddress") + } /** - * 开始设备关联流程 - * - * @param activity 当前Activity,用于启动系统关联对话框 - * @param deviceNamePattern 设备名称正则表达式,例如"MyHeadset.*" - * @param serviceUuid 可选的服务UUID - * @param macAddress 可选的MAC地址 + * 清除已连接的设备MAC地址 */ - @RequiresApi(Build.VERSION_CODES.O) - fun startAssociation( - activity: Activity, - deviceNamePattern: String? = null, - serviceUuid: String? = null, - macAddress: String? = null - ): Boolean { - // 使用Activity上下文获取CompanionDeviceManager - val companionDeviceManager = getCompanionDeviceManager(activity) - if (companionDeviceManager == null) { - Log.e(TAG, "设备不支持CompanionDeviceManager") - return false - } + fun clearPairedMacAddress() { + sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply() + Log.i(TAG, "已清除连接设备MAC") + } + + // /** + // * 检查是否有已关联的设备 + // */ + // @RequiresApi(Build.VERSION_CODES.O) + // fun hasAssociations(): Boolean { + // val associationIds = getAssociationIds() + // return associationIds.isNotEmpty() + // } + + // /** + // * 获取所有关联ID + // */ + // @RequiresApi(Build.VERSION_CODES.O) + // fun getAssociationIds(): List { + // // 使用默认上下文获取CompanionDeviceManager + // val companionDeviceManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager + // } else { + // null + // } + // return getAssociationIds(companionDeviceManager) + // } + + // /** + // * 获取所有关联ID(使用指定的CompanionDeviceManager) + // */ + // @RequiresApi(Build.VERSION_CODES.O) + // private fun getAssociationIds(companionDeviceManager: CompanionDeviceManager?): List { + // if (companionDeviceManager == null) { + // return emptyList() + // } - try { - // 构建设备过滤器 - val deviceFilterBuilder = BluetoothDeviceFilter.Builder() + // // 注意: allAssociations 仅在API 33 (Android 13)及以上版本可用 + // // 在早期版本中,使用associations (返回的是String数组) + // return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // try { + // // 由于AssociationInfo类在API 30+引入,使用反射获取更安全 + // val method = CompanionDeviceManager::class.java.getMethod("getAllAssociations") + // @Suppress("UNCHECKED_CAST") + // val associations = method.invoke(companionDeviceManager) as? List + // associations?.map { association -> + // // 尝试获取AssociationInfo.id + // val idField = association.javaClass.getField("id") + // idField.get(association).toString() + // } ?: emptyList() + // } catch (e: Exception) { + // Log.e(TAG, "获取关联ID失败: ${e.message}", e) + // emptyList() + // } + // } else { + // // API 26-32 使用 associations 方法 + // companionDeviceManager.associations?.toList() ?: emptyList() + // } + // } + + // /** + // * 开始设备关联流程 + // * + // * @param activity 当前Activity,用于启动系统关联对话框 + // * @param deviceNamePattern 设备名称正则表达式,例如"MyHeadset.*" + // * @param serviceUuid 可选的服务UUID + // * @param macAddress 可选的MAC地址 + // */ + // @RequiresApi(Build.VERSION_CODES.O) + // fun startAssociation( + // activity: Activity, + // deviceNamePattern: String? = null, + // serviceUuid: String? = null, + // macAddress: String? = null + // ): Boolean { + // // 使用Activity上下文获取CompanionDeviceManager + // val companionDeviceManager = getCompanionDeviceManager(activity) + // if (companionDeviceManager == null) { + // Log.e(TAG, "设备不支持CompanionDeviceManager") + // return false + // } + + // try { + // // 构建设备过滤器 + // val deviceFilterBuilder = BluetoothDeviceFilter.Builder() - // 添加设备名称 - 严格匹配模式 - deviceNamePattern?.let { - // 使用"^设备名称$"格式创建精确匹配的正则表达式 - val exactNamePattern = Pattern.compile("^$it$") - deviceFilterBuilder.setNamePattern(exactNamePattern) - Log.i(TAG, "设置严格名称匹配: $it") - } + // // 添加设备名称 - 严格匹配模式 + // deviceNamePattern?.let { + // // 使用"^设备名称$"格式创建精确匹配的正则表达式 + // val exactNamePattern = Pattern.compile("^$it$") + // deviceFilterBuilder.setNamePattern(exactNamePattern) + // Log.i(TAG, "设置严格名称匹配: $it") + // } - // 添加服务UUID - serviceUuid?.let { - try { - val uuid = ParcelUuid(java.util.UUID.fromString(it)) - deviceFilterBuilder.addServiceUuid(uuid, null) - Log.i(TAG, "设置服务UUID匹配: $it") - } catch (e: Exception) { - Log.e(TAG, "设置服务UUID失败: ${e.message}", e) - } - } + // // 添加服务UUID + // serviceUuid?.let { + // try { + // val uuid = ParcelUuid(java.util.UUID.fromString(it)) + // deviceFilterBuilder.addServiceUuid(uuid, null) + // Log.i(TAG, "设置服务UUID匹配: $it") + // } catch (e: Exception) { + // Log.e(TAG, "设置服务UUID失败: ${e.message}", e) + // } + // } - // 添加MAC地址 - 严格匹配 - macAddress?.let { - try { - // 检查是否是有效的MAC地址格式 - if (!isValidMacAddress(it)) { - Log.e(TAG, "无效的MAC地址格式: $it") - return false - } + // // 添加MAC地址 - 严格匹配 + // macAddress?.let { + // try { + // // 检查是否是有效的MAC地址格式 + // if (!isValidMacAddress(it)) { + // Log.e(TAG, "无效的MAC地址格式: $it") + // return false + // } - // 存储MAC地址到临时变量,成功匹配后再保存 - pendingMacAddress = it + // // 存储MAC地址到临时变量,成功匹配后再保存 + // pendingMacAddress = it - // 设置MAC地址严格匹配 - deviceFilterBuilder.setAddress(it) - Log.i(TAG, "设置MAC地址严格匹配: $it") - } catch (e: Exception) { - Log.e(TAG, "设置MAC地址失败: ${e.message}", e) - return false - } - } ?: run { - // 如果没有提供MAC地址也没有提供设备名称,则返回错误 - if (deviceNamePattern == null && serviceUuid == null) { - Log.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一") - return false - } - } + // // 设置MAC地址严格匹配 + // deviceFilterBuilder.setAddress(it) + // Log.i(TAG, "设置MAC地址严格匹配: $it") + // } catch (e: Exception) { + // Log.e(TAG, "设置MAC地址失败: ${e.message}", e) + // return false + // } + // } ?: run { + // // 如果没有提供MAC地址也没有提供设备名称,则返回错误 + // if (deviceNamePattern == null && serviceUuid == null) { + // Log.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一") + // return false + // } + // } - // 创建请求 - val associationRequest = AssociationRequest.Builder() - .addDeviceFilter(deviceFilterBuilder.build()) - .setSingleDevice(true) // 找到一个即停止 - .build() + // // 创建请求 + // val associationRequest = AssociationRequest.Builder() + // .addDeviceFilter(deviceFilterBuilder.build()) + // .setSingleDevice(true) // 找到一个即停止 + // .build() - // 发起关联请求 - companionDeviceManager.associate( - associationRequest, - object : CompanionDeviceManager.Callback() { - override fun onDeviceFound(intentSender: IntentSender) { - try { - activity.startIntentSenderForResult( - intentSender, - REQUEST_CODE_ASSOCIATE, - null, - 0, - 0, - 0 - ) - Log.i(TAG, "已启动设备选择对话框") - } catch (e: Exception) { - Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e) - } - } + // // 发起关联请求 + // companionDeviceManager.associate( + // associationRequest, + // object : CompanionDeviceManager.Callback() { + // override fun onDeviceFound(intentSender: IntentSender) { + // try { + // activity.startIntentSenderForResult( + // intentSender, + // REQUEST_CODE_ASSOCIATE, + // null, + // 0, + // 0, + // 0 + // ) + // Log.i(TAG, "已启动设备选择对话框") + // } catch (e: Exception) { + // Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e) + // } + // } - // API 26 (Android 8) 使用 onDeviceFound - // API 28+ (Android 9+) 使用 onAssociationPending - @RequiresApi(Build.VERSION_CODES.P) - override fun onAssociationPending(intentSender: IntentSender) { - try { - activity.startIntentSenderForResult( - intentSender, - REQUEST_CODE_ASSOCIATE, - null, - 0, - 0, - 0 - ) - Log.i(TAG, "已启动设备选择对话框") - } catch (e: Exception) { - Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e) - } - } + // // API 26 (Android 8) 使用 onDeviceFound + // // API 28+ (Android 9+) 使用 onAssociationPending + // @RequiresApi(Build.VERSION_CODES.P) + // override fun onAssociationPending(intentSender: IntentSender) { + // try { + // activity.startIntentSenderForResult( + // intentSender, + // REQUEST_CODE_ASSOCIATE, + // null, + // 0, + // 0, + // 0 + // ) + // Log.i(TAG, "已启动设备选择对话框") + // } catch (e: Exception) { + // Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e) + // } + // } - // API 33+ (Android 13+) 使用 onAssociationCreated - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - override fun onAssociationCreated(associationInfo: AssociationInfo) { - Log.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}") - // 保存关联ID - try { - val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress") - val mac = macAddressMethod.invoke(associationInfo) as? String - mac?.let { savePairedMacAddress(it) } - } catch (e: Exception) { - Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e) - } - } + // // API 33+ (Android 13+) 使用 onAssociationCreated + // @RequiresApi(Build.VERSION_CODES.TIRAMISU) + // override fun onAssociationCreated(associationInfo: AssociationInfo) { + // Log.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}") + // // 保存关联ID + // try { + // val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress") + // val mac = macAddressMethod.invoke(associationInfo) as? String + // mac?.let { savePairedMacAddress(it) } + // } catch (e: Exception) { + // Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e) + // } + // } - override fun onFailure(error: CharSequence?) { - Log.e(TAG, "设备关联失败: $error") - } - }, - null - ) + // override fun onFailure(error: CharSequence?) { + // Log.e(TAG, "设备关联失败: $error") + // } + // }, + // null + // ) - return true - } catch (e: Exception) { - Log.e(TAG, "启动设备关联失败: ${e.message}", e) - return false - } - } + // return true + // } catch (e: Exception) { + // Log.e(TAG, "启动设备关联失败: ${e.message}", e) + // return false + // } + // } - /** - * 处理关联结果 - * - * @param requestCode 请求码 - * @param resultCode 结果码 - * @param data Intent数据 - * @return 是否处理了关联结果 - */ - @RequiresApi(Build.VERSION_CODES.O) - fun handleAssociationResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode != REQUEST_CODE_ASSOCIATE) { - return false - } + // /** + // * 处理关联结果 + // * + // * @param requestCode 请求码 + // * @param resultCode 结果码 + // * @param data Intent数据 + // * @return 是否处理了关联结果 + // */ + // @RequiresApi(Build.VERSION_CODES.O) + // fun handleAssociationResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + // if (requestCode != REQUEST_CODE_ASSOCIATE) { + // return false + // } - Log.i(TAG, "收到关联结果: resultCode=$resultCode") + // Log.i(TAG, "收到关联结果: resultCode=$resultCode") - if (resultCode == Activity.RESULT_OK && data != null) { - // 获取选中的蓝牙设备 - val bluetoothDevice: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE, BluetoothDevice::class.java) - } else { - @Suppress("DEPRECATION") - data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE) - } + // if (resultCode == Activity.RESULT_OK && data != null) { + // // 获取选中的蓝牙设备 + // val bluetoothDevice: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE, BluetoothDevice::class.java) + // } else { + // @Suppress("DEPRECATION") + // data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE) + // } - // 获取关联ID (Android 13+) - val associationId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - try { - // 由于EXTRA_ASSOCIATION_ID常量仅在API 33+可用,直接使用已知的常量字符串值 - // 不再尝试使用反射获取字段 - val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID" - data.getStringExtra(extraAssociationIdKey) - } catch (e: Exception) { - Log.e(TAG, "获取关联ID失败: ${e.message}", e) - null - } - } else { - null - } + // // 获取关联ID (Android 13+) + // val associationId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // try { + // // 由于EXTRA_ASSOCIATION_ID常量仅在API 33+可用,直接使用已知的常量字符串值 + // // 不再尝试使用反射获取字段 + // val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID" + // data.getStringExtra(extraAssociationIdKey) + // } catch (e: Exception) { + // Log.e(TAG, "获取关联ID失败: ${e.message}", e) + // null + // } + // } else { + // null + // } - // 获取并验证MAC地址 - if (bluetoothDevice != null) { - val deviceMac = bluetoothDevice.address - val deviceName = bluetoothDevice.name ?: "未知设备" + // bluetoothDevice?.let { device -> + // val deviceMac = device.address + // Log.i(TAG, "设备关联成功: ${device.name} ($deviceMac)") - // 验证MAC地址格式 - if (!isValidMacAddress(deviceMac)) { - Log.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac") - return false - } + // // 验证MAC地址是否匹配预期 + // pendingMacAddress?.let { expectedMac -> + // if (deviceMac.equals(expectedMac, ignoreCase = true)) { + // Log.i(TAG, "MAC地址验证成功: $deviceMac") + // } else { + // Log.w(TAG, "MAC地址不匹配: 预期=$expectedMac, 实际=$deviceMac") + // } + // } - // 检查是否与预期的MAC地址匹配 - if (pendingMacAddress != null && pendingMacAddress != deviceMac) { - Log.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配") - // 如果要严格匹配MAC,可以在这里返回false - return false - } - - // 匹配成功后,保存新的MAC地址 - savePairedMacAddress(deviceMac) - Log.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId") + // // 保存设备MAC地址 + // savePairedMacAddress(deviceMac) - // 清除临时MAC地址 - pendingMacAddress = null + // // 保存关联ID (如果可用) + // associationId?.let { id -> + // Log.i(TAG, "保存关联ID: $id") + // // 这里可以保存关联ID到SharedPreferences或其他存储 + // } - // 🎯 立即发起BLE连接 - // try { - // Log.i(TAG, "配对成功,立即发起BLE连接: $deviceMac") - // BleService.connect(deviceMac) - // } catch (e: Exception) { - // Log.e(TAG, "配对成功后发起BLE连接失败: ${e.message}", e) - // } + // // 清除临时MAC地址 + // pendingMacAddress = null - return true - } else { - Log.e(TAG, "无法获取关联的蓝牙设备信息") - } - } else { - Log.w(TAG, "设备关联被取消或失败: resultCode=$resultCode") - // 清除临时MAC地址 - pendingMacAddress = null - } - - return false - } + // return true + // } ?: run { + // Log.e(TAG, "无法获取蓝牙设备信息") + // return false + // } + // } else { + // Log.w(TAG, "设备关联被取消或失败") + // // 清除临时MAC地址 + // pendingMacAddress = null + // return false + // } + // } /** * 注册后台扫描 @@ -382,12 +483,14 @@ class BleCompanionManager(private val context: Context) { ) { try { Log.d(TAG, "添加后台扫描过滤器UUID: $uuids, macAddress: $macAddress") - val mac = getPairedMacAddress() - if (mac.isNullOrEmpty()) { + //val mac = getPairedMacAddress() + + if (macAddress.isNullOrEmpty()||uuids.isNullOrEmpty()) { Log.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址") return } - + savePairedMacAddress(macAddress) + savePairedUuid(uuids) val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager val bluetoothAdapter = bluetoothManager?.adapter val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner @@ -440,24 +543,24 @@ class BleCompanionManager(private val context: Context) { .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) .build() - // 创建PendingIntent - val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) - .setClass(context, BleWakeReceiver::class.java) + // // 创建PendingIntent + // val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) + // .setClass(context, BleWakeReceiver::class.java) - val pendingIntent = PendingIntent.getBroadcast( - context, - 0, - scanIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - ) + // val pendingIntent = PendingIntent.getBroadcast( + // context, + // 0, + // scanIntent, + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + // } else { + // PendingIntent.FLAG_UPDATE_CURRENT + // } + // ) // 启动后台扫描 bluetoothLeScanner.startScan(filters, settings, pendingIntent) - Log.i(TAG, "已注册后台扫描,目标设备MAC: $mac") + Log.i(TAG, "已注册后台扫描,目标设备MAC: $macAddress") } catch (e: Exception) { Log.e(TAG, "注册后台扫描失败: ${e.message}", e) } @@ -477,20 +580,20 @@ class BleCompanionManager(private val context: Context) { return } - // 创建PendingIntent - val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) - .setClass(context, BleWakeReceiver::class.java) + // // 创建PendingIntent + // val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) + // .setClass(context, BleWakeReceiver::class.java) - val pendingIntent = PendingIntent.getBroadcast( - context, - 0, - scanIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - ) + // val pendingIntent = PendingIntent.getBroadcast( + // context, + // 0, + // scanIntent, + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + // } else { + // PendingIntent.FLAG_UPDATE_CURRENT + // } + // ) // 停止后台扫描 bluetoothLeScanner.stopScan(pendingIntent) @@ -500,42 +603,42 @@ class BleCompanionManager(private val context: Context) { } } - /** - * 清除所有关联 - */ - @RequiresApi(Build.VERSION_CODES.O) - fun clearAllAssociations() { - try { - // 获取CompanionDeviceManager - val companionDeviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager - if (companionDeviceManager == null) { - Log.e(TAG, "无法获取CompanionDeviceManager") - return - } + // /** + // * 清除所有关联 + // */ + // @RequiresApi(Build.VERSION_CODES.O) + // fun clearAllAssociations() { + // try { + // // 获取CompanionDeviceManager + // val companionDeviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager + // if (companionDeviceManager == null) { + // Log.e(TAG, "无法获取CompanionDeviceManager") + // return + // } - // 清除本地保存的MAC地址 - clearPairedMacAddress() + // // 清除本地保存的MAC地址 + // clearPairedMacAddress() - // 获取所有关联ID - val associationIds = getAssociationIds(companionDeviceManager) + // // 获取所有关联ID + // val associationIds = getAssociationIds(companionDeviceManager) - // 遍历解除关联 - for (id in associationIds) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - companionDeviceManager.disassociate(id) - } else { - companionDeviceManager.disassociate(id) - } - Log.i(TAG, "已解除设备关联: $id") - } catch (e: Exception) { - Log.e(TAG, "解除设备关联失败: $id, ${e.message}", e) - } - } - } catch (e: Exception) { - Log.e(TAG, "清除所有关联失败: ${e.message}", e) - } - } + // // 遍历解除关联 + // for (id in associationIds) { + // try { + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // companionDeviceManager.disassociate(id) + // } else { + // companionDeviceManager.disassociate(id) + // } + // Log.i(TAG, "已解除设备关联: $id") + // } catch (e: Exception) { + // Log.e(TAG, "解除设备关联失败: $id, ${e.message}", e) + // } + // } + // } catch (e: Exception) { + // Log.e(TAG, "清除所有关联失败: ${e.message}", e) + // } + // } /** * 验证是否是有效的MAC地址格式 diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt deleted file mode 100644 index 1e50ccc32..000000000 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.yunqiinnovation.ble_service - -import android.companion.AssociationInfo -import android.companion.CompanionDeviceService -import android.content.Intent -import android.os.Build -import android.os.IBinder -import androidx.annotation.RequiresApi -import android.util.Log - -/** - * BleCompanionService - * 用于监听配对设备的出现和消失 - * 注:此服务仅在Android 11 (API 30)及以上版本可用 - */ -@RequiresApi(Build.VERSION_CODES.R) -class BleCompanionService : CompanionDeviceService() { - private val TAG = "BleCompanionService" - - override fun onCreate() { - super.onCreate() - Log.i(TAG, "BleCompanionService已创建") - } - - override fun onDeviceAppeared(info: AssociationInfo) { - super.onDeviceAppeared(info) - Log.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}") - - // 当配对设备出现时,启动前台服务 - try { - val serviceIntent = Intent().apply { - // 尝试使用BleMediaService,如果不存在则回退到BleService - val serviceClassName = try { - Class.forName("com.yunqiinnovation.agent_service.BleMediaService").name - } catch (e: ClassNotFoundException) { - BleService::class.java.name - } - - setClassName(this@BleCompanionService, serviceClassName) - action = "android.intent.action.DEVICE_APPEARED" - putExtra("assocId", info.id) - putExtra("displayName", info.displayName) - - // 如果设备信息中有MAC地址,也可以传递 - // 使用反射获取MAC地址,因为deviceMacAddress在不同API版本可能不同或不存在 - try { - val macAddressMethod = info.javaClass.getMethod("getDeviceMacAddress") - val mac = macAddressMethod.invoke(info) as? String - mac?.let { putExtra("mac", it) } - } catch (e: Exception) { - Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e) - } - } - - // 启动前台服务 - startForegroundService(serviceIntent) - Log.i(TAG, "已启动服务") - } catch (e: Exception) { - Log.e(TAG, "启动服务失败: ${e.message}", e) - } - } - - override fun onDeviceDisappeared(info: AssociationInfo) { - super.onDeviceDisappeared(info) - Log.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}") - - // 可以选择停止服务或执行其他清理操作 - } - - // 注意: 不需要重写 onBindCompanionDeviceService 和 onUnbindCompanionDeviceService - // 这些方法在 Android 12+ 才添加,我们可以使用父类的默认实现 -} \ No newline at end of file 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 87355f98c..d9b133f2a 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 @@ -9,12 +9,7 @@ import android.os.* import androidx.lifecycle.MutableLiveData import java.util.* import java.util.concurrent.CopyOnWriteArrayList -// Import Jieli Opus SDK classes -import com.jieli.jl_audio_decode.opus.model.OpusOption -import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback -import com.jieli.jl_audio_decode.callback.OnEncodeStreamCallback -import com.jieli.jl_audio_decode.opus.OpusManager -import com.jieli.jl_audio_decode.exceptions.OpusException + import java.io.File @@ -37,10 +32,11 @@ import java.util.concurrent.TimeUnit */ object BleService { - // ====================================================================================================== + // ====================================================================================================== // 常量定义 // ====================================================================================================== private const val TAG = "BleService" + private const val REPLY_TIMEOUT_MS = 1000L // 响应超时时间1秒 // ====================================================================================================== // 回调接口 @@ -68,7 +64,6 @@ object BleService { // 上下文 private lateinit var context: Context - private val commandQueue: Queue = LinkedList() // 蓝牙相关组件 private lateinit var bluetoothManager: BluetoothManager @@ -108,19 +103,16 @@ object BleService { // 回调集合 - 使用线程安全的集合存储所有回调 private val callbacks = CopyOnWriteArrayList() - // Opus解码器实例 - private var opusManager: OpusManager? = null - private var option: OpusOption? = null + // Opus音频管理器 + private val opusAudioManager = OpusAudioManager() private val mainHandler = Handler(Looper.getMainLooper()) - // 解码音频数据队列和处理线程 - private val audioDataQueue = LinkedBlockingQueue() - private var audioQueueProcessorThread: Thread? = null + // // 解码音频数据队列和处理线程 + // private val audioDataQueue = LinkedBlockingQueue() + // private var audioQueueProcessorThread: Thread? = null // 音频数据缓存 - private val audioDataBuffer = mutableListOf() - //private val AUDIO_BUFFER_SIZE = 1280// 1280字节缓存阈值 - + //private val audioDataBuffer = mutableListOf() // 音频数据发送相关 private val audioSendQueue = LinkedBlockingQueue() private var audioSendThread: Thread? = null @@ -128,21 +120,35 @@ object BleService { private val isAudioSending = AtomicBoolean(false) // 音频数据分块发送的常量 - private val AUDIO_CHUNK_SIZE = 120 // 每次发送80字节 - private val AUDIO_SEND_INTERVAL = 60L // 发送间隔20ms - + private val AUDIO_CHUNK_SIZE = 120 // 每次发送120字节 + private val AUDIO_SEND_INTERVAL = 60L // 发送间隔60ms // 音频数据缓冲区,用于累积数据到80字节再发送 private val audioBuffer = mutableListOf() - // 重发机制相关常量 private val MAX_RETRY_COUNT = 1 // 最大重试次数 private val RETRY_DELAY = 30L // 重试延迟时间(毫秒) + // MTU相关变量 + private var currentMtu = 23 // 默认MTU值 + private var maxMtu = 517 // 最大MTU值 (512 + 5字节头部) + private var isWaitingForWriteCallback = false // 是否正在等待写入回调 + // 初始化状态 private var isInitialized = false + // ====================================================================================================== + // 指令发送和管理相关变量(整合自BleCommandSender) + // ====================================================================================================== + + /** 指令发送器实例 */ + private val commandSender = BleCommandSender() + + private var companionManager: BleCompanionManager? = null /** * 检查是否有蓝牙扫描权限 + * Android 12+ 需要BLUETOOTH_SCAN权限,之前版本需要ACCESS_FINE_LOCATION权限 + * + * @return true 如果有相应权限,false 如果没有权限 */ private fun hasBluetoothScanPermission(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -177,14 +183,7 @@ object BleService { } } - private val replyTimeoutHandler = Handler(Looper.getMainLooper()) - private val replyTimeoutRunnable = Runnable { - if (!isReply) { - Log.w(TAG, "命令响应超时,自动重置 isReply 为 true") - isReply = true - processNextCommand() - } - } + /** * 初始化BLE服务 @@ -210,18 +209,20 @@ object BleService { recordfile1 = RecordingFile(this.context) recordfile1!!.fileName = "重新压缩" - // 启动队列处理 - startAudioQueueProcessing() - // 初始化OpusManager和OpusOption - try { - opusManager = OpusManager() - option = OpusOption() - Log.d(TAG, "OpusManager初始化成功") - } catch (e: OpusException) { - Log.e(TAG, "OpusManager初始化失败: ${e.message}", e) + // 初始化Opus音频管理器 + if (!opusAudioManager.initialize()) { + Log.e(TAG, "Opus音频管理器初始化失败") // 根据需要决定是否因为Opus初始化失败而返回false + } else { + // 设置Opus音频管理器回调 + setupOpusAudioManagerCallback() + Log.d(TAG, "Opus音频管理器初始化成功") } + + // 初始化指令发送器 + initializeCommandSender() + isInitialized = true return true } catch (e: Exception) { @@ -230,6 +231,35 @@ object BleService { } } + /** + * 设置Opus音频管理器回调 + */ + private fun setupOpusAudioManagerCallback() { + opusAudioManager.setCallback(object : OpusAudioManager.OpusAudioCallback { + override fun onAudioDataDecoded(data: ByteArray, channel: Int) { + // 将解码后的音频数据通过BLE服务回调传递给上层 + notifyAudioDataReceived(data, channel) + } + + override fun onAudioDataEncoded(data: ByteArray) { + // 将编码后的音频数据加入发送队列 + addAudioDataToSendQueue(data) + } + + override fun onDecodeStreamStateChanged(isStarted: Boolean) { + Log.d(TAG, "Opus解码流状态变化: $isStarted") + } + + override fun onEncodeStreamStateChanged(isStarted: Boolean) { + Log.d(TAG, "Opus编码流状态变化: $isStarted") + } + + override fun onError(errorType: String, code: Int, message: String?) { + Log.e(TAG, "Opus音频管理器错误 [$errorType]: [$code] $message") + } + }) + } + /** * 添加回调监听器 * @param callback 要添加的回调监听器 @@ -244,9 +274,69 @@ object BleService { return false } + /** + * 初始化指令发送器 + */ + private fun initializeCommandSender() { + commandSender.setCallback(object : BleCommandSender.CommandSenderCallback { + /** + * 检查是否已连接 + */ + override fun isConnected(): Boolean { + return checkConn() + } + + /** + * 获取写入特征 + */ + override fun getWriteCharacteristic(): BluetoothGattCharacteristic? { + return writeChar + } + + /** + * 获取蓝牙GATT连接 + */ + override fun getBluetoothGatt(): BluetoothGatt? { + return bluetoothGatt + } + + /** + * 通知设备信息接收 + * @param infoType 信息类型 + * @param infoData 信息数据 + */ + override fun notifyDeviceInfoReceived(infoType: Int, infoData: Map) { + // 使用外部类的方法来分发设备信息,避免无限递归 + this@BleService.notifyDeviceInfoReceived(infoType, infoData) + } + + /** + * 通知唤醒信号接收 + */ + override fun notifyWakeupSignalReceived() { + // 使用外部类的方法来分发唤醒信号,避免无限递归 + this@BleService.notifyWakeupSignalReceived() + } + + /** + * 获取录音文件实例 + */ + override fun getRecordFile(): RecordingFile? { + return recordfile + } + + /** + * 获取录音文件实例1 + */ + override fun getRecordFile1(): RecordingFile? { + return recordfile1 + } + }) + } + /** * 移除回调监听器 - * @param callback 要移除的回调监听器 + * @param callback 要移除回调监听器 * @return 是否移除成功 */ fun removeCallback(callback: Callback): Boolean { @@ -265,28 +355,24 @@ object BleService { Log.d(TAG, "已清除所有BLE回调") } + /** + * 写入外部音频数据到Opus编码流 + * 该函数用于将外部音频数据(如麦克风录音)写入到Opus编码器中 + * + * @param data 音频数据字节数组,通常为PCM格式 + */ fun writeExternalAudioData(data: ByteArray) { - // 检查OpusManager是否已初始化 - if (opusManager == null) { - Log.e(TAG, "opusManager 未初始化") - return - } - - // 如果已经在编码流中,直接写入数据 - if (opusManager?.isEncodeStream == true) { - Log.d(TAG, "正在进行Opus编码流,写入音频数据") - // 将外部音频数据写入编码流,每次处理1280字节 - opusManager?.writeEncodeStream(data) - } else { - Log.w(TAG, "Opus编码流未启动,无法写入音频数据") - // 可选:自动启动编码流 - // startOpusEncodeStream() - } + opusAudioManager.writeExternalAudioData(data) } // ====================================================================================================== // 扫描功能 // ====================================================================================================== + + /** + * 重新初始化蓝牙适配器和扫描器 + * 当蓝牙适配器状态发生变化时,需要重新获取最新的实例引用 + */ fun reinitializeBle() { val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothAdapter = manager.adapter // 重新获取新引用 @@ -516,15 +602,18 @@ object BleService { // ====================================================================================================== /** * 自动连接已配对的BLE设备 + * + * 该方法会尝试连接之前通过CompanionDeviceManager配对的设备 * 如果没有找到已配对的MAC地址,将不会进行连接 */ fun connect() { try { - // 创建 BleCompanionManager 实例 - val companionManager = BleCompanionManager(context) + Log.i(TAG, "设备连接与通信: 自动连接已配对的BLE设备") + // 获取 BleCompanionManager 单例实例 + companionManager = BleCompanionManager.getInstance(context) // 获取已配对的 MAC 地址 - val pairedMac = companionManager.getPairedMacAddress() + val pairedMac = companionManager?.getPairedMacAddress() if (pairedMac.isNullOrEmpty()) { Log.w(TAG, "未找到已配对的设备 MAC 地址") @@ -603,7 +692,7 @@ object BleService { Log.e(TAG, "关闭GATT连接异常: ${e.message}", e) } isReply = true; - commandQueue.clear() + commandSender.clearCommandQueue() bluetoothGatt = null notifyChar = null writeChar = null @@ -646,6 +735,7 @@ object BleService { status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> { updateConnectionState(BleConst.STATE_CONNECTED) Log.d(TAG, "成功连接Ble蓝牙设备:${g.device.name}") + companionManager?.unregisterBackgroundScan() // 设置PHY值为2M以提高传输速度 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { @@ -679,7 +769,11 @@ object BleService { newState == BluetoothProfile.STATE_DISCONNECTED -> { updateConnectionState(BleConst.STATE_DISCONNECTED) Log.d(TAG, "监听到已断开连接") + disconnectGatt() + val pairedUuid = companionManager?.getPairedUuid() + val pairedMac = companionManager?.getPairedMacAddress() + companionManager?.registerBackgroundScan(pairedMac, pairedUuid) } else -> { @@ -756,20 +850,23 @@ object BleService { return } Log.d(TAG, "找到通话音频服务所需特征") -startBytesStatistics() + startBytesStatistics() writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE callWriteChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE //要先设置音频服务的通知,否则接收不到 // 设置音频服务的通知(如果存在) if (audioChar != null) { - setupNotifications(g, audioChar) - Log.i(TAG, "音频服务特征找到并设置通知") + setupNotifications(g, audioChar) + Log.i(TAG, "音频服务特征找到并设置通知") + } else { Log.w(TAG, "音频服务特征未找到") } - // 设置主服务的通知 - setupNotifications(g, notifyChar) + setupNotifications(g, notifyChar) + + // 在服务发现完成后,请求最大MTU + //requestMaxMtu(g) } @@ -788,16 +885,16 @@ startBytesStatistics() } // 通知特征数据(命令和控制) BleConst.NOTIFY_CHAR_UUID -> { - // Log.i(TAG, "收到通知特征数据(命令和控制)") + Log.i(TAG, "收到通知特征数据(命令和控制)") // 判断数据类型 when { // 设备响应 (0xBB开头) data.isNotEmpty() && data[0] == BleConst.DEVICE_RESPONSE_HEADER -> { - processDeviceResponse(data) + commandSender.processDeviceResponse(data) } // 设备主动上报 (0xCC开头) data.isNotEmpty() && data[0] == BleConst.DEVICE_NOTIFY_HEADER -> { - processDeviceNotification(data) + commandSender.processDeviceNotification(data) } // 其他未知数据 else -> { @@ -822,13 +919,14 @@ startBytesStatistics() } } + override fun onCharacteristicWrite( g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int ) { - Log.i( - TAG, - "写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}" - ) + Log.i( + TAG, + "写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}" + ) } override fun onDescriptorWrite( @@ -837,563 +935,140 @@ startBytesStatistics() if (status == BluetoothGatt.GATT_SUCCESS) Log.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}") } + + /** + * MTU变化回调 - 处理MTU协商结果 + */ + override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { + super.onMtuChanged(gatt, mtu, status) + if (status == BluetoothGatt.GATT_SUCCESS) { + currentMtu = mtu + Log.i(TAG, "MTU协商成功: $mtu 字节") + // 更新音频块大小,预留5字节的ATT头部 + val newChunkSize = (mtu - 5).coerceAtMost(512) + Log.i(TAG, "更新音频块大小为: $newChunkSize 字节") + } else { + Log.w(TAG, "MTU协商失败: status=$status,使用默认MTU: $currentMtu") + } + } } /** * 设置特征的通知功能 + * @param g BluetoothGatt实例 + * @param characteristic 要设置通知的特征 */ - private fun setupNotifications(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) { + private fun setupNotifications(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) : Boolean{ if (characteristic == null || characteristic.properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) { Log.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}") - return + return false + } + + // 在本地启用特征通知监听 + val localResult = g.setCharacteristicNotification(characteristic, true) + if (!localResult) { + Log.e(TAG, "本地通知设置失败: ${characteristic.uuid}") + return false } + Log.d(TAG, "本地通知设置成功: ${characteristic.uuid}") - g.setCharacteristicNotification(characteristic, true) + // 获取客户端特征配置描述符 val desc = characteristic.getDescriptor(BleConst.CLIENT_CHAR_CONFIG_UUID) - desc?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - g.writeDescriptor(desc) - } + if (desc == null) { + Log.w(TAG, "未找到客户端配置描述符: ${characteristic.uuid}") + return false + } + + // 设置描述符值为启用通知 + desc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + // 向远程设备写入描述符(异步操作,结果在onDescriptorWrite回调中处理) + val writeResult = g.writeDescriptor(desc) + if (writeResult) { + Log.d(TAG, "开始写入通知描述符: ${characteristic.uuid}") + } else { + Log.e(TAG, "通知描述符写入请求失败: ${characteristic.uuid}") + } + return true + } + /** - * 处理音频数据 - * @param data 音频数据 (Opus) + * 请求最大MTU值 + * @param gatt BluetoothGatt实例 */ - private fun processAudioData(data: ByteArray) { + private fun requestMaxMtu(gatt: BluetoothGatt) { try { - - - if (opusManager?.isDecodeStream == true) { - recordfile?.saveAudioDataToWav(data) - // 将完整的帧加入队列,由专门的线程处理 - audioDataQueue.offer(data) - + val result = gatt.requestMtu(maxMtu) + if (result) { + Log.i(TAG, "请求MTU $maxMtu 成功") } else { - // Log.d(TAG, "Opus解码流未启动,忽略音频数据") + Log.w(TAG, "请求MTU $maxMtu 失败") } } catch (e: Exception) { - Log.e(TAG, "处理音频数据异常: ${e.message}", e) + Log.e(TAG, "请求MTU异常: ${e.message}", e) } } - /** - * 处理设备响应数据 - 帧头为0xBB + * 处理接收到的音频数据 + * @param data 音频数据 (Opus编码) */ - @SuppressLint("StaticFieldLeak") - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - private fun processDeviceResponse(data: ByteArray) { - Log.d( - TAG, - "收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" - ) - if (data.size < 3) { - Log.e(TAG, "设备响应数据格式错误:数据长度过短") - return - } - - val header = data[0] // 0xBB - val cmdType = data[1].toInt() and 0xFF - - // 判断命令失败响应(长度为0) - if (data.size == 4 && data[2].toInt() and 0xFF == 0) { - Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") - // 创建失败结果Map - val resultMap = mapOf( - "success" to false, - "command" to cmdType, - "rawData" to byteArrayOf() - ) - notifyDeviceInfoReceived(cmdType, resultMap) - return - } - - // 正常响应处理 - val length = data[2].toInt() and 0xFF - - // 检查数据长度 - if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC - Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") - return - } - - // 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 - val payload = data.sliceArray(0 until data.size - 1) - val calculatedCrc = crc8Maxim(payload, payload.size) - val receivedCrc = data[data.size - 1].toInt() and 0xFF - - if (calculatedCrc.toInt() and 0xFF != receivedCrc) { - Log.e( - TAG, - "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" - ) - return - } - - // 提取响应数据 - val responseData = if (length > 0) ByteArray(length) else ByteArray(0) - if (length > 0) { - System.arraycopy(data, 3, responseData, 0, length) - } - - Log.d( - TAG, - "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ - responseData.joinToString(", ") { - "0x${ - (it.toInt() and 0xFF).toString(16) - }" - } - }" - ) - isReply = true - cmdReplyType = cmdType - replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable) // 移除超时回调 - processNextCommand() - // 根据命令类型处理不同的响应 - when (cmdType) { - BleConst.CMD_GET_VERSION -> { - val resultMap = mutableMapOf( - "success" to true, - "command" to "versionInfo", - "rawData" to responseData - ) - - if (responseData.isNotEmpty()) { - // 转换为字符串前,确认是否是有效的ASCII字符 - val version = - responseData - .map { it.toInt() and 0xFF } // 转为无符号整数 (0-255) - .dropWhile { it == 0xFF } // 仅丢弃开头的连续 255 - .joinToString(".") { it.toString() } - resultMap["version"] = version - Log.i(TAG, "接收到目标版本号: $version") - } - - notifyDeviceInfoReceived(cmdType, resultMap) - - } - - BleConst.CMD_GET_PRODUCT_ID -> { - val resultMap = mutableMapOf( - "success" to true, - "command" to "productId", - "rawData" to responseData - ) - - if (responseData.isNotEmpty()) { - // 转换为字符串前,确认是否是有效的ASCII字符 - val productId = - responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } - resultMap["productId"] = productId - Log.i(TAG, "接收到产品ID: $productId") - } - - notifyDeviceInfoReceived(cmdType, resultMap) - } - - BleConst.CMD_GET_COLOR_ID -> { - val resultMap = mutableMapOf( - "success" to true, - "command" to "colorInfo", - "rawData" to responseData - ) - - if (responseData.isNotEmpty()) { - val colorId = (responseData[0].toInt() and 0xFF).toString() - resultMap["colorId"] = colorId - Log.i(TAG, "接收到耳机颜色ID: $colorId") - } - - notifyDeviceInfoReceived(cmdType, resultMap) - } - - BleConst.CMD_GET_BATTERY_INFO -> { - val resultMap = mutableMapOf( - "success" to true, - "command" to "batteryInfo", - "rawData" to responseData - ) - - if (responseData.isNotEmpty() && responseData.size >= 3) { - // 处理电池信息 - 左耳、右耳、充电仓 - val leftEarBattery = responseData[0].toInt() and 0x7F - val leftEarCharging = (responseData[0].toInt() and 0x80) != 0 - - val rightEarBattery = - if (responseData.size > 1) responseData[1].toInt() and 0x7F else 0 - val rightEarCharging = - if (responseData.size > 1) (responseData[1].toInt() and 0x80) != 0 else false - - val caseBattery = - if (responseData.size > 2) responseData[2].toInt() and 0x7F else 0 - val caseCharging = - if (responseData.size > 2) (responseData[2].toInt() and 0x80) != 0 else false - - // 添加到结果Map - resultMap["leftEarBattery"] = leftEarBattery - resultMap["leftEarCharging"] = leftEarCharging - resultMap["rightEarBattery"] = rightEarBattery - resultMap["rightEarCharging"] = rightEarCharging - resultMap["caseBattery"] = caseBattery - resultMap["caseCharging"] = caseCharging - resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - - Log.i( - TAG, "接收到电量信息: 左耳=${leftEarBattery}% " + - "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + - "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + - "(${if (caseCharging) "充电中" else "未充电"})" - ) - } - - notifyDeviceInfoReceived(cmdType, resultMap) - } - - BleConst.CMD_CONTROL_CODEC -> { - val resultMap = mutableMapOf( - "success" to true, - "command" to cmdType, - "rawData" to responseData - ) - - if (responseData.isNotEmpty() && responseData.size >= 2) { - val codecStatus = responseData[0].toInt() and 0xFF - val channelMode = responseData[1].toInt() and 0xFF - - // 解码状态描述 - val statusDesc = when (codecStatus) { - BleConst.CODEC_CONTROL_CLOSE -> "已关闭编解码" - BleConst.CODEC_CONTROL_DECODE_ON -> "已打开解码" - BleConst.CODEC_CONTROL_A2DP_PLAY -> "A2DP播放模式" - BleConst.CODEC_CONTROL_CALL_RECORD_PLAY -> "通话记录播放模式" - BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码" - else -> "未知状态($codecStatus)" - } - if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON || - codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY || - codecStatus == BleConst.CODEC_CONTROL_CALL_RECORD_PLAY || - codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON - ) { - recordfile1!!.closeFile() - recordfile1!!.creatingFiles() - recordfile!!.closeFile() - recordfile!!.creatingFiles() - } else if (codecStatus == BleConst.CODEC_CONTROL_CLOSE) { - recordfile!!.closeFile() - recordfile1!!.closeFile() - } - Log.d(TAG, "收到设备响应,channelMode: $channelMode") - // 声道模式描述 - val channelDesc = when (channelMode) { - BleConst.AUDIO_CHANNEL_LEFT -> "左声道" - BleConst.AUDIO_CHANNEL_RIGHT -> "右声道" - BleConst.AUDIO_CHANNEL_STEREO -> "立体声" - else -> "未知声道($channelMode)" - } - - resultMap["codecStatus"] = codecStatus - resultMap["channelMode"] = channelMode - resultMap["statusDesc"] = statusDesc - resultMap["channelDesc"] = channelDesc - - - - Log.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") - }else if (responseData.isNotEmpty() && responseData.size == 1) { - val codecStatus = responseData[0].toInt() and 0xFF - if(codecStatus==0x00){ - Log.d( - "BleService", - "收到设备响应:失败,codecStatus: $codecStatus" - ) - val resultMap = mutableMapOf( - "success" to true, - "command" to "Error", - "rawData" to responseData - ) - } - } - - notifyDeviceInfoReceived(cmdType, resultMap) - } - - else -> { - // 对于未知命令,返回原始数据 - val resultMap = mapOf( - "success" to true, - "command" to cmdType, - "rawData" to responseData, - "isUnknown" to true - ) - Log.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}") - notifyDeviceInfoReceived(cmdType, resultMap) - } - } + private fun processAudioData(data: ByteArray) { + // 委托给Opus音频管理器处理 + opusAudioManager.processAudioData(data) } - /** - * 处理设备主动上报数据 - 帧头为0xCC - */ - private fun processDeviceNotification(data: ByteArray) { - Log.d( - TAG, - "收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" - ) - if (data.size < 3) { - Log.e(TAG, "设备主动上报数据格式错误:数据长度过短") - return - } - - val header = data[0] // 0xCC - val infoType = data[1].toInt() and 0xFF - val length = data[2].toInt() and 0xFF - - // 检查数据长度 - if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC - Log.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") - return - } - - // 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 - val payload = data.sliceArray(0 until data.size - 1) - val calculatedCrc = crc8Maxim(payload, payload.size) - val receivedCrc = data[data.size - 1].toInt() and 0xFF - - if (calculatedCrc.toInt() and 0xFF != receivedCrc) { - Log.e( - TAG, - "设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" - ) - return - } - - // 提取上报数据 - val notifyData = if (length > 0) ByteArray(length) else ByteArray(0) - if (length > 0) { - System.arraycopy(data, 3, notifyData, 0, length) - } - - Log.d( - TAG, - "收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${ - notifyData.joinToString(", ") { - "0x${ - (it.toInt() and 0xFF).toString(16) - }" - } - }" - ) - - // 根据上报类型处理 - when (infoType) { - BleConst.CMD_GET_BATTERY_INFO -> { // 电量信息 - val resultMap = mutableMapOf( - "success" to true, - "command" to infoType, - "rawData" to notifyData, - "isNotification" to true // 标记为主动上报 - ) - - if (notifyData.isNotEmpty() && notifyData.size >= 3) { - // 处理电池信息 - 左耳、右耳、充电仓 - val leftEarBattery = notifyData[0].toInt() and 0x7F - val leftEarCharging = (notifyData[0].toInt() and 0x80) != 0 - - val rightEarBattery = - if (notifyData.size > 1) notifyData[1].toInt() and 0x7F else 0 - val rightEarCharging = - if (notifyData.size > 1) (notifyData[1].toInt() and 0x80) != 0 else false - - val caseBattery = if (notifyData.size > 2) notifyData[2].toInt() and 0x7F else 0 - val caseCharging = - if (notifyData.size > 2) (notifyData[2].toInt() and 0x80) != 0 else false - - - // 添加到结果Map - resultMap["leftEarBattery"] = leftEarBattery - resultMap["leftEarCharging"] = leftEarCharging - resultMap["rightEarBattery"] = rightEarBattery - resultMap["rightEarCharging"] = rightEarCharging - resultMap["caseBattery"] = caseBattery - resultMap["caseCharging"] = caseCharging - resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - - Log.i( - TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " + - "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + - "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + - "(${if (caseCharging) "充电中" else "未充电"})" - ) - } - notifyDeviceInfoReceived(infoType, resultMap) - } - - BleConst.CMD_WAKE_UP -> { - Log.i(TAG, "收到唤醒指令") - // 接收到唤醒信号,打开编码器 (设备侧) - - notifyWakeupSignalReceived() - - } - // 可以在这里添加其他主动上报类型的处理 - else -> { - // 对于未知上报类型,返回原始数据 - val resultMap = mapOf( - "success" to true, - "command" to infoType, - "rawData" to notifyData, - "isUnknown" to true, - "isNotification" to true // 标记为主动上报 - ) - Log.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}") - notifyDeviceInfoReceived(infoType, resultMap) - } - } - } // ====================================================================================================== - // Opus 解码相关 + // Opus 解码相关 - 委托给OpusAudioManager // ====================================================================================================== + /** + * 开始Opus数据流解码 + * @param hasHeader 是否包含文件头,通常BLE传输的Opus没有文件头 + * @param channel 音频通道数 + * @param sampleRate 采样率,确认设备端Opus编码采样率 + * @param packetSize 包大小,确认设备端Opus编码帧长 + * @return 是否成功开始解码 + */ public fun startOpusStreamDecoding( - hasHeader: Boolean = false, // 通常BLE传输的Opus没有文件头 + hasHeader: Boolean = false, channel: Int = 1, - sampleRate: Int = 16000, // 确认设备端Opus编码采样率 - packetSize: Int = 40 // 确认设备端Opus编码帧长,必须与iOS版本frameSize保持一致 + sampleRate: Int = 16000, + packetSize: Int = 40 ): Boolean { - if (opusManager == null) { - Log.e(TAG, "OpusManager未初始化,无法开始解码") - return false - } - // 如果已经在解码流,先停止 - if (opusManager?.isDecodeStream == true) { - opusManager?.stopDecodeStream() - Log.d(TAG, "已停止正在进行的Opus解码流") - } - - // 清理音频数据缓存,确保开始时是干净的状态 - synchronized(audioDataBuffer) { - audioDataBuffer.clear() - Log.d(TAG, "开始解码前已清理音频数据缓存") - } - - option!!.setHasHead(hasHeader) - option!!.setChannel(channel) - option!!.setSampleRate(sampleRate) - option!!.setPacketSize(packetSize) - - Log.d(TAG, "准备开始Opus数据流解码, 参数: $option") - - var streamStartedSuccessfully = false - opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { - override fun onDecodeStream(data: ByteArray?) { - if (data != null) { - if (option!!.getChannel() >= 1) { - notifyAudioDataReceived(data,option!!.getChannel()) - } else { - Log.e(TAG, "Opus解码数据错误: ${data.size} bytes") - } - } - } - - override fun onStart() { - streamStartedSuccessfully = true - Log.i(TAG, "Opus数据流解码已开始") - // 可以通过回调通知上层解码已开始 - } - - override fun onComplete(outPath: String?) { - Log.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)") - } - - override fun onError(code: Int, message: String?) { - Log.e(TAG, "Opus数据流解码错误: [$code] $message") - // 可以通过回调通知上层解码错误 - } - }) - // 注意:startDecodeStream是异步启动的,这里的返回值可能不准确反映是否真的成功启动。 - // onStart回调才是真正启动成功的标志。为了简单,我们暂时认为调用即尝试启动。 - // 后续可以改造为返回Future或通过回调传递启动结果。 - return true // 暂定为调用即成功 + return opusAudioManager.startDecodeStream(hasHeader, channel, sampleRate, packetSize) } + /** + * 停止Opus数据流解码 + * @return 是否成功停止解码 + */ private fun stopOpusStreamDecoding(): Boolean { - if (opusManager?.isDecodeStream == true) { - opusManager?.stopDecodeStream() - - // 清理音频数据缓存 - synchronized(audioDataBuffer) { - audioDataBuffer.clear() - Log.d(TAG, "已清理音频数据缓存") - } - - Log.i(TAG, "已停止Opus数据流解码") - return true - } - Log.d(TAG, "Opus数据流未在解码或OpusManager未初始化") - return false + return opusAudioManager.stopDecodeStream() } -// ====================================================================================================== - // Opus 编码相关 + // ====================================================================================================== + // Opus 编码相关 - 委托给OpusAudioManager // ====================================================================================================== + /** + * 开始Opus数据流编码 + * @return 是否成功开始编码 + */ private fun startOpusEncodeStream(): Boolean { - if (opusManager == null) { - Log.e(TAG, "OpusManager未初始化,无法开始编码") - return false - } - - // 如果已经在编码流,先停止 - if (opusManager?.isEncodeStream == true) { - Log.d(TAG, "Opus编码流已在运行") - return true - } - - // 启动音频发送线程 - startAudioSendThread() - - var streamStartedSuccessfully = false - opusManager?.startEncodeStream(object : OnEncodeStreamCallback { - override fun onEncodeStream(data: ByteArray?) { - if (data != null) { - // 编码完成的数据处理: - // 1. 保存到WAV文件 - recordfile1?.saveAudioDataToWav(data) - // 2. 将编码后的数据加入发送队列进行分块发送 - addAudioDataToSendQueue(data) - } else { - Log.w(TAG, "编码回调收到空数据") - } - } - - override fun onStart() { - streamStartedSuccessfully = true - Log.i(TAG, "Opus数据流编码已开始") - } - - override fun onComplete(outPath: String?) { - Log.i(TAG, "Opus数据流编码完成: $outPath") - } - - override fun onError(code: Int, message: String?) { - Log.e(TAG, "Opus数据流编码错误: [$code] $message") - } - }) - - return true + startAudioSendThread() + return opusAudioManager.startEncodeStream() } + /** + * 停止Opus数据流编码 + * @return 是否成功停止编码 + */ private fun stopOpusEncodeStream(): Boolean { - if (opusManager?.isEncodeStream == true) { - opusManager?.stopEncodeStream() - stopAudioSendThread() // 停止音频发送线程 - Log.i(TAG, "已停止Opus数据流编码") - return true - } - Log.d(TAG, "Opus数据流未在编码或OpusManager未初始化") - return false + stopAudioSendThread() + return opusAudioManager.stopEncodeStream() } /** @@ -1427,8 +1102,9 @@ startBytesStatistics() } } + /** - * 启动音频数据发送线程 + * 启动音频数据发送线程(优化版本) */ private fun startAudioSendThread() { if (audioSendThread?.isAlive == true) { @@ -1446,10 +1122,8 @@ startBytesStatistics() val audioChunk = audioSendQueue.poll(100, TimeUnit.MILLISECONDS) if (audioChunk != null) { - // 发送音频数据块 - sendAudioChunk(audioChunk) - - // 控制发送频率,避免蓝牙缓冲区溢出 + // 发送音频数据块并等待回调 + sendAudioChunkWithCallback(audioChunk) Thread.sleep(AUDIO_SEND_INTERVAL) } } catch (e: InterruptedException) { @@ -1478,328 +1152,328 @@ startBytesStatistics() } /** - * 发送音频数据块到设备(带重发机制) + * 发送音频数据块并等待写入回调(新方法) * @param chunk 要发送的音频数据块 - * @param retryCount 当前重试次数 */ - private fun sendAudioChunk(chunk: ByteArray, retryCount: Int = 0) { + private fun sendAudioChunkWithCallback(chunk: ByteArray) { try { if (callWriteChar == null || bluetoothGatt == null) { Log.e(TAG, "蓝牙连接或特征值未准备就绪") return } + // // 检查数据块大小是否超过当前MTU限制 + // val maxDataSize = currentMtu - 5 // 减去ATT头部5字节 + // if (chunk.size > maxDataSize) { + // Log.w(TAG, "数据块过大 (${chunk.size} > $maxDataSize),分片发送") + // // 分片发送 + // sendChunksInFragments(chunk, maxDataSize) + // return + // } + // 在主线程中执行蓝牙写入操作 audioSendHandler.post { try { - callWriteChar?.value = chunk - val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) - - if (isSuccess == true) { - // 累加发送成功的字节数 - bytesSentInCurrentSecond += chunk.size - Log.d(TAG, "成功发送音频数据块,大小: ${chunk.size} 字节") - } else { - Log.e(TAG, "发送音频数据块失败,大小: ${chunk.size} 字节") - // 详细的失败原因分析 - val failureReason = StringBuilder("发送音频数据块失败,大小: ${chunk.size} 字节") + + isWaitingForWriteCallback = true - // 检查蓝牙连接状态 - if (bluetoothGatt == null) { - failureReason.append(" - 原因: BluetoothGatt为空") - } else { - // 检查连接状态 - val State = bluetoothManager.getConnectionState( - bluetoothGatt!!.device, - BluetoothProfile.GATT - ) - when (State) { - BluetoothProfile.STATE_DISCONNECTED -> { - failureReason.append(" -发送失败 原因: 设备已断开连接") - } - BluetoothProfile.STATE_CONNECTING -> { - failureReason.append(" -发送失败 原因: 设备正在连接中") - } - BluetoothProfile.STATE_DISCONNECTING -> { - failureReason.append(" -发送失败 原因: 设备正在断开连接") - } - BluetoothProfile.STATE_CONNECTED -> { - // 连接正常,检查其他原因 - if (callWriteChar == null) { - failureReason.append(" -发送失败 原因: 写入特征值为空") - } else { - // 检查特征值属性 - val properties = callWriteChar!!.properties - if ((properties and BluetoothGattCharacteristic.PROPERTY_WRITE) == 0 && - (properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) { - failureReason.append(" -发送失败 原因: 特征值不支持写入操作") - } else if (chunk.size > 512) { // BLE MTU通常限制 - failureReason.append(" -发送失败 原因: 数据块过大 (${chunk.size} > 512字节)") - } else { - failureReason.append(" -发送失败 原因: 未知错误,可能是设备忙碌或缓冲区满") - } - } - } - else -> { - failureReason.append(" -发送失败 原因: 未知连接状态($connectionState)") - } - } - } + callWriteChar?.value = chunk + recordfile1?.saveAudioDataAsUtf8Hex(chunk, true) + val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) - // 检查蓝牙适配器状态 - if (bluetoothAdapter?.isEnabled != true) { - failureReason.append(" - 蓝牙适配器未启用") + if (isSuccess == true) { + isWaitingForWriteCallback = false + recordfile1?.saveAudioDataToWav("发送成功,大小: ${chunk.size} 字节") + bytesSentInCurrentSecond += chunk.size + Log.d(TAG, "成功发送音频数据块,大小: ${chunk.size} 字节,等待写入回调...") + } else { + isWaitingForWriteCallback = false + recordfile1?.saveAudioDataToWav("发送失败,大小: ${chunk.size} 字节") + Log.e(TAG, "发送音频数据块失败,大小: ${chunk.size} 字节") + // analyzeWriteFailure(chunk) } - - Log.e(TAG, failureReason.toString()) - - // // 实现重发机制 - // if (retryCount < MAX_RETRY_COUNT) { - // Log.w(TAG, "准备重发音频数据块,重试次数: ${retryCount + 1}/$MAX_RETRY_COUNT") - - // // 延迟后重发 - // audioSendHandler.postDelayed({ - // val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) - // if (isSuccess == true) { - // Log.d(TAG, "重发音频数据块成功") - // } else { - // Log.e(TAG, "重发音频数据块失败") - // } - // }, RETRY_DELAY) - // } else { - // Log.e(TAG, "音频数据块发送失败,已达到最大重试次数($MAX_RETRY_COUNT),丢弃数据块") - // // 可以选择将失败的数据块记录到日志或进行其他处理 - // } - } } catch (e: Exception) { + Log.e(TAG, "发送音频数据块异常: ${e.message}", e) - - // // 异常情况下也进行重发 - // if (retryCount < MAX_RETRY_COUNT) { - // Log.w(TAG, "发送异常,准备重发音频数据块,重试次数: ${retryCount + 1}/$MAX_RETRY_COUNT") - // audioSendHandler.postDelayed({ - // val isSuccess = bluetoothGatt?.writeCharacteristic(callWriteChar) - - // if (isSuccess == true) { - // Log.d(TAG, "重发音频数据块成功") - // } else { - // Log.e(TAG, "重发音频数据块失败") - // } - // }, RETRY_DELAY) - // } else { - // Log.e(TAG, "音频数据块发送异常,已达到最大重试次数($MAX_RETRY_COUNT),丢弃数据块") - // } } } } catch (e: Exception) { Log.e(TAG, "发送音频数据块异常: ${e.message}", e) } } + + /** + * 分片发送大数据块 + * @param data 原始数据 + * @param maxChunkSize 每片的最大大小 + */ + private fun sendChunksInFragments(data: ByteArray, maxChunkSize: Int) { + var offset = 0 + while (offset < data.size) { + val remainingSize = data.size - offset + val chunkSize = minOf(remainingSize, maxChunkSize) + val fragment = data.copyOfRange(offset, offset + chunkSize) + + // 递归调用发送单个片段 + sendAudioChunkWithCallback(fragment) + + offset += chunkSize + } + } + + /** + * 分析写入失败的原因(从原sendAudioChunk方法提取) + * @param chunk 失败的数据块 + */ + private fun analyzeWriteFailure(chunk: ByteArray) { + val failureReason = StringBuilder("发送音频数据块失败,大小: ${chunk.size} 字节") + + // 检查蓝牙连接状态 + if (bluetoothGatt == null) { + failureReason.append(" - 原因: BluetoothGatt为空") + } else { + // 检查连接状态 + val state = bluetoothManager.getConnectionState( + bluetoothGatt!!.device, + BluetoothProfile.GATT + ) + when (state) { + BluetoothProfile.STATE_DISCONNECTED -> { + failureReason.append(" -发送失败 原因: 设备已断开连接") + } + BluetoothProfile.STATE_CONNECTING -> { + failureReason.append(" -发送失败 原因: 设备正在连接中") + } + BluetoothProfile.STATE_DISCONNECTING -> { + failureReason.append(" -发送失败 原因: 设备正在断开连接") + } + BluetoothProfile.STATE_CONNECTED -> { + // 连接正常,检查其他原因 + if (callWriteChar == null) { + failureReason.append(" -发送失败 原因: 写入特征值为空") + } else { + // 检查特征值属性 + val properties = callWriteChar!!.properties + if ((properties and BluetoothGattCharacteristic.PROPERTY_WRITE) == 0 && + (properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) { + failureReason.append(" -发送失败 原因: 特征值不支持写入操作") + } else if (chunk.size > currentMtu - 5) { + failureReason.append(" -发送失败 原因: 数据块过大 (${chunk.size} > ${currentMtu - 5}字节)") + } else { + failureReason.append(" -发送失败 原因: 未知错误,可能是设备忙碌或缓冲区满") + } + } + } + else -> { + failureReason.append(" -发送失败 原因: 未知连接状态($state)") + } + } + } + + // 检查蓝牙适配器状态 + if (bluetoothAdapter?.isEnabled != true) { + failureReason.append(" - 蓝牙适配器未启用") + } + + Log.e(TAG, failureReason.toString()) + } // ====================================================================================================== - // 公开的命令接口 + // 设备控制方法 - 使用新的指令发送器 // ====================================================================================================== + /** - * 获取耳机版本号 + * 获取设备版本信息 + * @return 是否成功发送命令 */ fun getDeviceVersion(): Boolean { - Log.i(TAG, "获取耳机版本号...") - return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte())) + Log.i(TAG, "获取设备版本信息...") + return commandSender.sendCommand(BleConst.CMD_GET_VERSION.toByte()) } /** * 获取产品ID + * @return 是否成功发送命令 */ fun getProductId(): Boolean { - Log.i(TAG, "获取耳机产品ID...") - return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte())) + Log.i(TAG, "获取产品ID...") + return commandSender.sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte()) } /** - * 获取耳机颜色ID + * 获取颜色ID + * @return 是否成功发送命令 */ fun getColorId(): Boolean { - Log.i(TAG, "获取耳机颜色ID...") - return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte())) + Log.i(TAG, "获取颜色ID...") + return commandSender.sendCommand(BleConst.CMD_GET_COLOR_ID.toByte()) } /** - * 获取耳机电量信息 + * 获取电量信息 + * @return 是否成功发送命令 */ fun getBatteryInfo(): Boolean { - Log.i(TAG, "获取耳机电量信息...") - return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte())) + Log.i(TAG, "获取电量信息...") + return commandSender.sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte()) } /** - * 控制编解码 - 关闭编解码 + * 关闭编解码 + * @return 是否成功发送命令 */ fun closeCodec(): Boolean { Log.i(TAG, "关闭编解码...") - - return sendCommand( - BleConst.CMD_CONTROL_CODEC.toByte(), - byteArrayOf( - BleConst.CODEC_CONTROL_CLOSE.toByte(), - - BleConst.AUDIO_CHANNEL_RIGHT.toByte() - ) // 或其他默认声道 + val cmdData = byteArrayOf( + BleConst.CODEC_CONTROL_CLOSE.toByte(), + BleConst.AUDIO_CHANNEL_STEREO.toByte() ) + + return commandSender.sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), cmdData) } - fun closeCodec1(): Boolean { - recordfile1!!.closeFile() - - recordfile!!.closeFile() - return true - } + /** - * 控制编解码 - 打开解码 + * 打开解码器 + * @param channelMode 声道模式 + * @return 是否成功发送命令 */ - fun openDecoder(): Boolean { - // 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. - // ) + fun openDecoder(channelMode: Int = BleConst.AUDIO_CHANNEL_STEREO): Boolean { + Log.i(TAG, "打开解码器...") startOpusStreamDecoding(false, 1, 16000, 40) - - Log.i(TAG, "打开解码0xA1") - return sendCommand( - BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( - BleConst.CODEC_CONTROL_DECODE_ON.toByte(), - - BleConst.AUDIO_CHANNEL_RIGHT.toByte() - ) + val cmdData = byteArrayOf( + BleConst.CODEC_CONTROL_DECODE_ON.toByte(), + channelMode.toByte() ) + return commandSender.sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), cmdData) } /** - * 控制编解码 - 打开解码 + * 打开A2DP解码器 + * @param channelMode 声道模式 + * @return 是否成功发送命令 */ - fun openA2DPDecoder(): Boolean { - Log.i(TAG, "打开编码 0xA2") + fun openA2DPDecoder(channelMode: Int = BleConst.AUDIO_CHANNEL_STEREO): Boolean { + Log.i(TAG, "打开A2DP解码器...") startOpusEncodeStream() - //双声道 80字节 - startOpusStreamDecoding(false, 2, 16000, 80) - // Log.i(TAG, "打开解码...") - return sendCommand( - BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( - BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), - BleConst.AUDIO_CHANNEL_RIGHT.toByte() - ) + startOpusStreamDecoding(false, 2, 16000, 80) + val cmdData = byteArrayOf( + BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), + channelMode.toByte() ) + return commandSender.sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), cmdData) } + /** - * 控制编解码 - 打开解码 + * 打开通话录音解码器 + * @param channelMode 声道模式 + * @return 是否成功发送命令 */ - fun openCallRecordDecoder(): Boolean { - Log.i(TAG, "打开编码 0xA3") - //双声道 80字节 + fun openCallRecordDecoder(channelMode: Int = BleConst.AUDIO_CHANNEL_STEREO): Boolean { + Log.i(TAG, "打开通话录音解码器...") startOpusStreamDecoding(false, 2, 16000, 80) - // Log.i(TAG, "打开解码...") - return sendCommand( - BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( - BleConst.CODEC_CONTROL_CALL_RECORD_PLAY.toByte(), - - BleConst.AUDIO_CHANNEL_RIGHT.toByte() - ) + val cmdData = byteArrayOf( + BleConst.CODEC_CONTROL_CALL_RECORD_PLAY.toByte(), + channelMode.toByte() ) + return commandSender.sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), cmdData) } /** - * 控制编解码 - A2DP播放 + * A2DP播放 + * @return 是否成功发送命令 */ fun playA2DP(): Boolean { - Log.i(TAG, "切换至A2DP播放模式...") - return sendCommand(BleConst.CMD_PLAY_A2DP.toByte()) + Log.i(TAG, "A2DP播放...") + return commandSender.sendCommand(BleConst.CMD_PLAY_A2DP.toByte()) } - /** - * 控制编解码 - 打开编码 + * 打开编码器 + * @param channelMode 声道模式 + * @return 是否成功发送命令 */ - fun openEncoder(): Boolean { - Log.i(TAG, "打开编码0xB1") - recordfile1!!.closeFile() - recordfile1!!.creatingFiles() - recordfile!!.closeFile() - recordfile!!.creatingFiles() + fun openEncoder(channelMode: Int = BleConst.AUDIO_CHANNEL_STEREO): Boolean { + Log.i(TAG, "打开编码器...") startOpusStreamDecoding(false, 1, 16000, 40) - // // 目前仅发送命令通知设备开始编码。 - // return sendCommand( - // BleConst.CMD_CONTROL_CODEC.toByte(), - // byteArrayOf( - // BleConst.CODEC_CONTROL_ENCODE_ON.toByte(), - // BleConst.AUDIO_CHANNEL_RIGHT.toByte() // 或设备期望的声道 - // ) + // val cmdData = byteArrayOf( + // BleConst.CODEC_CONTROL_ENCODE_ON.toByte(), + // channelMode.toByte() // ) + // return commandSender.sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), cmdData) return true } - + /** + * 打开编码器 + * @param channelMode 声道模式 + * @return 是否成功发送命令 + */ + fun openB1Encoder(channelMode: Int = BleConst.AUDIO_CHANNEL_STEREO): Boolean { + Log.i(TAG, "打开B1编码器...") + startOpusStreamDecoding(false, 1, 16000, 40) + val cmdData = byteArrayOf( + BleConst.CODEC_CONTROL_ENCODE_ON.toByte(), + channelMode.toByte() + ) + return commandSender.sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), cmdData) + } /** * 设置声道模式 - * @param channelMode 声道模式:AUDIO_CHANNEL_LEFT/AUDIO_CHANNEL_RIGHT/AUDIO_CHANNEL_STEREO + * @param channelMode 声道模式 + * @return 是否成功发送命令 */ fun setChannelMode(channelMode: Int): Boolean { - when (channelMode) { - BleConst.AUDIO_CHANNEL_LEFT -> Log.i(TAG, "设置左声道模式...") - BleConst.AUDIO_CHANNEL_RIGHT -> Log.i(TAG, "设置右声道模式...") - BleConst.AUDIO_CHANNEL_STEREO -> Log.i(TAG, "设置立体声模式...") - else -> { - Log.e(TAG, "无效的声道模式: $channelMode") - return false - } - } - - // 高级控制:设置命令类型、长度和两个参数 - return sendCommand( - BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( - 0xA1.toByte(), // 状态码 - 使用解码状态 - channelMode.toByte() // 声道模式 - ) + Log.i(TAG, "设置声道模式: $channelMode") + val cmdData = byteArrayOf( + BleConst.CODEC_CONTROL_ENCODE_ON.toByte(), + channelMode.toByte() ) + return commandSender.sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), cmdData) } /** * 音量增大 + * @return 是否成功发送命令 */ fun volumeUp(): Boolean { Log.i(TAG, "设备音量增加...") - return sendCommand(BleConst.CMD_VOLUME_UP.toByte()) + return commandSender.sendCommand(BleConst.CMD_VOLUME_UP.toByte()) } /** * 音量减小 + * @return 是否成功发送命令 */ fun volumeDown(): Boolean { Log.i(TAG, "设备音量减小...") - return sendCommand(BleConst.CMD_VOLUME_DOWN.toByte()) + return commandSender.sendCommand(BleConst.CMD_VOLUME_DOWN.toByte()) } // ====================================================================================================== - // 工具方法 + // 连接状态和工具方法 // ====================================================================================================== + /** - * 检查是否已连接 + * 检查连接状态 + * @return 是否已连接 */ - fun isConnected(): Boolean { - return connectionState.value == BleConst.STATE_CONNECTED + private fun checkConn(): Boolean { + return bluetoothGatt != null && + connectionState.value == BleConst.STATE_CONNECTED && + writeChar != null } /** - * 连接状态检查 + * 是否已连接 + * @return 连接状态 */ - private fun checkConn(): Boolean = - bluetoothGatt != null && writeChar != null && callWriteChar != null && - connectionState.value == BleConst.STATE_CONNECTED + fun isConnected(): Boolean { + return connectionState.value == BleConst.STATE_CONNECTED + } /** - * 获取已连接设备地址 + * 获取已连接设备的地址 + * @return 设备地址,未连接时返回null */ fun getConnectedDeviceAddress(): String? { return bluetoothGatt?.device?.address @@ -1807,156 +1481,91 @@ startBytesStatistics() /** * 获取蓝牙适配器 + * @return 蓝牙适配器实例 */ fun getBluetoothAdapter(): BluetoothAdapter? { return bluetoothAdapter } + + /** + * 获取当前MTU值 + * @return 当前MTU值 + */ + fun getCurrentMtu(): Int { + return currentMtu + } + + /** + * 获取可用的数据传输大小(MTU减去头部) + * @return 可用的数据传输大小 + */ + fun getAvailableDataSize(): Int { + return currentMtu - 5 // 减去ATT头部5字节 + } + + /** + * 手动请求MTU协商 + * @param mtu 请求的MTU值 + * @return 是否成功发起请求 + */ + fun requestMtu(mtu: Int): Boolean { + return bluetoothGatt?.let { gatt -> + try { + val result = gatt.requestMtu(mtu.coerceIn(23, 517)) + Log.i(TAG, "手动请求MTU $mtu: $result") + result + } catch (e: Exception) { + Log.e(TAG, "手动请求MTU异常: ${e.message}", e) + false + } + } ?: false + } // ====================================================================================================== // 资源清理 // ====================================================================================================== /** - * 清理所有资源 + * 清理资源 */ fun cleanup() { + Log.i(TAG, "清理BLE服务资源...") + + // 重置指令发送器 + commandSender.reset() + + // 停止扫描 stopScan() - scanHandler.removeCallbacksAndMessages(null) - disconnectGatt() - // 停止音频队列处理线程 - audioQueueProcessorThread?.interrupt() - audioQueueProcessorThread = null - audioDataQueue.clear() + + // 断开连接 + disconnect() + + // 停止统计 + stopBytesStatistics() + + // 清理Opus音频管理器 + opusAudioManager.cleanup() // 停止音频发送线程 stopAudioSendThread() - - // 清理音频数据缓存 - synchronized(audioDataBuffer) { - audioDataBuffer.clear() - Log.d(TAG, "已清理音频数据缓存") - } - - Log.d(TAG, "音频解码线程和队列处理线程已清理") - - // 释放OpusManager - stopOpusEncodeStream() + + // 清理回调 + clearCallbacks() + + // 清理录音文件 + recordfile = null + recordfile1 = null + // 停止音频处理 + stopAudioSendThread() stopOpusStreamDecoding() - opusManager?.release() - opusManager = null - option = null - Log.d(TAG, "OpusManager已释放") - isInitialized = false // 标记为未初始化 + stopOpusEncodeStream() + + isInitialized = false + Log.d(TAG, "BLE服务资源清理完成") } - // ====================================================================================================== - // 协议命令实现 - // ====================================================================================================== - /** - * 发送命令到设备 - 通用方法 - * @param cmdType 命令类型 - * @param cmdData 命令数据 - * @return 是否成功发送 - */ - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { - if (!checkConn()) { - Log.e(TAG, "发送命令失败: 设备未连接") - return false - } - - try { - // 计算命令长度 - val cmdLength = cmdData.size - - // 创建命令包 - val cmdPacket = ByteArray(4 + cmdLength) // 帧头 + 命令 + 长度 + 数据 + CRC - - // 帧头 - cmdPacket[0] = BleConst.APP_REQUEST_HEADER - // 命令类型 - cmdPacket[1] = cmdType - // 命令长度 - cmdPacket[2] = cmdLength.toByte() - - // 填充命令数据(如果有) - if (cmdLength > 0) { - System.arraycopy(cmdData, 0, cmdPacket, 3, cmdLength) - } - // 计算CRC - 对帧头到数据的所有字节进行校验 (不包括CRC自身) - val crcPayload = cmdPacket.sliceArray(0 until cmdPacket.size - 1) - val crc = crc8Maxim(crcPayload, crcPayload.size) - - // 设置CRC - cmdPacket[3 + cmdLength] = crc - - // 记录命令详情 - Log.d( - TAG, - "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " + - "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + - "CRC=0x${(crc.toInt() and 0xFF).toString(16)}" - ) - - // 发送数据 - writeChar!!.value = cmdPacket - // 加入队列并触发发送 - - if (isReply) { - val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar) - if (isSuccess == true) { - isReply = false - replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时 - return true - } else { - commandQueue.poll() - return false - } - } else { - commandQueue.offer(cmdPacket) - } - return true - - } catch (e: Exception) { - Log.e(TAG, "发送命令异常: ${e.message}", e) - return false - } - } - - private fun processNextCommand() { - if (!commandQueue.isEmpty()) { - val nextCmd = commandQueue.poll() - writeChar!!.value = nextCmd - val isSuccess = bluetoothGatt?.writeCharacteristic(writeChar) - if (isSuccess == true) { - isReply = false - replyTimeoutHandler.postDelayed(replyTimeoutRunnable, 1000) // 设置1秒超时 - Log.d(TAG, "发送队列中的下一个命令") - } - } - } - - /** - * 计算CRC8/MAXIM校验值 - */ - private fun crc8Maxim(data: ByteArray, length: Int): Byte { - var crc = 0 - val polynomial = 0x31 - - for (i in 0 until length) { - crc = crc xor (data[i].toInt() and 0xFF) - for (j in 0 until 8) { - if (crc and 0x80 != 0) { - crc = ((crc shl 1) xor polynomial) and 0xFF - } else { - crc = (crc shl 1) and 0xFF - } - } - } - return crc.toByte() - } /** * 向所有回调监听器分发扫描结果 @@ -2046,128 +1655,6 @@ startBytesStatistics() } } - /** - * 启动音频队列处理 - */ - private fun startAudioQueueProcessing() { - audioQueueProcessorThread = Thread { - while (!Thread.currentThread().isInterrupted) { - try { - // 从队列中取出音频数据进行解码 - val audioData = audioDataQueue.take() // 阻塞等待数据 - - opusManager?.writeAudioStream(audioData) - } catch (e: InterruptedException) { - Log.d(TAG, "音频队列处理线程被中断") - break - } catch (e: Exception) { - Log.e(TAG, "音频队列处理异常: ${e.message}", e) - } - } - }.apply { - name = "AudioQueueProcessor" - start() - } - } - - } -class RecordingFile(private val context: Context) { - private var currentAudioFile: File? = null - private var fos: FileOutputStream? = null - - // 新增:用于异步写入的队列和线程 - private val writeQueue = LinkedBlockingQueue() - private val isWriting = AtomicBoolean(false) - private var writeThread: Thread? = null - - // 用于存储音频数据的缓冲区 - private val dataBuffer = mutableListOf() - private var totalBytesWritten = 0 - - var fileName = "" - internal fun creatingFiles() { - if (fos != null || currentAudioFile != null) { - return - } - // 创建新的音频文件 - val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) - val timestamp = dateFormat.format(Date()) - - val filePath = - File(context.getExternalFilesDir(null), "${fileName}_${timestamp}.opus").absolutePath - currentAudioFile = File(filePath) - currentAudioFile?.createNewFile() - // 追加音频数据到文件 - fos = FileOutputStream(currentAudioFile, true) - startWriteThread() // 新增:启动写入线程 - Log.d("TAG", "创建文件: ${currentAudioFile?.absolutePath}") - } - - // 新增:启动写入线程 - private fun startWriteThread() { - if (isWriting.get()) return - isWriting.set(true) - writeThread = Thread { - try { - while (isWriting.get() || writeQueue.isNotEmpty()) { - val data = writeQueue.poll() ?: continue - fos?.write(data) - } - } catch (e: Exception) { - Log.e("", "异步写入音频数据失败: ${e.message}") - } - } - writeThread?.start() - } - - /** - * 保存音频数据到 opus 文件(异步) - */ - internal fun saveAudioDataToWav(buffer: ByteArray) { - if (fos == null || currentAudioFile == null) return - // 放入队列,由写线程写入 - // 1. 将字节数据转换为十六进制字符串 - // val hexData = buildString { - // buffer.forEachIndexed { index, byte -> - // append("%02X".format(byte)) - // } - // } - - // 2. 写入十六进制字符串 - - writeQueue.offer(buffer) - totalBytesWritten += buffer.size - } - - - internal fun closeFile() { - try { - if (fos == null || currentAudioFile == null) { - return - } - isWriting.set(false) - writeThread?.join(500) // 等待写线程结束 - fos?.close() - - // 更新WAV文件头 - currentAudioFile?.let { file -> - // 如果文件小于44字节,直接删除 - if (file.length() <= 0) { - file.delete() - Log.d("TAG", "音频文件过小已删除: ${file.absolutePath}") - } else { - Log.d("TAG", "音频文件保存完成: ${file.absolutePath}") - } - } - - } catch (e: Exception) { - Log.e("TAG", "更新opus失败: ${e.message}") - } finally { - fos = null - currentAudioFile = null - } - } -} 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 02d4c49d8..75c61ff6c 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 @@ -68,7 +68,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, BleService.addCallback(this) // 初始化CompanionManager - bleCompanionManager = BleCompanionManager(context) + bleCompanionManager = BleCompanionManager.getInstance(context) } // 设置事件通道 - 简化版 @@ -123,21 +123,21 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, // 实现ActivityResultListener接口 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - // 处理设备关联结果 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val handled = bleCompanionManager.handleAssociationResult(requestCode, resultCode, data) - if (handled) { - // 返回设备关联结果 - pendingAssociationResult?.success(true) - pendingAssociationResult = null - return true - } else { - pendingAssociationResult?.success(false) - pendingAssociationResult = null - return true - } - } - return false + // // 处理设备关联结果 + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // val handled = bleCompanionManager.handleAssociationResult(requestCode, resultCode, data) + // if (handled) { + // // 返回设备关联结果 + // pendingAssociationResult?.success(true) + // pendingAssociationResult = null + // return true + // } else { + // pendingAssociationResult?.success(false) + // pendingAssociationResult = null + // return true + // } + // } + return true } // 添加sendEvent函数 @@ -247,18 +247,18 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, return } - // 启动设备关联 - val success = bleCompanionManager.startAssociation( - currentActivity, - deviceNamePattern, - serviceUuid, - macAddress - ) + // // 启动设备关联 + // val success = bleCompanionManager.startAssociation( + // currentActivity, + // deviceNamePattern, + // serviceUuid, + // macAddress + // ) - if (!success) { - pendingAssociationResult = null - result.error("FAILED", "启动设备关联失败", null) - } + // if (!success) { + // pendingAssociationResult = null + // result.error("FAILED", "启动设备关联失败", null) + // } // 不要立即返回结果,等待onActivityResult回调 } "getPairedMacAddress" -> { @@ -304,7 +304,8 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, } "clearAssociations" -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - bleCompanionManager.clearAllAssociations() + bleCompanionManager.unregisterBackgroundScan() //停止后台扫描 + bleCompanionManager.clearPairedMacAddress() result.success(true) } else { result.error("UNSUPPORTED", "设备不支持Companion Device Manager", null) diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt index 78557535a..432f69b9c 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt @@ -37,32 +37,70 @@ class BleWakeReceiver : BroadcastReceiver() { } } + /** + * 启动BLE服务的改进方法 + * 处理Android系统对后台服务启动的限制 + */ private fun startBleService(context: Context) { try { - // 检查是否存在BleMediaService类,如果不存在则使用BleService + // 检查是否存在BleMediaService类 val serviceClass = try { + Log.d(TAG, "startBleService存在") Class.forName("com.yunqiinnovation.agent_service.BleMediaService") } catch (e: ClassNotFoundException) { Log.i(TAG, "BleMediaService不存在") return } - // 创建服务Intent,并携带MAC地址 + // 创建服务Intent val serviceIntent = Intent().apply { setClassName(context, serviceClass.name) action = BleConst.ACTION_SCAN_RESULT } - - // 启动前台服务 + + // 启动服务的改进策略 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(serviceIntent) - Log.i(TAG, "已启动前台服务: ${serviceClass.name}") + try { + // 首先尝试启动前台服务 + Log.i(TAG, "尝试启动前台服务: ${serviceClass.name}") + context.startForegroundService(serviceIntent) + Log.i(TAG, "前台服务启动成功") + } catch (e: IllegalStateException) { + Log.w(TAG, "前台服务启动失败,尝试其他方式: ${e.message}") + + // 如果前台服务启动失败,尝试通过Activity启动 + try { + Log.i(TAG, "通过Activity启动服务成功: ${serviceClass.name}") + val activityIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtra("auto_start_ble_service", true) + putExtra("ble_service_action", BleConst.ACTION_SCAN_RESULT) + } + + if (activityIntent != null) { + context.startActivity(activityIntent) + + Log.i(TAG, "通过Activity启动应用,将在前台启动服务") + } else { + Log.e(TAG, "无法获取应用启动Intent") + } + } catch (activityException: Exception) { + Log.e(TAG, "通过Activity启动也失败: ${activityException.message}") + } + } catch (e: Exception) { + Log.e(TAG, "启动前台服务时发生未知错误: ${e.message}", e) + } } else { - context.startService(serviceIntent) - Log.i(TAG, "已启动服务: ${serviceClass.name}") + // Android 8.0以下直接启动服务 + try { + context.startService(serviceIntent) + Log.i(TAG, "已启动服务: ${serviceClass.name}") + } catch (e: Exception) { + Log.e(TAG, "启动服务失败: ${e.message}", e) + } } } catch (e: Exception) { Log.e(TAG, "启动BLE服务失败: ${e.message}", e) } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt index d0a03afc4..4ee7223c8 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt @@ -12,15 +12,22 @@ class BootReceiver : BroadcastReceiver() { private val TAG = "BootReceiver" override fun onReceive(ctx: Context, i: Intent) { val serviceClass = try { + Log.d(TAG, "BleMediaService不存在") Class.forName("com.yunqiinnovation.agent_service.BleMediaService") } catch (e: ClassNotFoundException) { - Log.i(TAG, "BleMediaService不存在") + Log.d(TAG, "BleMediaService不存在") return } if (i.action == Intent.ACTION_BOOT_COMPLETED) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + Log.d(TAG, "BleMediaService") ctx.startForegroundService(Intent(ctx, serviceClass)) - else ctx.startService(Intent(ctx, serviceClass)) + } + else{ + Log.d(TAG, "BleMediaService") + ctx.startService(Intent(ctx, serviceClass)) + } } } } \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt new file mode 100644 index 000000000..110d502dc --- /dev/null +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt @@ -0,0 +1,506 @@ +package com.yunqiinnovation.ble_service + +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.jieli.jl_audio_decode.opus.model.OpusOption +import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback +import com.jieli.jl_audio_decode.callback.OnEncodeStreamCallback +import com.jieli.jl_audio_decode.opus.OpusManager +import com.jieli.jl_audio_decode.exceptions.OpusException +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Opus音频编解码管理器 + * 负责处理音频数据的Opus编码和解码功能 + * + * 功能包括: + * 1. Opus解码流管理 + * 2. Opus编码流管理 + * 3. 音频数据缓冲和处理 + * 4. 编解码状态管理 + */ +class OpusAudioManager { + + companion object { + private const val TAG = "OpusAudioManager" + + // 音频数据分块发送的常量 + private const val AUDIO_CHUNK_SIZE = 120 // 每次发送120字节 + private const val AUDIO_SEND_INTERVAL = 60L // 发送间隔60ms + + // 重发机制相关常量 + private const val MAX_RETRY_COUNT = 1 // 最大重试次数 + private const val RETRY_DELAY = 30L // 重试延迟时间(毫秒) + } + + // ====================================================================================================== + // 回调接口定义 + // ====================================================================================================== + + /** + * Opus音频管理器回调接口 + */ + interface OpusAudioCallback { + /** + * 音频数据解码完成回调 + * @param data 解码后的音频数据 + * @param channel 音频通道数 + */ + fun onAudioDataDecoded(data: ByteArray, channel: Int) + + /** + * 音频数据编码完成回调 + * @param data 编码后的音频数据 + */ + fun onAudioDataEncoded(data: ByteArray) + + /** + * 解码流状态变化回调 + * @param isStarted 是否已启动 + */ + fun onDecodeStreamStateChanged(isStarted: Boolean) + + /** + * 编码流状态变化回调 + * @param isStarted 是否已启动 + */ + fun onEncodeStreamStateChanged(isStarted: Boolean) + + /** + * 错误回调 + * @param errorType 错误类型 (decode/encode) + * @param code 错误代码 + * @param message 错误信息 + */ + fun onError(errorType: String, code: Int, message: String?) + } + + // ====================================================================================================== + // 成员变量 + // ====================================================================================================== + + // Opus解码器实例 + private var opusManager: OpusManager? = null + private var option: OpusOption? = null + + private var opusManager1: OpusManager? = null + private var option1: OpusOption? = null + private val mainHandler = Handler(Looper.getMainLooper()) + + // 解码音频数据队列和处理线程 + private val audioDataQueue = LinkedBlockingQueue() + private var audioQueueProcessorThread: Thread? = null + + // 音频数据缓存 + private val audioDataBuffer = mutableListOf() + + // 音频数据发送相关 + private val audioSendQueue = LinkedBlockingQueue() + private var audioSendThread: Thread? = null + private val audioSendHandler = Handler(Looper.getMainLooper()) + private val isAudioSending = AtomicBoolean(false) + + // 音频数据缓冲区,用于累积数据到指定字节再发送 + private val audioBuffer = mutableListOf() + + // 回调接口 + private var callback: OpusAudioCallback? = null + + // 初始化状态 + private var isInitialized = false + + // ====================================================================================================== + // 初始化和配置方法 + // ====================================================================================================== + + /** + * 初始化Opus音频管理器 + * @return 是否初始化成功 + */ + fun initialize(): Boolean { + if (isInitialized) return true + + try { + // 初始化OpusManager和OpusOption + opusManager = OpusManager() + option = OpusOption() + + opusManager1 = OpusManager() + option1 = OpusOption() + + // 启动队列处理 + startAudioQueueProcessing() + + isInitialized = true + Log.d(TAG, "OpusAudioManager初始化成功") + return true + } catch (e: OpusException) { + Log.e(TAG, "OpusAudioManager初始化失败: ${e.message}", e) + return false + } + } + + /** + * 设置回调接口 + * @param callback 回调接口实例 + */ + fun setCallback(callback: OpusAudioCallback?) { + this.callback = callback + } + + /** + * 清理资源 + */ + fun cleanup() { + Log.d(TAG, "开始清理OpusAudioManager资源") + + // 停止编解码流 + stopDecodeStream() + stopEncodeStream() + + // 停止音频发送线程 + stopAudioSendThread() + + // 停止音频队列处理线程 + audioQueueProcessorThread?.interrupt() + audioQueueProcessorThread = null + + // 清理缓存 + synchronized(audioDataBuffer) { + audioDataBuffer.clear() + } + synchronized(audioBuffer) { + audioBuffer.clear() + } + audioDataQueue.clear() + audioSendQueue.clear() + + // 清理Opus相关资源 + opusManager = null + option = null + opusManager1 = null + option1 = null + callback = null + + isInitialized = false + Log.d(TAG, "OpusAudioManager资源清理完成") + } + + // ====================================================================================================== + // Opus解码相关方法 + // ====================================================================================================== + + /** + * 开始Opus数据流解码 + * @param hasHeader 是否包含文件头,通常BLE传输的Opus没有文件头 + * @param channel 音频通道数 + * @param sampleRate 采样率,确认设备端Opus编码采样率 + * @param packetSize 包大小,确认设备端Opus编码帧长 + * @return 是否成功开始解码 + */ + fun startDecodeStream( + hasHeader: Boolean = false, + channel: Int = 1, + sampleRate: Int = 16000, + packetSize: Int = 40 + ): Boolean { + if (!isInitialized || opusManager == null) { + Log.e(TAG, "OpusManager未初始化,无法开始解码") + return false + } + + // 如果已经在解码流,先停止 + if (opusManager?.isDecodeStream == true) { + opusManager?.stopDecodeStream() + Log.d(TAG, "已停止正在进行的Opus解码流") + } + + // 清理音频数据缓存,确保开始时是干净的状态 + synchronized(audioDataBuffer) { + audioDataBuffer.clear() + Log.d(TAG, "开始解码前已清理音频数据缓存") + } + + // 配置解码参数 + option?.apply { + setHasHead(hasHeader) + setChannel(channel) + setSampleRate(sampleRate) + setPacketSize(packetSize) + } + + Log.d(TAG, "准备开始Opus数据流解码, 参数: hasHeader=$hasHeader, channel=$channel, sampleRate=$sampleRate, packetSize=$packetSize") + + try { + opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { + override fun onDecodeStream(data: ByteArray?) { + if (data != null) { + val channelCount = option?.getChannel() ?: 1 + if (channelCount >= 1) { + callback?.onAudioDataDecoded(data, channelCount) + } else { + // Log.e(TAG, "Opus解码数据错误: ${data.size} bytes") + } + } + } + + override fun onStart() { + Log.i(TAG, "Opus数据流解码已开始") + callback?.onDecodeStreamStateChanged(true) + } + + override fun onComplete(outPath: String?) { + Log.i(TAG, "Opus数据流解码完成: $outPath") + callback?.onDecodeStreamStateChanged(false) + } + + override fun onError(code: Int, message: String?) { + Log.e(TAG, "Opus数据流解码错误: [$code] $message") + callback?.onError("decode", code, message) + callback?.onDecodeStreamStateChanged(false) + } + }) + + return true + } catch (e: Exception) { + Log.e(TAG, "启动Opus解码流失败: ${e.message}", e) + callback?.onError("decode", -1, e.message) + return false + } + } + + /** + * 停止Opus数据流解码 + * @return 是否成功停止解码 + */ + fun stopDecodeStream(): Boolean { + if (opusManager?.isDecodeStream == true) { + opusManager?.stopDecodeStream() + + // 清理音频数据缓存 + synchronized(audioDataBuffer) { + audioDataBuffer.clear() + Log.d(TAG, "已清理音频数据缓存") + } + + Log.i(TAG, "已停止Opus数据流解码") + callback?.onDecodeStreamStateChanged(false) + return true + } + Log.d(TAG, "Opus数据流未在解码或OpusManager未初始化") + return false + } + + /** + * 处理接收到的音频数据进行解码 + * @param data 音频数据 (Opus编码) + */ + fun processAudioData(data: ByteArray) { + if (!isInitialized) { + Log.w(TAG, "OpusAudioManager未初始化,忽略音频数据") + return + } + + if (opusManager?.isDecodeStream == true) { + // 将数据加入队列进行异步处理 + audioDataQueue.offer(data) + } else { + Log.d(TAG, "Opus解码流未启动,忽略音频数据") + } + } + + // ====================================================================================================== + // Opus编码相关方法 + // ====================================================================================================== + + /** + * 开始Opus数据流编码 + * @return 是否成功开始编码 + */ + fun startEncodeStream(): Boolean { + if (!isInitialized || opusManager1 == null) { + Log.e(TAG, "OpusManager未初始化,无法开始编码") + return false + } + + // 如果已经在编码流,先停止 + if (opusManager1?.isEncodeStream == true) { + Log.d(TAG, "Opus编码流已在运行") + return true + } + + // 配置解码参数 + option1?.apply { + setHasHead(false) + setChannel(1) + setSampleRate(16000) + setPacketSize(40) + } + try { + Log.d(TAG, "准备开始Opus数据流编码") + opusManager1?.startEncodeStream(object : OnEncodeStreamCallback { + override fun onEncodeStream(data: ByteArray?) { + Log.d(TAG, "Opus编码回调收到数据,大小: ${data?.size ?: 0} bytes") + if (data != null) { + + // 编码完成的数据处理: + // 1. 通知回调 + callback?.onAudioDataEncoded(data) + + // // 2. 将编码后的数据加入发送队列进行分块发送 + // addAudioDataToSendQueue(data) + } else { + Log.w(TAG, "编码回调收到空数据") + } + } + + override fun onStart() { + Log.i(TAG, "Opus数据流编码已开始") + callback?.onEncodeStreamStateChanged(true) + } + + override fun onComplete(outPath: String?) { + Log.i(TAG, "Opus数据流编码完成: $outPath") + callback?.onEncodeStreamStateChanged(false) + } + + override fun onError(code: Int, message: String?) { + Log.e(TAG, "Opus数据流编码错误: [$code] $message") + callback?.onError("encode", code, message) + callback?.onEncodeStreamStateChanged(false) + } + }) + + return true + } catch (e: Exception) { + Log.e(TAG, "启动Opus编码流失败: ${e.message}", e) + callback?.onError("encode", -1, e.message) + return false + } + } + + /** + * 停止Opus数据流编码 + * @return 是否成功停止编码 + */ + fun stopEncodeStream(): Boolean { + if (opusManager1?.isEncodeStream == true) { + opusManager1?.stopEncodeStream() + stopAudioSendThread() // 停止音频发送线程 + Log.i(TAG, "已停止Opus数据流编码") + callback?.onEncodeStreamStateChanged(false) + return true + } + Log.d(TAG, "Opus数据流未在编码或OpusManager未初始化") + return false + } + + /** + * 写入外部音频数据到Opus编码流 + * 该函数用于将外部音频数据(如麦克风录音)写入到Opus编码器中 + * + * @param data 音频数据字节数组,通常为PCM格式 + */ + fun writeExternalAudioData(data: ByteArray) { + if (!isInitialized || opusManager1 == null) { + Log.e(TAG, "OpusManager未初始化") + return + } + + // 如果已经在编码流中,直接写入数据 + if (opusManager1?.isEncodeStream == true) { + Log.d(TAG, "正在进行Opus编码流,写入音频数据") + // 将外部音频数据写入编码流 + opusManager1?.writeEncodeStream(data) + } else { + Log.w(TAG, "Opus编码流未启动,无法写入音频数据") + } + } + + // ====================================================================================================== + // 状态查询方法 + // ====================================================================================================== + + /** + * 检查是否正在解码 + * @return 是否正在解码 + */ + fun isDecoding(): Boolean { + return opusManager?.isDecodeStream == true + } + + /** + * 检查是否正在编码 + * @return 是否正在编码 + */ + fun isEncoding(): Boolean { + return opusManager1?.isEncodeStream == true + } + + /** + * 检查是否已初始化 + * @return 是否已初始化 + */ + fun isInitialized(): Boolean { + return isInitialized + } + + // ====================================================================================================== + // 私有方法 - 音频数据处理 + // ====================================================================================================== + + + /** + * 停止音频发送线程 + */ + private fun stopAudioSendThread() { + isAudioSending.set(false) + audioSendThread?.interrupt() + + try { + audioSendThread?.join(1000) // 等待最多1秒 + } catch (e: InterruptedException) { + Log.w(TAG, "等待音频发送线程停止时被中断") + } + + audioSendThread = null + audioSendQueue.clear() + + synchronized(audioBuffer) { + audioBuffer.clear() + } + + Log.d(TAG, "音频发送线程已停止") + } + + /** + * 启动音频队列处理线程 + */ + private fun startAudioQueueProcessing() { + audioQueueProcessorThread = Thread { + Log.d(TAG, "音频队列处理线程已启动") + + while (!Thread.currentThread().isInterrupted) { + try { + // 从队列中取出音频数据进行处理 + val audioData = audioDataQueue.take() // 阻塞等待 + + // 将音频数据写入Opus解码流 + opusManager?.writeAudioStream(audioData) + + } catch (e: InterruptedException) { + Log.d(TAG, "音频队列处理线程被中断") + break + } catch (e: Exception) { + Log.e(TAG, "音频队列处理异常: ${e.message}", e) + } + } + + Log.d(TAG, "音频队列处理线程已停止") + } + + audioQueueProcessorThread?.start() + } +} \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/RecordingFile.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/RecordingFile.kt new file mode 100644 index 000000000..78d73aa46 --- /dev/null +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/RecordingFile.kt @@ -0,0 +1,160 @@ +package com.yunqiinnovation.ble_service + +import android.content.Context +import android.util.Log +import java.io.File +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.atomic.AtomicBoolean + +/** + * 录音文件管理类:负责音频数据的异步写入和文件管理 + */ +class RecordingFile(private val context: Context) { + private var currentAudioFile: File? = null + private var fos: FileOutputStream? = null + + // 新增:用于异步写入的队列和线程 + private val writeQueue = LinkedBlockingQueue() + private val isWriting = AtomicBoolean(false) + private var writeThread: Thread? = null + + // 用于存储音频数据的缓冲区 + private val dataBuffer = mutableListOf() + private var totalBytesWritten = 0 + + var fileName = "" + internal fun creatingFiles() { + if (fos != null || currentAudioFile != null) { + return + } + // 创建新的音频文件 + val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) + val timestamp = dateFormat.format(Date()) + + val filePath = + File(context.getExternalFilesDir(null), "${fileName}_${timestamp}.txt").absolutePath + currentAudioFile = File(filePath) + currentAudioFile?.createNewFile() + // 追加音频数据到文件 + fos = FileOutputStream(currentAudioFile, true) + startWriteThread() // 新增:启动写入线程 + Log.d("TAG", "创建文件: ${currentAudioFile?.absolutePath}") + } + + // 新增:启动写入线程 + private fun startWriteThread() { + if (isWriting.get()) return + isWriting.set(true) + writeThread = Thread { + try { + while (isWriting.get() || writeQueue.isNotEmpty()) { + val data = writeQueue.poll() ?: continue + fos?.write(data) + } + } catch (e: Exception) { + Log.e("", "异步写入音频数据失败: ${e.message}") + } + } + writeThread?.start() + } + + /** + * 保存音频数据到 opus 文件(异步) + * @param buffer 音频数据字节数组 + */ + internal fun saveAudioDataToWav(buffer: ByteArray) { + if (fos == null || currentAudioFile == null) return + // 放入队列,由写线程写入 + // // 1. 将字节数据转换为十六进制字符串 + // val hexData = buildString { + // buffer.forEachIndexed { index, byte -> + // append("%02X".format(byte)) + + // } + // } + + // 2. 写入十六进制字符串 + + writeQueue.offer(buffer) + totalBytesWritten += buffer.size + } + + /** + * 保存日志信息到文件(重载方法) + * 使用UTF-8编码处理字符串 + * @param logMessage 日志消息字符串 + */ + internal fun saveAudioDataToWav(logMessage: String) { + if (fos == null || currentAudioFile == null) return + try { + // 使用UTF-8编码将字符串转换为字节数组,并添加换行符 + val logBytes = (logMessage + "\n").toByteArray(Charsets.UTF_8) + writeQueue.offer(logBytes) + totalBytesWritten += logBytes.size + Log.d("RecordingFile", "保存日志信息: ${logMessage.length} 字符, ${logBytes.size} 字节 (UTF-8)") + } catch (e: Exception) { + Log.e("RecordingFile", "UTF-8编码转换失败: ${e.message}") + } + } + + /** + * 将音频数据转换为UTF-8编码的十六进制字符串格式并保存 + * @param buffer 音频数据字节数组 + * @param addTimestamp 是否添加时间戳,默认为true + */ + internal fun saveAudioDataAsUtf8Hex(buffer: ByteArray, addTimestamp: Boolean = true) { + if (fos == null || currentAudioFile == null) return + + try { + val timestamp = if (addTimestamp) { + "[${SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())}] " + } else "" + + // 将字节数组转换为十六进制字符串 + val hexString = buffer.joinToString(" ") { "%02X".format(it) } + val dataLine = "${timestamp}音频数据 (${buffer.size} 字节): $hexString\n" + + // 使用UTF-8编码转换为字节数组 + val utf8Bytes = dataLine.toByteArray(Charsets.UTF_8) + writeQueue.offer(utf8Bytes) + totalBytesWritten += utf8Bytes.size + + Log.d("RecordingFile", "保存UTF-8格式音频数据: ${buffer.size} 字节 -> ${utf8Bytes.size} UTF-8字节") + } catch (e: Exception) { + Log.e("RecordingFile", "UTF-8格式保存失败: ${e.message}") + } + } + + + internal fun closeFile() { + try { + if (fos == null || currentAudioFile == null) { + return + } + isWriting.set(false) + writeThread?.join(500) // 等待写线程结束 + fos?.close() + + // 更新WAV文件头 + currentAudioFile?.let { file -> + // 如果文件小于44字节,直接删除 + if (file.length() <= 0) { + file.delete() + Log.d("TAG", "音频文件过小已删除: ${file.absolutePath}") + } else { + Log.d("TAG", "音频文件保存完成: ${file.absolutePath}") + } + } + + } catch (e: Exception) { + Log.e("TAG", "更新opus失败: ${e.message}") + } finally { + fos = null + currentAudioFile = null + } + } +} diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index da86e4dca..46dd97445 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -635,7 +635,25 @@ private var cmdReplyType: UInt8 = 0 //return result return true } - + /// 打开B1编码器 + public func openB1Encoder() -> Bool { + os_log("🎙️ 准备打开编码(设备侧)...", log: logger, type: .info) + // 确保Opus解码器处于活跃状态(如果不是则启动) + if opusProcessor?.isDecoding() != true { + startOpusDecoding() + } + 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) diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift index 7368fe88c..5b0b26fb2 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift @@ -141,7 +141,7 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { case "getPairedMacAddress": let pairedUUID = BleService.shared.getPairedMacAddress() - result(pairedUUID) + result(true) case "clearAssociations": BleService.shared.clearAssociations() result(true) diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift index 87367acd5..7e0898ec3 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift @@ -282,7 +282,7 @@ class SwiftOpusAudioProcessor: NSObject { return } - print("Opus解码数据: \(data.count) bytes, 声道数: \(channels)") + // print("Opus解码数据: \(data.count) bytes, 声道数: \(channels)") DispatchQueue.main.async { [weak self] in self?.callback?.onAudioDataReceived(data: data, channel: self?.channels ?? 0) } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index f17b7d27c..e0374ae37 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -631,7 +631,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } // 调用MCP工具 val toolResult = try { - withTimeout(10000) { // 100秒超时 + withTimeout(20000) { // 100秒超时 // 再次检查会话状态 if (sessionid != currSessionId) { throw CancellationException("Session cancelled") diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift index 19e02ba87..c37e74bae 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift @@ -501,7 +501,7 @@ private func processToolCalls(_ sessionId:String) async -> Bool { } else { // 使用8秒超时调用MCP工具 do { - let toolResult = try await withTimeout(seconds: 8.0) { + let toolResult = try await withTimeout(seconds: 20.0) { return await client.callTool(name: firstToolCall.name, arguments: args) } diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index 9dfda89a8..1b892350a 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -195,7 +195,7 @@ public class SystemFunctionHandler: NSObject { let handler = GetContactsHandler() _ = client.registerLocalFunction( name: "get_contacts", - description: "获取通讯录中的所有联系人", + description: "获取联系人列表,读取通讯录信息,联系人名称和电话", parameters: parameters, handler: handler ) diff --git a/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift b/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift index e1f0515e8..b79f17bed 100644 --- a/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift +++ b/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift @@ -16,21 +16,21 @@ public class ChatStorageHelper { private let logger = OSLog(subsystem: "com.yunqiinnovation.chat_storage", category: "ChatStorageHelper") private init() { - // 获取文档目录路径 + // 获取文档目录路径(不变) let fileURL = try! FileManager.default .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) .appendingPathComponent("chat_storage.db") dbPath = fileURL.path - // 打开数据库 + // 打开数据库(不变) if sqlite3_open(dbPath, &db) != SQLITE_OK { let errmsg = String(cString: sqlite3_errmsg(db)!) os_log("无法打开数据库: %{public}@", log: logger, type: .error, errmsg) return } - // 创建消息表 + // 关键修改:新增 UNIQUE (agent_id, session_id, sender) 联合唯一索引 let createTableString = """ CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -39,9 +39,10 @@ public class ChatStorageHelper { message TEXT NOT NULL, sender TEXT NOT NULL, metadata TEXT, - created_at INTEGER NOT NULL + created_at INTEGER NOT NULL, + UNIQUE (agent_id, session_id, sender) ON CONFLICT REPLACE ); - CREATE INDEX IF NOT EXISTS idx_agent_id ON messages (session_id); + CREATE INDEX IF NOT EXISTS idx_agent_id ON messages (agent_id); CREATE INDEX IF NOT EXISTS idx_session_id ON messages (session_id); CREATE INDEX IF NOT EXISTS idx_created_at ON messages (created_at); """ @@ -66,38 +67,46 @@ public class ChatStorageHelper { * @param metadata 可选的元数据 * @return 插入的消息ID,失败则返回-1 */ - public func saveMessage(agentId:String,sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { - let insertStatementString = "INSERT INTO messages (agent_id,session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?);" + public func saveMessage(agentId: String, sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { + // 关键修改:用 INSERT OR REPLACE 替换 INSERT,支持冲突时更新 + let insertStatementString = """ + INSERT OR REPLACE INTO messages + (agent_id, session_id, message, sender, metadata, created_at) + VALUES (?, ?, ?, ?, ?, ?); + """ var insertStatement: OpaquePointer? if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK { + // 绑定参数(逻辑不变,确保三个唯一字段正确传入) sqlite3_bind_text(insertStatement, 1, (agentId as NSString).utf8String, -1, nil) - sqlite3_bind_text(insertStatement, 1, (sessionId as NSString).utf8String, -1, nil) - sqlite3_bind_text(insertStatement, 2, (message as NSString).utf8String, -1, nil) - sqlite3_bind_text(insertStatement, 3, (sender as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 2, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 3, (message as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 4, (sender as NSString).utf8String, -1, nil) if let metadata = metadata { - sqlite3_bind_text(insertStatement, 4, (metadata as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 5, (metadata as NSString).utf8String, -1, nil) } else { - sqlite3_bind_null(insertStatement, 4) + sqlite3_bind_null(insertStatement, 5) } let currentTime = Int(Date().timeIntervalSince1970) - sqlite3_bind_int(insertStatement, 5, Int32(currentTime)) + sqlite3_bind_int(insertStatement, 6, Int32(currentTime)) + // 执行语句(冲突时会自动替换,返回新的 rowid) if sqlite3_step(insertStatement) == SQLITE_DONE { - let id = sqlite3_last_insert_rowid(db) + let id = sqlite3_last_insert_rowid(db) // 替换后返回新的 id(原 id 会被删除) sqlite3_finalize(insertStatement) + os_log("消息保存成功(新增/更新),id: %{public}lld", log: logger, type: .info, id) return id } else { let errmsg = String(cString: sqlite3_errmsg(db)!) - os_log("插入消息失败: %{public}@", log: logger, type: .error, errmsg) + os_log("插入/更新消息失败: %{public}@", log: logger, type: .error, errmsg) } sqlite3_finalize(insertStatement) } else { let errmsg = String(cString: sqlite3_errmsg(db)!) - os_log("插入消息语句准备失败: %{public}@", log: logger, type: .error, errmsg) + os_log("插入/更新消息语句准备失败: %{public}@", log: logger, type: .error, errmsg) } return -1 @@ -131,7 +140,7 @@ public class ChatStorageHelper { let totalPages = totalCount == 0 ? 0 : (totalCount + pageSize - 1) / pageSize let queryString = """ - SELECT id, message, sender, metadata, created_at + SELECT id, session_id, message, sender, metadata, created_at FROM messages WHERE agent_id = ? ORDER BY created_at DESC @@ -148,16 +157,19 @@ public class ChatStorageHelper { while sqlite3_step(queryStatement) == SQLITE_ROW { let id = sqlite3_column_int(queryStatement, 0) - let messagePtr = sqlite3_column_text(queryStatement, 1) + let sessionIdPtr = sqlite3_column_text(queryStatement, 1) + let sessionId = sessionIdPtr != nil ? String(cString: sessionIdPtr!) : "" + + let messagePtr = sqlite3_column_text(queryStatement, 2) let message = messagePtr != nil ? String(cString: messagePtr!) : "" - let senderPtr = sqlite3_column_text(queryStatement, 2) + let senderPtr = sqlite3_column_text(queryStatement, 3) let sender = senderPtr != nil ? String(cString: senderPtr!) : "" - let metadataPtr = sqlite3_column_text(queryStatement, 3) + let metadataPtr = sqlite3_column_text(queryStatement, 4) let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil - let createdAt = sqlite3_column_int(queryStatement, 4) + let createdAt = sqlite3_column_int(queryStatement, 5) // 将时间戳转换为ISO 8601格式的字符串 let date = Date(timeIntervalSince1970: TimeInterval(createdAt)) @@ -167,6 +179,7 @@ public class ChatStorageHelper { var messageDict: [String: Any] = [ "id": id, "agentId": agentId, // 添加agentId字段 + "sessionId": sessionId, // 添加sessionId字段 "message": message, "sender": sender, "timestamp": timestamp // 使用timestamp而不是created_at @@ -323,7 +336,7 @@ public class ChatStorageHelper { // 构建查询语句 - 按时间倒序获取最近的N条,然后在结果中再按时间正序 let queryString = """ SELECT * FROM ( - SELECT id, message, sender, metadata, created_at + SELECT id, session_id, message, sender, metadata, created_at FROM messages WHERE agent_id = ? ORDER BY created_at DESC @@ -340,19 +353,24 @@ public class ChatStorageHelper { while sqlite3_step(queryStatement) == SQLITE_ROW { let id = sqlite3_column_int(queryStatement, 0) - let messagePtr = sqlite3_column_text(queryStatement, 1) + let sessionIdPtr = sqlite3_column_text(queryStatement, 1) + let sessionId = sessionIdPtr != nil ? String(cString: sessionIdPtr!) : "" + + + let messagePtr = sqlite3_column_text(queryStatement, 2) let message = messagePtr != nil ? String(cString: messagePtr!) : "" - let senderPtr = sqlite3_column_text(queryStatement, 2) + let senderPtr = sqlite3_column_text(queryStatement, 3) let sender = senderPtr != nil ? String(cString: senderPtr!) : "" - let metadataPtr = sqlite3_column_text(queryStatement, 3) + let metadataPtr = sqlite3_column_text(queryStatement, 4) let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil - let createdAt = sqlite3_column_int(queryStatement, 4) + let createdAt = sqlite3_column_int(queryStatement, 5) var messageDict: [String: Any] = [ "id": id, + "sessionId":sessionId, "message": message, "sender": sender, "created_at": createdAt diff --git a/pubspec.yaml b/pubspec.yaml index 8291a2912..34f2f8244 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: voitrans description: "Voitrans - AI Voice Assistant." publish_to: "none" -version: 1.0.16+41 +version: 1.0.17+46 environment: sdk: ">=3.3.0 <4.0.0"