Browse Source

Merge branch 'ble' of https://github.com/deepcloud2048/deep_voice into dev

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
79c7cd4782
  1. 10
      .mcp copy.json
  2. 20
      android/app/build.gradle.kts
  3. 2
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  4. 2
      android/build.gradle.kts
  5. 9
      android/settings.gradle.kts
  6. 4
      lib/core/themes/theme_service.dart
  7. 85
      lib/core/utils/logger.dart
  8. 49
      lib/data/services/ble_manager.dart
  9. 8
      lib/data/services/bluetooth_manager.dart
  10. 2
      lib/data/services/chat_history_service.dart
  11. 6
      lib/data/services/deapsound_ai_service.dart
  12. 6
      lib/data/services/kouzi_ai_service.dart
  13. 4
      lib/data/services/speech_factory.dart
  14. 21
      lib/data/services/speech_impl/azure_asr_service.dart
  15. 22
      lib/data/services/speech_impl/azure_tts_service.dart
  16. 18
      lib/data/services/speech_impl/flutter_asr_service.dart
  17. 24
      lib/data/services/speech_impl/flutter_tts_service.dart
  18. 22
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  19. 38
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  20. 16
      lib/data/services/speech_impl/volcano_tts_api_service.dart
  21. 20
      lib/data/services/speech_impl/xunfei_asr_service.dart
  22. 18
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  23. 12
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  24. 123
      lib/modules/agent/controllers/agent_controller.dart
  25. 10
      lib/modules/chat/controllers/chat_controller.dart
  26. 11
      lib/modules/jieli_test/bindings/jieli_test_binding.dart
  27. 323
      lib/modules/jieli_test/controllers/jieli_test_controller.dart
  28. 568
      lib/modules/jieli_test/views/jieli_test_view.dart
  29. 8
      lib/modules/meeting/controllers/meeting_details_controller.dart
  30. 124
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  31. 16
      lib/modules/pairing/controllers/pairing_controller.dart
  32. 18
      lib/modules/speech_test/controllers/speech_test_controller.dart
  33. 2
      lib/modules/splash/controllers/splash_controller.dart
  34. 26
      lib/modules/translation/controllers/translation_controller.dart
  35. 8
      lib/modules/translation/controllers/translation_detail_controller.dart
  36. 9
      lib/routes/app_pages.dart
  37. 6
      local_plugins/agent_service/android/build.gradle.kts
  38. 14
      local_plugins/agent_service/android/settings.gradle.kts
  39. 469
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  40. 30
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  41. 28
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  42. 57
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  43. 14
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt
  44. 14
      local_plugins/agent_service/pubspec.yaml
  45. 5
      local_plugins/azure_speech/android/build.gradle.kts
  46. 51
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  47. 224
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  48. 298
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  49. 2
      local_plugins/ble_service/android/build.gradle.kts
  50. BIN
      local_plugins/ble_service/android/libs/jl_audio_decode_V2.0.0_20005.aar
  51. BIN
      local_plugins/ble_service/android/libs/jl_bt_ota_V1.9.2-release.aar
  52. 74
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  53. 15
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt
  54. 282
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  55. 11
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  56. 13
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt
  57. 116
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt
  58. 28
      local_plugins/bytedance_speech/android/build.gradle.kts
  59. 2
      local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml
  60. 392
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt
  61. 329
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt
  62. 670
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt
  63. 757
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt
  64. 5
      local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt
  65. 2
      local_plugins/classic_bluetooth/pubspec.yaml
  66. 46
      local_plugins/deepvoice_utils/android/build.gradle.kts
  67. 1
      local_plugins/deepvoice_utils/android/settings.gradle.kts
  68. 7
      local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml
  69. 33
      local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt
  70. 237
      local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt
  71. 4
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h
  72. 12
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m
  73. 30
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift
  74. 286
      local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift
  75. 11
      local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift
  76. 54
      local_plugins/deepvoice_utils/ios/README.md
  77. 19
      local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec
  78. 3
      local_plugins/deepvoice_utils/lib/deepvoice_utils.dart
  79. 31
      local_plugins/deepvoice_utils/lib/src/file_logger.dart
  80. 27
      local_plugins/deepvoice_utils/pubspec.yaml
  81. 144
      local_plugins/jieli/README.md
  82. 1
      local_plugins/jieli/android/settings.gradle.kts
  83. 22
      local_plugins/jieli/android/src/main/AndroidManifest.xml
  84. 665
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt
  85. 294
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt
  86. 283
      local_plugins/jieli/lib/jieli.dart
  87. 23
      local_plugins/jieli/pubspec.yaml
  88. 9
      local_plugins/jl_ota/android/build.gradle
  89. BIN
      local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.0-release.aar
  90. 2
      local_plugins/jl_ota/android/settings.gradle
  91. 255
      local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt
  92. 1
      local_plugins/music_service/android/build.gradle
  93. 25
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  94. 4
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  95. 49
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  96. 173
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt
  97. BIN
      local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock
  98. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  99. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties
  100. 27
      local_plugins/speech/android/build.gradle.kts

10
.mcp copy.json

@ -1,10 +0,0 @@
{
"mcpServers": {
"web-search": {
"url": "http://mcp.ideapsound.com:8000/sse"
},
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=df7b0b6747c632fc38ddad22e7176646"
}
}
}

20
android/app/build.gradle.kts

@ -120,28 +120,8 @@ android {
} }
dependencies { dependencies {
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
// 添加本地插件模块依赖
implementation(project(":spotify_sdk"))
implementation(project(":deepvoice_utils"))
implementation(project(":ble_service"))
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3")
// 添加核心库反糖化 // 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// 添加androidx.media依赖
implementation("androidx.media:media:1.6.0")
// 添加legacy-support-v4依赖
implementation("androidx.legacy:legacy-support-v4:1.0.0")
} }
flutter { flutter {

2
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -12,7 +12,7 @@ import io.flutter.embedding.android.FlutterActivity
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
private val TAG = "MainActivity" private val TAG = "MainActivity"

2
android/build.gradle.kts

@ -35,7 +35,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
// 其他依赖... // 其他依赖...
} }
} }

9
android/settings.gradle.kts

@ -37,9 +37,11 @@ include(":chat_storage")
include(":jieli") include(":jieli")
include(":spotify_sdk") include(":spotify_sdk")
include(":ble_service") include(":ble_service")
include(":deepvoice_utils")
include(":classic_bluetooth") include(":classic_bluetooth")
include(":agent_service") include(":agent_service")
include(":volcano_speech")
include(":bytedance_speech")
include(":speech")
// 设置azure_speech项目的路径 // 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android") project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
@ -47,7 +49,8 @@ project(":chat_storage").projectDir = file("../local_plugins/chat_storage/androi
project(":jieli").projectDir = file("../local_plugins/jieli/android") project(":jieli").projectDir = file("../local_plugins/jieli/android")
project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android") project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android")
project(":ble_service").projectDir = file("../local_plugins/ble_service/android") project(":ble_service").projectDir = file("../local_plugins/ble_service/android")
project(":deepvoice_utils").projectDir = file("../local_plugins/deepvoice_utils/android")
project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android") project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android")
project(":agent_service").projectDir = file("../local_plugins/agent_service/android") project(":agent_service").projectDir = file("../local_plugins/agent_service/android")
project(":volcano_speech").projectDir = file("../local_plugins/volcano_speech/android")
project(":bytedance_speech").projectDir = file("../local_plugins/bytedance_speech/android")
project(":speech").projectDir = file("../local_plugins/speech/android")

4
lib/core/themes/theme_service.dart

@ -21,7 +21,7 @@ class ThemeService {
Logger.info('从存储读取主题设置: $value'); Logger.info('从存储读取主题设置: $value');
return value ?? false; return value ?? false;
} catch (e) { } catch (e) {
Logger.error('读取主题设置失败: $e'); Logger.error('读取主题设置失败: ${e.toString()}');
return false; return false;
} }
} }
@ -32,7 +32,7 @@ class ThemeService {
Logger.info('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}'); Logger.info('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}');
await _box.write(_key, isDarkMode); await _box.write(_key, isDarkMode);
} catch (e) { } catch (e) {
Logger.error('保存主题设置失败: $e'); Logger.error('保存主题设置失败: ${e.toString()}');
} }
} }
} }

85
lib/core/utils/logger.dart

@ -15,77 +15,84 @@ class Logger {
/// 当前日志级别,低于此级别的日志不会被输出 /// 当前日志级别,低于此级别的日志不会被输出
static LogLevel _currentLevel = LogLevel.debug; static LogLevel _currentLevel = LogLevel.debug;
/// 默认标签
static const String _defaultTag = 'App';
/// 设置日志级别 /// 设置日志级别
static void setLevel(LogLevel level) { static void setLevel(LogLevel level) {
_currentLevel = level; _currentLevel = level;
} }
/// 输出调试日志 (不带标签版本)
static void debug(String message) {
if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', _defaultTag, message);
}
}
/// 输出调试日志 /// 输出调试日志
static void debug(String message, {String? tag}) { static void d(String tag, String message) {
if (_currentLevel.index <= LogLevel.debug.index) { if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', message, tag: tag); _log('DEBUG', tag, message);
}
}
/// 输出信息日志 (不带标签版本)
static void info(String message) {
if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', _defaultTag, message);
} }
} }
/// 输出信息日志 /// 输出信息日志
static void info(String message, {String? tag}) { static void i(String tag, String message) {
if (_currentLevel.index <= LogLevel.info.index) { if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', message, tag: tag); _log('INFO', tag, message);
}
}
/// 输出警告日志 (不带标签版本)
static void warning(String message) {
if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', _defaultTag, message);
} }
} }
/// 输出警告日志 /// 输出警告日志
static void warning(String message, {String? tag}) { static void w(String tag, String message) {
if (_currentLevel.index <= LogLevel.warning.index) { if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', message, tag: tag); _log('WARNING', tag, message);
} }
} }
/// 输出错误日志 /// 输出错误日志 (不带标签版本)
static void error(String message, [dynamic error, StackTrace? stackTrace, String? tag]) { static void error(String message, [dynamic error, StackTrace? stackTrace]) {
if (_currentLevel.index <= LogLevel.error.index) { if (_currentLevel.index <= LogLevel.error.index) {
_log('ERROR', message, tag: tag); _log('ERROR', _defaultTag, message);
if (error != null) { if (error != null) {
_log('ERROR', 'Original error: $error', tag: tag); _log('ERROR', _defaultTag, 'Original error: $error');
} }
if (stackTrace != null) { if (stackTrace != null) {
_log('ERROR', 'Stack trace: $stackTrace', tag: tag); _log('ERROR', _defaultTag, 'Stack trace: $stackTrace');
} }
} }
} }
/// 获取当前调用栈的类名 /// 输出错误日志
static String? _getCallerClassName() { static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) {
try { if (_currentLevel.index <= LogLevel.error.index) {
final stackTrace = StackTrace.current.toString(); _log('ERROR', tag, message);
final frames = stackTrace.split('\n'); if (error != null) {
_log('ERROR', tag, 'Original error: $error');
// 查找调用者帧 (跳过Logger框架内部的帧) }
for (int i = 0; i < frames.length; i++) { if (stackTrace != null) {
final frame = frames[i].trim(); _log('ERROR', tag, 'Stack trace: $stackTrace');
if (frame.contains('Logger.') || frame.contains('_log')) {
continue;
}
// 简单的解析类名
final regex = RegExp(r'#\d+\s+([^\.]+)\.([^\.]+)\s');
final match = regex.firstMatch(frame);
if (match != null && match.groupCount >= 1) {
return match.group(1);
}
break;
} }
} catch (e) {
// 忽略解析错误
} }
return null;
} }
/// 内部日志输出方法 /// 内部日志输出方法
static void _log(String level, String message, {String? tag}) { static void _log(String level, String tag, String message) {
final timestamp = DateTime.now().toString(); developer.log('[$tag]: $message');
final className = tag ?? _getCallerClassName() ?? '';
final tagInfo = className.isNotEmpty ? '[$className] ' : '';
developer.log('[$timestamp][$level][$tagInfo]: $message');
} }
} }

49
lib/data/services/ble_manager.dart

@ -54,7 +54,7 @@ class DeviceInfo {
this.rightEarCharging = false, this.rightEarCharging = false,
this.caseCharging = false, this.caseCharging = false,
this.firmwareVersion = '', this.firmwareVersion = '',
this.firmwareProductId = 's3', this.firmwareProductId = '',
this.firmwareProductColor = '', this.firmwareProductColor = '',
DateTime? lastUpdated, DateTime? lastUpdated,
}) : lastUpdated = lastUpdated ?? DateTime.now(); }) : lastUpdated = lastUpdated ?? DateTime.now();
@ -168,7 +168,7 @@ class BleManager extends GetxService {
Future.delayed(const Duration(milliseconds: 300), () { Future.delayed(const Duration(milliseconds: 300), () {
// 可选:检查是否仍处于连接状态 // 可选:检查是否仍处于连接状态
if (_isConnected.value) { if (_isConnected.value) {
fetchDeviceInfo(); // fetchDeviceInfo();
} }
}); });
} else if (state == BleConnectionState.disconnected || } else if (state == BleConnectionState.disconnected ||
@ -198,7 +198,7 @@ class BleManager extends GetxService {
}, },
); );
} catch (e) { } catch (e) {
Logger.error('设置BLE事件监听器失败: $e'); Logger.error('设置BLE事件监听器失败: ${e.toString()}');
} }
} }
@ -225,7 +225,7 @@ class BleManager extends GetxService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('处理设备信息异常: $e'); Logger.error('处理设备信息异常: ${e.toString()}');
} }
} }
@ -303,10 +303,9 @@ class BleManager extends GetxService {
caseCharging: caseCharging, caseCharging: caseCharging,
); );
Logger.info( Logger.info('设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
'设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
} catch (e) { } catch (e) {
Logger.error('更新电池信息失败: $e'); Logger.error('更新电池信息失败: ${e.toString()}');
} }
} }
@ -329,7 +328,7 @@ class BleManager extends GetxService {
Logger.info('设备版本信息已更新: 版本=$version'); Logger.info('设备版本信息已更新: 版本=$version');
} catch (e) { } catch (e) {
Logger.error('更新版本信息失败: $e'); Logger.error('更新版本信息失败: ${e.toString()}');
} }
} }
@ -352,7 +351,7 @@ class BleManager extends GetxService {
Logger.info('设备设备id已更新: 设备id=$productId'); Logger.info('设备设备id已更新: 设备id=$productId');
} catch (e) { } catch (e) {
Logger.error('更新设备id失败: $e'); Logger.error('更新设备id失败: ${e.toString()}');
} }
} }
@ -440,7 +439,7 @@ class BleManager extends GetxService {
Logger.warning('产品id获取指令发送失败'); Logger.warning('产品id获取指令发送失败');
} }
} catch (e) { } catch (e) {
Logger.error('获取BLE设备信息失败: $e'); Logger.error('获取BLE设备信息失败: ${e.toString()}');
} }
} }
@ -455,7 +454,7 @@ class BleManager extends GetxService {
return _connectionState.value; return _connectionState.value;
} catch (e) { } catch (e) {
Logger.error('获取BLE连接状态失败: $e'); Logger.error('获取BLE连接状态失败: ${e.toString()}');
return BleConnectionState.error; return BleConnectionState.error;
} }
} }
@ -497,6 +496,8 @@ class BleManager extends GetxService {
'openaiModel': openaiModel, 'openaiModel': openaiModel,
'systemPrompt': systemPrompt, 'systemPrompt': systemPrompt,
'mcpServer': jsonEncode(jsonData), 'mcpServer': jsonEncode(jsonData),
'volcanoAppId': '2316081933',
'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En',
}; };
// 记录日志(不记录敏感信息) // 记录日志(不记录敏感信息)
@ -531,7 +532,7 @@ class BleManager extends GetxService {
// 如果设备信息存在,则获取详细信息 // 如果设备信息存在,则获取详细信息
if (deviceInfo.value != null) { if (deviceInfo.value != null) {
// 获取详细设备信息 // 获取详细设备信息
fetchDeviceInfo(); // fetchDeviceInfo();
} }
} }
} else { } else {
@ -540,7 +541,7 @@ class BleManager extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('启动BLE服务失败: $e'); Logger.error('启动BLE服务失败: ${e.toString()}');
return false; return false;
} }
} }
@ -560,7 +561,7 @@ class BleManager extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('停止BLE服务失败: $e'); Logger.error('停止BLE服务失败: ${e.toString()}');
return false; return false;
} }
} }
@ -570,7 +571,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.getBatteryLevel(); return await _bleService.getBatteryLevel();
} catch (e) { } catch (e) {
Logger.error('获取电池电量失败: $e'); Logger.error('获取电池电量失败: ${e.toString()}');
return 0; return 0;
} }
} }
@ -580,7 +581,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.getBatteryInfo(); return await _bleService.getBatteryInfo();
} catch (e) { } catch (e) {
Logger.error('获取电池信息失败: $e'); Logger.error('获取电池信息失败: ${e.toString()}');
return false; return false;
} }
} }
@ -590,7 +591,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.getProductId(); return await _bleService.getProductId();
} catch (e) { } catch (e) {
Logger.error('获取设备版本失败: $e'); Logger.error('获取设备版本失败: ${e.toString()}');
return false; return false;
} }
} }
@ -600,7 +601,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.getDeviceVersion(); return await _bleService.getDeviceVersion();
} catch (e) { } catch (e) {
Logger.error('获取设备版本失败: $e'); Logger.error('获取设备版本失败: ${e.toString()}');
return false; return false;
} }
} }
@ -610,7 +611,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.openEncoder(); return await _bleService.openEncoder();
} catch (e) { } catch (e) {
Logger.error('打开编码器失败: $e'); Logger.error('打开编码器失败: ${e.toString()}');
return false; return false;
} }
} }
@ -620,7 +621,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.openDecoder(); return await _bleService.openDecoder();
} catch (e) { } catch (e) {
Logger.error('打开解码器失败: $e'); Logger.error('打开解码器失败: ${e.toString()}');
return false; return false;
} }
} }
@ -630,7 +631,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.openA2DPDecoder(); return await _bleService.openA2DPDecoder();
} catch (e) { } catch (e) {
Logger.error('打开解码器失败: $e'); Logger.error('打开解码器失败: ${e.toString()}');
return false; return false;
} }
} }
@ -640,7 +641,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.closeCodec(); return await _bleService.closeCodec();
} catch (e) { } catch (e) {
Logger.error('关闭编解码器失败: $e'); Logger.error('关闭编解码器失败: ${e.toString()}');
return false; return false;
} }
} }
@ -650,7 +651,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.playA2DP(); return await _bleService.playA2DP();
} catch (e) { } catch (e) {
Logger.error('播放A2DP失败: $e'); Logger.error('播放A2DP失败: ${e.toString()}');
return false; return false;
} }
} }
@ -662,4 +663,4 @@ class BleManager extends GetxService {
} }
return Future.value(Get.find<BleManager>()); return Future.value(Get.find<BleManager>());
} }
} }

8
lib/data/services/bluetooth_manager.dart

@ -75,11 +75,11 @@ class BluetoothManager extends GetxService {
} }
}, },
onError: (error) { onError: (error) {
Logger.error('蓝牙事件流错误: $error'); Logger.error('蓝牙事件流错误: ${error.toString()}');
}, },
); );
} catch (e) { } catch (e) {
Logger.error('设置蓝牙事件监听器失败: $e'); Logger.error('设置蓝牙事件监听器失败: ${e.toString()}');
} }
} }
@ -106,7 +106,7 @@ class BluetoothManager extends GetxService {
Logger.info('未找到已连接的蓝牙设备'); Logger.info('未找到已连接的蓝牙设备');
} catch (e) { } catch (e) {
Logger.error('刷新设备连接失败: $e'); Logger.error('刷新设备连接失败: ${e.toString()}');
} }
} }
@ -117,7 +117,7 @@ class BluetoothManager extends GetxService {
_bluetoothState.value = isEnabled ? 'on' : 'off'; _bluetoothState.value = isEnabled ? 'on' : 'off';
return isEnabled; return isEnabled;
} catch (e) { } catch (e) {
Logger.error('检查蓝牙状态失败: $e'); Logger.error('检查蓝牙状态失败: ${e.toString()}');
return false; return false;
} }
} }

2
lib/data/services/chat_history_service.dart

@ -39,7 +39,7 @@ class ChatHistoryService extends GetxService {
// 检查并清理过多的历史记录 // 检查并清理过多的历史记录
await _cleanupOldMessages(agentId); await _cleanupOldMessages(agentId);
} catch (e) { } catch (e) {
Logger.error('保存聊天记录失败', e); Logger.error('保存聊天记录失败', e.toString());
} }
} }

6
lib/data/services/deapsound_ai_service.dart

@ -112,14 +112,14 @@ class DeapsoundAIService implements AiService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('JSON解析错误: $e'); Logger.error('JSON解析错误: ${e.toString()}');
} }
} }
} }
} catch (e) { } catch (e) {
Logger.error('请求异常: $e'); Logger.error('请求异常: ${e.toString()}');
} finally { } finally {
client.close(); client.close();
} }
} }
} }

6
lib/data/services/kouzi_ai_service.dart

@ -82,14 +82,14 @@ class KouziAIService implements AiService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('JSON解析错误: $e'); Logger.error('JSON解析错误: ${e.toString()}');
} }
} }
} }
} catch (e) { } catch (e) {
Logger.error('请求异常: $e'); Logger.error('请求异常: ${e.toString()}');
} finally { } finally {
client.close(); client.close();
} }
} }
} }

4
lib/data/services/speech_factory.dart

@ -88,7 +88,7 @@ class SpeechFactory extends GetxService {
Logger.info( Logger.info(
'语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}'); '语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}');
} catch (e) { } catch (e) {
Logger.error('语音服务初始化失败: $e'); Logger.error('语音服务初始化失败: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -108,4 +108,4 @@ class SpeechFactory extends GetxService {
} }
return Get.find<TtsService>(); return Get.find<TtsService>();
} }
} }

21
lib/data/services/speech_impl/azure_asr_service.dart

@ -106,7 +106,7 @@ class AzureAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}');
return result; return result;
} catch (e) { } catch (e) {
Logger.error('Azure 语音识别服务初始化失败: $e'); Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false; _isInitialized = false;
rethrow; rethrow;
} }
@ -144,7 +144,7 @@ class AzureAsrService extends GetxService implements AsrService {
throw Exception('无效的识别结果格式'); throw Exception('无效的识别结果格式');
} catch (e) { } catch (e) {
Logger.error('语音识别失败: $e'); Logger.error('语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString()); return RecognitionEvent.error(e.toString());
} }
} }
@ -177,7 +177,7 @@ class AzureAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = true; _isContinuousRecognitionActive = true;
return _eventStreamController!.stream; return _eventStreamController!.stream;
} catch (e) { } catch (e) {
Logger.error('开始连续语音识别失败: $e'); Logger.error('开始连续语音识别失败: ${e.toString()}');
_cleanupEventStream(); _cleanupEventStream();
rethrow; rethrow;
} }
@ -195,7 +195,7 @@ class AzureAsrService extends GetxService implements AsrService {
_cleanupEventStream(); _cleanupEventStream();
return result; return result;
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败: $e'); Logger.error('停止连续语音识别失败: ${e.toString()}');
_cleanupEventStream(); _cleanupEventStream();
rethrow; rethrow;
} }
@ -214,7 +214,6 @@ class AzureAsrService extends GetxService implements AsrService {
final String eventType = eventMap['type'] as String? ?? ''; final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试 // 添加日志帮助调试
Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap');
switch (eventType) { switch (eventType) {
case 'result': case 'result':
@ -260,7 +259,7 @@ class AzureAsrService extends GetxService implements AsrService {
final String errorDetails = eventMap['errorDetails'] as String? ?? ''; final String errorDetails = eventMap['errorDetails'] as String? ?? '';
if (reason.isNotEmpty || errorDetails.isNotEmpty) { if (reason.isNotEmpty || errorDetails.isNotEmpty) {
Logger.error('识别取消: $reason - $errorDetails'); Logger.error('识别取消: $reason - ${errorDetails.toString()}');
} }
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
@ -271,7 +270,7 @@ class AzureAsrService extends GetxService implements AsrService {
case 'error': case 'error':
final String error = eventMap['message'] as String? ?? ''; final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: $error'); Logger.error('识别错误: ${error.toString()}');
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.error, type: RecognitionEventType.error,
error: error, error: error,
@ -282,7 +281,7 @@ class AzureAsrService extends GetxService implements AsrService {
/// 处理识别事件流错误 /// 处理识别事件流错误
void _handleRecognitionError(Object error) { void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: $error'); Logger.error('识别事件流错误: ${error.toString()}');
_eventStreamController?.addError(error); _eventStreamController?.addError(error);
_cleanupEventStream(); _cleanupEventStream();
} }
@ -311,7 +310,7 @@ class AzureAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别资源已释放'); Logger.info('Azure 语音识别资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放语音识别资源失败: $e'); Logger.error('释放语音识别资源失败: ${e.toString()}');
_cleanupEventStream(); _cleanupEventStream();
_isInitialized = false; _isInitialized = false;
} }
@ -339,8 +338,8 @@ class AzureAsrService extends GetxService implements AsrService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('推送音频数据失败: $e'); Logger.error('推送音频数据失败: ${e.toString()}');
return false; return false;
} }
} }
} }

22
lib/data/services/speech_impl/azure_tts_service.dart

@ -69,7 +69,7 @@ class AzureTtsService extends GetxService implements TtsService {
_handleTtsEvent(event); _handleTtsEvent(event);
} }
}, onError: (error) { }, onError: (error) {
Logger.error('TTS事件流错误: $error'); Logger.error('TTS事件流错误: ${error.toString()}');
_eventStreamController.addError(error); _eventStreamController.addError(error);
}); });
} }
@ -78,7 +78,7 @@ class AzureTtsService extends GetxService implements TtsService {
void _handleTtsEvent(Map<dynamic, dynamic> event) { void _handleTtsEvent(Map<dynamic, dynamic> event) {
final String eventType = event['type'] as String? ?? ''; final String eventType = event['type'] as String? ?? '';
// Logger.debug('收到TTS事件: $eventType, 数据: $event'); // Logger.debug('收到TTS事件: ${e.toString()}ventType, 数据: ${e.toString()}vent');
switch (eventType) { switch (eventType) {
case 'synthesis_started': case 'synthesis_started':
@ -159,7 +159,7 @@ class AzureTtsService extends GetxService implements TtsService {
_isInitialized = result; _isInitialized = result;
return result; return result;
} catch (e) { } catch (e) {
Logger.error('初始化失败: $e'); Logger.error('初始化失败: ${e.toString()}');
return false; return false;
} }
} }
@ -177,7 +177,7 @@ class AzureTtsService extends GetxService implements TtsService {
if (result) _currentVoice = voiceName; if (result) _currentVoice = voiceName;
return result; return result;
} catch (e) { } catch (e) {
Logger.error('设置语音失败: $e'); Logger.error('设置语音失败: ${e.toString()}');
return false; return false;
} }
} }
@ -195,7 +195,7 @@ class AzureTtsService extends GetxService implements TtsService {
return result == true; return result == true;
} catch (e) { } catch (e) {
Logger.error('语音合成失败: $e'); Logger.error('语音合成失败: ${e.toString()}');
return false; return false;
} }
} }
@ -250,7 +250,7 @@ class AzureTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理流式文本失败: $e'); Logger.error('处理流式文本失败: ${e.toString()}');
return false; return false;
} }
} }
@ -316,7 +316,7 @@ class AzureTtsService extends GetxService implements TtsService {
Logger.info('TTS队列播放完成'); Logger.info('TTS队列播放完成');
return result; return result;
} catch (e) { } catch (e) {
Logger.error('刷新TTS流失败: $e'); Logger.error('刷新TTS流失败: ${e.toString()}');
return false; return false;
} }
} }
@ -358,7 +358,7 @@ class AzureTtsService extends GetxService implements TtsService {
_textQueue.removeAt(0); _textQueue.removeAt(0);
} }
} catch (e) { } catch (e) {
Logger.error('处理语音队列出错: $e'); Logger.error('处理语音队列出错: ${e.toString()}');
// 出错时移除当前项并尝试继续 // 出错时移除当前项并尝试继续
if (_textQueue.isNotEmpty) { if (_textQueue.isNotEmpty) {
@ -389,7 +389,7 @@ class AzureTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止语音合成出错: $e'); Logger.error('停止语音合成出错: ${e.toString()}');
return false; return false;
} }
} }
@ -410,7 +410,7 @@ class AzureTtsService extends GetxService implements TtsService {
await _channel.invokeMethod('dispose'); await _channel.invokeMethod('dispose');
_isInitialized = false; _isInitialized = false;
} catch (e) { } catch (e) {
Logger.error('释放资源失败: $e'); Logger.error('释放资源失败: ${e.toString()}');
} }
} }
} }

18
lib/data/services/speech_impl/flutter_asr_service.dart

@ -107,7 +107,7 @@ class FlutterAsrService extends GetxService implements AsrService {
} catch (e) { } catch (e) {
_isAvailable.value = false; _isAvailable.value = false;
_asrState.value = AsrState.error; _asrState.value = AsrState.error;
Logger.error('语音识别初始化失败: $e'); Logger.error('语音识别初始化失败: ${e.toString()}');
} }
} }
@ -203,7 +203,7 @@ class FlutterAsrService extends GetxService implements AsrService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: ${e.toString()}');
_asrState.value = AsrState.error; _asrState.value = AsrState.error;
return false; return false;
} }
@ -253,7 +253,7 @@ class FlutterAsrService extends GetxService implements AsrService {
?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突')); ?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突'));
} }
} catch (e) { } catch (e) {
Logger.error('重启连续识别失败: $e'); Logger.error('重启连续识别失败: ${e.toString()}');
// 发送错误给控制器 // 发送错误给控制器
_continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e')); _continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e'));
@ -304,7 +304,7 @@ class FlutterAsrService extends GetxService implements AsrService {
await _initSpeech(); await _initSpeech();
return _isAvailable.value; return _isAvailable.value;
} catch (e) { } catch (e) {
Logger.error('语音识别初始化失败: $e'); Logger.error('语音识别初始化失败: ${e.toString()}');
return false; return false;
} }
} }
@ -357,7 +357,7 @@ class FlutterAsrService extends GetxService implements AsrService {
}, },
); );
} catch (e) { } catch (e) {
Logger.error('一次性语音识别失败: $e'); Logger.error('一次性语音识别失败: ${e.toString()}');
return RecognitionEvent.error('$e'); return RecognitionEvent.error('$e');
} }
} }
@ -406,7 +406,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_isListening = true; _isListening = true;
return _continuousRecognitionController!.stream; return _continuousRecognitionController!.stream;
} catch (e) { } catch (e) {
Logger.error('启动连续语音识别失败: $e'); Logger.error('启动连续语音识别失败: ${e.toString()}');
// 如果已经创建了控制器,使用它 // 如果已经创建了控制器,使用它
if (_continuousRecognitionController != null) { if (_continuousRecognitionController != null) {
@ -442,7 +442,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_asrState.value = AsrState.done; _asrState.value = AsrState.done;
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败: $e'); Logger.error('停止连续语音识别失败: ${e.toString()}');
// 强制重置状态 // 强制重置状态
_isListening = false; _isListening = false;
@ -472,7 +472,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_isListening = false; _isListening = false;
_asrState.value = AsrState.notInitialized; _asrState.value = AsrState.notInitialized;
} catch (e) { } catch (e) {
Logger.error('释放语音识别资源失败: $e'); Logger.error('释放语音识别资源失败: ${e.toString()}');
} }
} }
} }

24
lib/data/services/speech_impl/flutter_tts_service.dart

@ -96,7 +96,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('FlutterTtsService 已初始化'); Logger.info('FlutterTtsService 已初始化');
} catch (e) { } catch (e) {
Logger.error('TTS初始化失败: $e'); Logger.error('TTS初始化失败: ${e.toString()}');
} }
} }
@ -118,7 +118,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('支持的语音: ${_supportedVoices.join(", ")}'); Logger.info('支持的语音: ${_supportedVoices.join(", ")}');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('TTS初始化失败: $e'); Logger.error('TTS初始化失败: ${e.toString()}');
return false; return false;
} }
} }
@ -149,7 +149,7 @@ class FlutterTtsService extends GetxService implements TtsService {
_currentVoice.value = _supportedVoices.first; _currentVoice.value = _supportedVoices.first;
} }
} catch (e) { } catch (e) {
Logger.error('加载TTS语音失败: $e'); Logger.error('加载TTS语音失败: ${e.toString()}');
// 添加默认语音 // 添加默认语音
if (_supportedVoices.isEmpty) { if (_supportedVoices.isEmpty) {
@ -175,7 +175,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('设置TTS语音: $voiceName'); Logger.info('设置TTS语音: $voiceName');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('设置TTS语音失败: $e'); Logger.error('设置TTS语音失败: ${e.toString()}');
return false; return false;
} }
} }
@ -200,7 +200,7 @@ class FlutterTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('设置语音参数失败: $e'); Logger.error('设置语音参数失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('设置语音参数失败: $e')); _eventController.add(TtsEvent.error('设置语音参数失败: $e'));
@ -227,7 +227,7 @@ class FlutterTtsService extends GetxService implements TtsService {
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error(e.toString())); _eventController.add(TtsEvent.error(e.toString()));
Logger.error('TTS播放失败: $e'); Logger.error('TTS播放失败: ${e.toString()}');
return false; return false;
} }
} }
@ -288,7 +288,7 @@ class FlutterTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理流式文本失败: $e'); Logger.error('处理流式文本失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error(e.toString())); _eventController.add(TtsEvent.error(e.toString()));
@ -333,7 +333,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS队列播放完成'); Logger.info('TTS队列播放完成');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('等待TTS队列播放完成失败: $e'); Logger.error('等待TTS队列播放完成失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error(e.toString())); _eventController.add(TtsEvent.error(e.toString()));
@ -382,7 +382,7 @@ class FlutterTtsService extends GetxService implements TtsService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('TTS处理队列失败: $e'); Logger.error('TTS处理队列失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error(e.toString())); _eventController.add(TtsEvent.error(e.toString()));
@ -422,7 +422,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS停止播放完成'); Logger.info('TTS停止播放完成');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('TTS停止播放失败: $e'); Logger.error('TTS停止播放失败: ${e.toString()}');
// 即使出错也尝试强制重置状态 // 即使出错也尝试强制重置状态
_isSpeaking.value = false; _isSpeaking.value = false;
@ -443,7 +443,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS资源已释放'); Logger.info('TTS资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放TTS资源失败: $e'); Logger.error('释放TTS资源失败: ${e.toString()}');
} }
} }
@ -462,7 +462,7 @@ class FlutterTtsService extends GetxService implements TtsService {
await flushStream(); await flushStream();
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理文本流时出错: $e'); Logger.error('处理文本流时出错: ${e.toString()}');
return false; return false;
} }
} }

22
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -76,7 +76,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
); );
return true; return true;
} catch (e) { } catch (e) {
Logger.error('TTS初始化失败: $e'); Logger.error('TTS初始化失败: ${e.toString()}');
return false; return false;
} }
} }
@ -88,7 +88,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
Logger.info('设置TTS语音: $voiceName'); Logger.info('设置TTS语音: $voiceName');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('设置TTS语音失败: $e'); Logger.error('设置TTS语音失败: ${e.toString()}');
return false; return false;
} }
} }
@ -127,7 +127,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
_handle(message); _handle(message);
}, },
onError: (error) { onError: (error) {
Logger.error('连接失败: $error'); Logger.error('连接失败: ${error.toString()}');
}, },
onDone: () { onDone: () {
Logger.error('WebSocket 连接已关闭!'); Logger.error('WebSocket 连接已关闭!');
@ -135,7 +135,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
cancelOnError: true, cancelOnError: true,
); );
} catch (e) { } catch (e) {
Logger.error('初始化异常: $e'); Logger.error('初始化异常: ${e.toString()}');
} }
} }
@ -226,7 +226,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
_isSpeaking.value = false; _isSpeaking.value = false;
Logger.error('语音合成失败: $e'); Logger.error('语音合成失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('语音合成失败: $e')); _eventController.add(TtsEvent.error('语音合成失败: $e'));
@ -285,7 +285,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理流式文本失败: $e'); Logger.error('处理流式文本失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('处理流式文本失败: $e')); _eventController.add(TtsEvent.error('处理流式文本失败: $e'));
@ -319,7 +319,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('刷新TTS流失败: $e'); Logger.error('刷新TTS流失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('刷新TTS流失败: $e')); _eventController.add(TtsEvent.error('刷新TTS流失败: $e'));
@ -347,7 +347,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
await speakOnce(item); await speakOnce(item);
} }
} catch (e) { } catch (e) {
Logger.error('处理语音队列出错: $e'); Logger.error('处理语音队列出错: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('处理语音队列出错: $e')); _eventController.add(TtsEvent.error('处理语音队列出错: $e'));
@ -370,7 +370,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
} }
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止语音合成出错: $e'); Logger.error('停止语音合成出错: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('停止语音合成出错: $e')); _eventController.add(TtsEvent.error('停止语音合成出错: $e'));
return false; return false;
@ -387,7 +387,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
await _eventController.close(); await _eventController.close();
_isInitialized = false; _isInitialized = false;
} catch (e) { } catch (e) {
Logger.error('释放资源失败: $e'); Logger.error('释放资源失败: ${e.toString()}');
} }
} }
} }

38
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -99,7 +99,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('火山语音API已初始化'); Logger.info('火山语音API已初始化');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('初始化火山语音API失败: $e'); Logger.error('初始化火山语音API失败: ${e.toString()}');
return false; return false;
} }
} }
@ -151,7 +151,7 @@ class VolcanoAsrApiService implements AsrService {
return resultCompleter.future; return resultCompleter.future;
} catch (e) { } catch (e) {
Logger.error('单次语音识别失败: $e'); Logger.error('单次语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString()); return RecognitionEvent.error(e.toString());
} }
} }
@ -184,7 +184,7 @@ class VolcanoAsrApiService implements AsrService {
return _recognitionController!.stream; return _recognitionController!.stream;
} catch (e) { } catch (e) {
Logger.error('开始连续语音识别失败: $e'); Logger.error('开始连续语音识别失败: ${e.toString()}');
await _cleanupResources(); await _cleanupResources();
// 返回一个包含错误的流 // 返回一个包含错误的流
@ -222,7 +222,7 @@ class VolcanoAsrApiService implements AsrService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败: $e'); Logger.error('停止连续语音识别失败: ${e.toString()}');
// 确保资源被清理 // 确保资源被清理
await _cleanupResources(); await _cleanupResources();
@ -250,7 +250,7 @@ class VolcanoAsrApiService implements AsrService {
_isInitialized = false; _isInitialized = false;
Logger.info('火山语音API资源已释放'); Logger.info('火山语音API资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放火山语音API资源失败: $e'); Logger.error('释放火山语音API资源失败: ${e.toString()}');
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
_isInitialized = false; _isInitialized = false;
} }
@ -284,7 +284,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('录音机初始化成功'); Logger.info('录音机初始化成功');
} catch (e) { } catch (e) {
// _recorder = null; // _recorder = null;
Logger.error('初始化录音机失败: $e'); Logger.error('初始化录音机失败: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -344,7 +344,7 @@ class VolcanoAsrApiService implements AsrService {
_connectionCompleter!.completeError(e); _connectionCompleter!.completeError(e);
} }
Logger.error('连接WebSocket失败: $e'); Logger.error('连接WebSocket失败: ${e.toString()}');
rethrow; rethrow;
} }
@ -432,7 +432,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(heartbeatMsg)); _channel!.sink.add(jsonEncode(heartbeatMsg));
} catch (e) { } catch (e) {
Logger.error('发送心跳包失败: $e'); Logger.error('发送心跳包失败: ${e.toString()}');
} }
} }
} }
@ -461,7 +461,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(startCommand)); _channel!.sink.add(jsonEncode(startCommand));
Logger.info('已发送开始识别指令'); Logger.info('已发送开始识别指令');
} catch (e) { } catch (e) {
Logger.error('发送开始识别指令失败: $e'); Logger.error('发送开始识别指令失败: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -496,7 +496,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('录音已开始'); Logger.info('录音已开始');
} catch (e) { } catch (e) {
_isRecording = false; _isRecording = false;
Logger.error('开始录音失败: $e'); Logger.error('开始录音失败: ${e.toString()}');
rethrow; rethrow;
} }
*/ */
@ -544,7 +544,7 @@ class VolcanoAsrApiService implements AsrService {
await _sendAudioData(audioData); await _sendAudioData(audioData);
} }
} catch (e) { } catch (e) {
Logger.error('捕获和发送音频失败: $e'); Logger.error('捕获和发送音频失败: ${e.toString()}');
} }
*/ */
} }
@ -562,7 +562,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(audioMessage)); _channel!.sink.add(jsonEncode(audioMessage));
} catch (e) { } catch (e) {
Logger.error('发送音频数据失败: $e'); Logger.error('发送音频数据失败: ${e.toString()}');
} }
} }
@ -579,7 +579,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(endCommand)); _channel!.sink.add(jsonEncode(endCommand));
Logger.info('已发送结束指令'); Logger.info('已发送结束指令');
} catch (e) { } catch (e) {
Logger.error('发送结束指令失败: $e'); Logger.error('发送结束指令失败: ${e.toString()}');
} }
} }
@ -596,7 +596,7 @@ class VolcanoAsrApiService implements AsrService {
_isRecording = false; _isRecording = false;
Logger.info('录音已停止'); Logger.info('录音已停止');
} catch (e) { } catch (e) {
Logger.error('停止录音失败: $e'); Logger.error('停止录音失败: ${e.toString()}');
} }
} }
*/ */
@ -641,7 +641,7 @@ class VolcanoAsrApiService implements AsrService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('处理WebSocket消息失败: $e'); Logger.error('处理WebSocket消息失败: ${e.toString()}');
if (_recognitionController != null && !_recognitionController!.isClosed) { if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.addError('处理消息失败: $e'); _recognitionController!.addError('处理消息失败: $e');
@ -675,7 +675,7 @@ class VolcanoAsrApiService implements AsrService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('处理结果消息失败: $e'); Logger.error('处理结果消息失败: ${e.toString()}');
} }
} }
@ -741,7 +741,7 @@ class VolcanoAsrApiService implements AsrService {
/// 处理WebSocket错误 /// 处理WebSocket错误
void _handleWebSocketError(dynamic error) { void _handleWebSocketError(dynamic error) {
Logger.error('WebSocket错误: $error'); Logger.error('WebSocket错误: ${e.toString()}rror');
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError(error); _connectionCompleter!.completeError(error);
@ -789,7 +789,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.close(); _channel!.sink.close();
_channel = null; _channel = null;
} catch (e) { } catch (e) {
Logger.error('关闭WebSocket连接失败: $e'); Logger.error('关闭WebSocket连接失败: ${e.toString()}');
} }
} }
} }
@ -811,4 +811,4 @@ class VolcanoAsrApiService implements AsrService {
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
_isRecognitionEndRequested = false; _isRecognitionEndRequested = false;
} }
} }

16
lib/data/services/speech_impl/volcano_tts_api_service.dart

@ -357,7 +357,7 @@ class VolcanoTtsApiService extends GetxController {
_handleMessage, _handleMessage,
onError: (error) { onError: (error) {
// 直接处理错误,不尝试重连 // 直接处理错误,不尝试重连
Logger.error('WebSocket错误: $error'); Logger.error('WebSocket错误: ${error.toString()}');
if (!_eventController.isClosed) { if (!_eventController.isClosed) {
_eventController.add({ _eventController.add({
'eventType': 'error', 'eventType': 'error',
@ -391,7 +391,7 @@ class VolcanoTtsApiService extends GetxController {
}); });
} }
} catch (e) { } catch (e) {
Logger.error('处理TTS连接超时事件失败: $e'); Logger.error('处理TTS连接超时事件失败: ${e.toString()}');
} }
} }
timeout = null; timeout = null;
@ -427,7 +427,7 @@ class VolcanoTtsApiService extends GetxController {
return result; return result;
} catch (e) { } catch (e) {
// 只记录错误,不尝试重连 // 只记录错误,不尝试重连
Logger.error('连接过程中发生异常: $e'); Logger.error('连接过程中发生异常: ${e.toString()}');
return false; return false;
} finally { } finally {
isConnecting.value = false; isConnecting.value = false;
@ -479,7 +479,7 @@ class VolcanoTtsApiService extends GetxController {
'error': '会话开始超时' 'error': '会话开始超时'
}); });
} catch (e) { } catch (e) {
Logger.error('处理TTS会话超时事件失败: $e'); Logger.error('处理TTS会话超时事件失败: ${e.toString()}');
} }
} }
timeout = null; timeout = null;
@ -515,7 +515,7 @@ class VolcanoTtsApiService extends GetxController {
return result; return result;
} catch (e) { } catch (e) {
// 只记录错误,不尝试恢复 // 只记录错误,不尝试恢复
Logger.error('开始TTS会话失败: $e'); Logger.error('开始TTS会话失败: ${e.toString()}');
return false; return false;
} }
} }
@ -902,10 +902,10 @@ class VolcanoTtsApiService extends GetxController {
'error': error.toString() 'error': error.toString()
}); });
Logger.error('火山语音TTS错误: $error'); Logger.error('火山语音TTS错误: ${error.toString()}');
} catch (e) { } catch (e) {
// 如果在处理错误时发生异常,记录到日志但不再抛出异常 // 如果在处理错误时发生异常,记录到日志但不再抛出异常
Logger.error('处理TTS错误时发生异常: $e,原始错误: $error'); Logger.error('处理TTS错误时发生异常: ${e.toString()},原始错误: ${error.toString()}');
} }
} }
@ -1445,7 +1445,7 @@ class VolcanoTtsApiService extends GetxController {
return success; return success;
} catch (e) { } catch (e) {
Logger.error('TTS一次性合成失败: $e'); Logger.error('TTS一次性合成失败: ${e.toString()}');
// 确保会话结束 // 确保会话结束
endSession(); endSession();
return false; return false;

20
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -54,7 +54,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别服务初始化成功'); Logger.info('Azure 语音识别服务初始化成功');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('Azure 语音识别服务初始化失败: $e'); Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false; _isInitialized = false;
rethrow; rethrow;
} }
@ -69,7 +69,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
try { try {
throw Exception('无效的识别结果格式'); throw Exception('无效的识别结果格式');
} catch (e) { } catch (e) {
Logger.error('语音识别失败: $e'); Logger.error('语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString()); return RecognitionEvent.error(e.toString());
} }
} }
@ -112,7 +112,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = true; _isContinuousRecognitionActive = true;
return _eventStreamController!.stream; return _eventStreamController!.stream;
} catch (e) { } catch (e) {
Logger.error('开始连续语音识别失败: $e'); Logger.error('开始连续语音识别失败: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -152,7 +152,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
}, },
onError: (error) { onError: (error) {
isConnected = false; isConnected = false;
Logger.error('连接失败: $error'); Logger.error('连接失败: ${error.toString()}');
}, },
onDone: () { onDone: () {
isConnected = false; isConnected = false;
@ -161,7 +161,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
cancelOnError: true, cancelOnError: true,
); );
} catch (e) { } catch (e) {
Logger.error("初始化异常----------------$e"); Logger.error("初始化异常----------------${e.toString()}");
} }
} }
@ -185,7 +185,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
} }
} }
} }
Logger.info( Logger.info(
"识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role"); "识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role");
if (msg.isNotEmpty) { if (msg.isNotEmpty) {
if (data['cn']['st']['type'] == "0") { if (data['cn']['st']['type'] == "0") {
@ -206,7 +206,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
} }
} }
} catch (e) { } catch (e) {
Logger.error("接受消息异常 $e"); Logger.error("接受消息异常 ${e.toString()}");
} }
} }
@ -242,7 +242,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败: $e'); Logger.error('停止连续语音识别失败: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -266,8 +266,8 @@ class XunfeiAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别资源已释放'); Logger.info('Azure 语音识别资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放语音识别资源失败: $e'); Logger.error('释放语音识别资源失败: ${e.toString()}');
_isInitialized = false; _isInitialized = false;
} }
} }
} }

18
lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart

@ -76,7 +76,7 @@ class AndroidVoiceInteractionService extends GetxService {
_handleVoiceInteractionEvent(event); _handleVoiceInteractionEvent(event);
} }
}, onError: (error) { }, onError: (error) {
Logger.error('语音交互事件通道错误: $error'); Logger.error('语音交互事件通道错误: ${error.toString()}');
}); });
} }
@ -106,7 +106,7 @@ class AndroidVoiceInteractionService extends GetxService {
Logger.info('语音交互服务初始化完成'); Logger.info('语音交互服务初始化完成');
return this; return this;
} catch (e) { } catch (e) {
Logger.error('语音交互服务初始化失败: $e'); Logger.error('语音交互服务初始化失败: ${e.toString()}');
return this; return this;
} }
} }
@ -123,7 +123,7 @@ class AndroidVoiceInteractionService extends GetxService {
_isServiceRunning.value = isRunning; _isServiceRunning.value = isRunning;
Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}');
} catch (e) { } catch (e) {
Logger.error('检查语音交互服务状态失败: $e'); Logger.error('检查语音交互服务状态失败: ${e.toString()}');
_isServiceRunning.value = false; _isServiceRunning.value = false;
} }
} }
@ -151,7 +151,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('启动语音交互服务失败: $e'); Logger.error('启动语音交互服务失败: ${e.toString()}');
return false; return false;
} }
} }
@ -175,7 +175,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('停止语音交互服务失败: $e'); Logger.error('停止语音交互服务失败: ${e.toString()}');
return false; return false;
} }
} }
@ -199,7 +199,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('暂停语音交互失败: $e'); Logger.error('暂停语音交互失败: ${e.toString()}');
return false; return false;
} }
} }
@ -213,7 +213,7 @@ class AndroidVoiceInteractionService extends GetxService {
final int timestamp = eventMap['timestamp'] as int? ?? 0; final int timestamp = eventMap['timestamp'] as int? ?? 0;
// 添加时间戳日志,帮助调试 // 添加时间戳日志,帮助调试
Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); Logger.info('收到原生端事件: ${eventType.toString()}, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}');
switch (eventType) { switch (eventType) {
case 'chatHistory': case 'chatHistory':
@ -252,7 +252,7 @@ class AndroidVoiceInteractionService extends GetxService {
break; break;
default: default:
Logger.warning('收到未知类型的语音交互事件: $eventType'); Logger.warning('收到未知类型的语音交互事件: ${eventType.toString()}');
break; break;
} }
} }
@ -297,7 +297,7 @@ class AndroidVoiceInteractionService extends GetxService {
// 保存更新后的历史记录 // 保存更新后的历史记录
await chatHistoryService.saveHistory(agentId, existingMessages); await chatHistoryService.saveHistory(agentId, existingMessages);
} catch (e) { } catch (e) {
Logger.error('保存聊天记录失败: $e'); Logger.error('保存聊天记录失败: ${e.toString()}');
} }
} }

12
lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart

@ -66,7 +66,7 @@ class IosVoiceInteractionService extends GetxService {
// 保持idle状态,不自动启动语音识别 // 保持idle状态,不自动启动语音识别
Logger.info('iOS语音交互服务初始化完成,处于idle状态'); Logger.info('iOS语音交互服务初始化完成,处于idle状态');
} catch (e) { } catch (e) {
Logger.error('iOS语音交互服务初始化失败: $e'); Logger.error('iOS语音交互服务初始化失败: ${e.toString()}');
} }
return this; return this;
@ -110,13 +110,13 @@ class IosVoiceInteractionService extends GetxService {
// 监听识别结果 // 监听识别结果
_recognitionSubscription = _recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent, onError: (error) { recognitionStream.listen(_handleRecognitionEvent, onError: (error) {
Logger.error('语音识别错误: $error'); Logger.error('语音识别错误: ${error.toString()}');
_startContinuousListening(); // 尝试重新启动 _startContinuousListening(); // 尝试重新启动
}); });
Logger.info('开始连续语音识别'); Logger.info('开始连续语音识别');
} catch (e) { } catch (e) {
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: ${e.toString()}');
} }
} }
@ -263,7 +263,7 @@ class IosVoiceInteractionService extends GetxService {
// 处理完成 // 处理完成
isProcessingAI.value = false; isProcessingAI.value = false;
} catch (e) { } catch (e) {
Logger.error('AI响应处理失败: $e'); Logger.error('AI响应处理失败: ${e.toString()}');
// 错误恢复 // 错误恢复
isProcessingAI.value = false; isProcessingAI.value = false;
} }
@ -437,7 +437,7 @@ class IosVoiceInteractionService extends GetxService {
Logger.info('保存聊天历史记录成功'); Logger.info('保存聊天历史记录成功');
} catch (e) { } catch (e) {
Logger.error('保存聊天历史记录失败: $e'); Logger.error('保存聊天历史记录失败: ${e.toString()}');
} }
} }
@ -463,4 +463,4 @@ class IosVoiceInteractionService extends GetxService {
_eventStreamController.add(event); _eventStreamController.add(event);
Logger.info('已发送识别开始事件'); Logger.info('已发送识别开始事件');
} }
} }

123
lib/modules/agent/controllers/agent_controller.dart

@ -5,12 +5,12 @@ import 'package:deep_voice/data/services/music_manager.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart'; import 'package:agent_service/agent_service.dart';
import 'package:logger/logger.dart';
import 'package:chat_storage/chat_storage.dart'; import 'package:chat_storage/chat_storage.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart'; import 'package:app_settings/app_settings.dart';
import 'package:deep_voice/core/utils/logger.dart';
class Message { class Message {
final bool isUser; //标识信息是否来自用户还是AI final bool isUser; //标识信息是否来自用户还是AI
@ -36,8 +36,6 @@ class Message {
class AgentController extends GetxController { class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式 static AgentController get to => Get.find(); //单例访问模式
final logger = Logger();
// 聊天消息列表 // 聊天消息列表
final messages = <Message>[].obs; final messages = <Message>[].obs;
@ -78,6 +76,8 @@ class AgentController extends GetxController {
bool _isNewAssistantResponse = true; bool _isNewAssistantResponse = true;
String _currentResponseId = ''; String _currentResponseId = '';
final TAG = 'AgentController';
// 会话ID // 会话ID
final String sessionId = 'default_agent'; final String sessionId = 'default_agent';
@ -109,7 +109,7 @@ class AgentController extends GetxController {
isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true; isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true;
} catch (e) { } catch (e) {
// 如果读取失败,使用默认值 // 如果读取失败,使用默认值
logger.e('加载TTS偏好设置失败: $e'); Logger.e(TAG, '加载TTS偏好设置失败: $e');
isTtsEnabled.value = true; isTtsEnabled.value = true;
} }
} }
@ -128,14 +128,14 @@ class AgentController extends GetxController {
final storage = GetStorage(); final storage = GetStorage();
await storage.write('agent_tts_enabled', isTtsEnabled.value); await storage.write('agent_tts_enabled', isTtsEnabled.value);
} catch (e) { } catch (e) {
logger.e('保存TTS偏好设置失败: $e'); Logger.e(TAG, '保存TTS偏好设置失败: $e');
} }
} }
// 加载聊天历史记录 // 加载聊天历史记录
Future<void> _loadChatHistory() async { Future<void> _loadChatHistory() async {
try { try {
logger.i('正在加载聊天历史...'); Logger.i(TAG, '正在加载聊天历史...');
final result = await ChatStorage.getMessages( final result = await ChatStorage.getMessages(
sessionId: sessionId, sessionId: sessionId,
page: 1, page: 1,
@ -160,14 +160,14 @@ class AgentController extends GetxController {
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messages.assignAll(historyMessages); messages.assignAll(historyMessages);
logger.i('成功加载 ${historyMessages.length} 条聊天记录'); Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录');
} else { } else {
// 没有历史记录时添加欢迎消息 // 没有历史记录时添加欢迎消息
_addWelcomeMessage(); _addWelcomeMessage();
logger.i('没有历史记录,添加欢迎消息'); Logger.i(TAG, '没有历史记录,添加欢迎消息');
} }
} catch (e) { } catch (e) {
logger.e('加载聊天历史失败: $e'); Logger.e(TAG, '加载聊天历史失败: $e');
_addWelcomeMessage(); _addWelcomeMessage();
} finally { } finally {
isHistoryLoaded.value = true; isHistoryLoaded.value = true;
@ -272,12 +272,14 @@ class AgentController extends GetxController {
case AgentServiceEventType.ttsStarted: case AgentServiceEventType.ttsStarted:
isSpeaking.value = true; isSpeaking.value = true;
Logger.i(TAG, 'TTS开始播放');
break; break;
case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsCompleted:
case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled: case AgentServiceEventType.ttsCanceled:
isSpeaking.value = false; isSpeaking.value = false;
Logger.i(TAG, 'TTS停止播放');
break; break;
case AgentServiceEventType.imageProcessing: case AgentServiceEventType.imageProcessing:
@ -339,7 +341,7 @@ class AgentController extends GetxController {
messages.refresh(); messages.refresh();
} }
logger.e('代理服务错误: ${event.data['message']}'); Logger.e(TAG, '代理服务错误: ${event.data['message']}');
break; break;
case AgentServiceEventType.autoStop: case AgentServiceEventType.autoStop:
@ -373,14 +375,14 @@ class AgentController extends GetxController {
case AgentServiceEventType.functionCall: case AgentServiceEventType.functionCall:
final functionName = event.data['name'] ?? ''; final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? ''; final arguments = event.data['arguments'] ?? '';
// logger.i('Fultter 开始调用 mcp: $functionName, $arguments'); // debugPrint('Fultter 开始调用 mcp: $functionName, $arguments');
break; break;
case AgentServiceEventType.functionCallResult: case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? ''; final responseId = event.data['responseId'] ?? '';
final result = event.data['result'] ?? ''; final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? ''; final functionCall = event.data['function_call'] ?? '';
logger.i('Flutter 结束调用 mcp: $result, $functionCall'); Logger.i(TAG, 'Flutter 结束调用 mcp: $result, $functionCall');
if (result.isNotEmpty) { if (result.isNotEmpty) {
final parsedResult = jsonDecode(result); final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"]; final meta = parsedResult["meta"];
@ -390,7 +392,7 @@ final responseId = event.data['responseId'] ?? '';
try { try {
metaResult = jsonDecode(meta); metaResult = jsonDecode(meta);
} catch (e) { } catch (e) {
print('解析meta数据失败: $e'); Logger.e(TAG, '解析meta数据失败: $e');
} }
} }
if(metaResult.isNotEmpty){ if(metaResult.isNotEmpty){
@ -452,7 +454,7 @@ final responseId = event.data['responseId'] ?? '';
await AgentService.processTextInput(text, await AgentService.processTextInput(text,
speakResponse: isTtsEnabled.value); speakResponse: isTtsEnabled.value);
} catch (e) { } catch (e) {
logger.e('发送文本消息失败: $e'); Logger.e(TAG, '发送文本消息失败: $e');
isProcessing.value = false; isProcessing.value = false;
} }
} }
@ -460,7 +462,7 @@ final responseId = event.data['responseId'] ?? '';
// 发送图片消息 // 发送图片消息
Future<void> sendImageMessage(String imagePath, {String? text}) async { Future<void> sendImageMessage(String imagePath, {String? text}) async {
if (imagePath.isEmpty || !File(imagePath).existsSync()) { if (imagePath.isEmpty || !File(imagePath).existsSync()) {
logger.e('图片不存在: $imagePath'); Logger.e(TAG, '图片不存在: $imagePath');
return; return;
} }
@ -494,7 +496,7 @@ final responseId = event.data['responseId'] ?? '';
speakResponse: isTtsEnabled.value, speakResponse: isTtsEnabled.value,
); );
} catch (e) { } catch (e) {
logger.e('处理图片失败: $e'); Logger.e(TAG, '处理图片失败: $e');
isProcessing.value = false; isProcessing.value = false;
isImageProcessing.value = false; isImageProcessing.value = false;
} }
@ -531,36 +533,73 @@ final responseId = event.data['responseId'] ?? '';
// 检查麦克风权限 // 检查麦克风权限
Future<bool> _checkMicrophonePermission() async { Future<bool> _checkMicrophonePermission() async {
try { try {
PermissionStatus status = await Permission.microphone.status; // 创建待请求权限列表
List<Permission> permissionsToRequest = [];
// 如果已授权,直接返回 true
if (status.isGranted) { if (Platform.isAndroid) {
// Android 需要考虑SDK版本不同的权限要求
permissionsToRequest.add(Permission.microphone);
} else if (Platform.isIOS) {
// iOS 只需要麦克风权限
permissionsToRequest.add(Permission.microphone);
}
// 批量请求所有需要的权限
final statuses = await permissionsToRequest.request();
// 检查是否所有权限都已授予
bool allGranted = true;
Permission? deniedPermission;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
deniedPermission = permission;
Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status');
}
});
// 单独检查麦克风权限状态,因为这是主要需求
final micStatus = await Permission.microphone.status;
// 如果所有权限都已授予,或至少麦克风权限已授予,返回true
if (allGranted || micStatus.isGranted) {
return true; return true;
} }
// 如果被永久拒绝,提示用户前往设置页面 // 处理特定权限被永久拒绝的情况
if (status.isPermanentlyDenied) { if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true); _showPermissionDialog(true);
return false; return false;
} }
// 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限 // 普通拒绝情况
if (status.isDenied) { if (deniedPermission != null) {
status = await Permission.microphone.request(); _showPermissionDialog(false);
if (status.isGranted) {
return true;
} else {
_showPermissionDialog(false);
return false;
}
} }
return false; return false;
} catch (e) { } catch (e) {
logger.e('检查麦克风权限失败: $e'); Logger.e(TAG, '检查麦克风权限失败: $e');
return false; return false;
} }
} }
// 获取Android SDK版本
Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) {
try {
// 这里我们简单返回一个较高的值,确保请求所有可能需要的权限
// 在实际应用中,你可能需要使用platform channel获取真实的SDK版本
return 31; // 对应Android 12
} catch (e) {
Logger.e(TAG, '获取Android SDK版本失败: $e');
}
}
return 0;
}
// 显示权限提示对话框 // 显示权限提示对话框
void _showPermissionDialog(bool isPermanent) { void _showPermissionDialog(bool isPermanent) {
@ -605,7 +644,7 @@ final responseId = event.data['responseId'] ?? '';
try { try {
await AgentService.startConversation(); await AgentService.startConversation();
} catch (e) { } catch (e) {
logger.e('启动语音输入失败: $e'); Logger.e(TAG, '启动语音输入失败: $e');
} }
} }
@ -616,7 +655,7 @@ final responseId = event.data['responseId'] ?? '';
try { try {
await AgentService.stopConversation(); await AgentService.stopConversation();
} catch (e) { } catch (e) {
logger.e('停止语音输入失败: $e'); Logger.e(TAG, '停止语音输入失败: $e');
} }
} }
@ -634,7 +673,7 @@ final responseId = event.data['responseId'] ?? '';
// 重置回复状态 // 重置回复状态
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
} catch (e) { } catch (e) {
logger.e('清除聊天历史失败: $e'); Logger.e(TAG, '清除聊天历史失败: $e');
} }
} }
@ -656,7 +695,7 @@ final responseId = event.data['responseId'] ?? '';
isImageInputActive.value = true; isImageInputActive.value = true;
} }
} catch (e) { } catch (e) {
logger.e('选择图片失败: $e'); Logger.e(TAG, '选择图片失败: $e');
} }
} }
@ -678,7 +717,7 @@ final responseId = event.data['responseId'] ?? '';
isImageInputActive.value = true; isImageInputActive.value = true;
} }
} catch (e) { } catch (e) {
logger.e('拍照失败: $e'); Logger.e(TAG, '拍照失败: $e');
} }
} }
@ -720,7 +759,7 @@ final responseId = event.data['responseId'] ?? '';
// 播放音乐 // 播放音乐
void musicStatus() async { void musicStatus() async {
var status = await _musiceManager.getStatus(); var status = await _musiceManager.getStatus();
// logger.i('获取当前音乐播放状态 $status'); // debugPrint('获取当前音乐播放状态 $status');
isPlaying.value = status['isPlaying']; isPlaying.value = status['isPlaying'];
currentUrl.value = status['url']; currentUrl.value = status['url'];
} }

10
lib/modules/chat/controllers/chat_controller.dart

@ -451,7 +451,7 @@ class ChatController extends GetxController {
} }
isPlay.value = true; isPlay.value = true;
} catch (e) { } catch (e) {
Logger.error('加载异常: $e'); Logger.error('加载异常: ${e.toString()}');
} }
} }
@ -469,7 +469,7 @@ class ChatController extends GetxController {
isPlay.value = true; isPlay.value = true;
audiourl.value = card['url'] as String; audiourl.value = card['url'] as String;
} catch (e) { } catch (e) {
Logger.error('加载异常: $e'); Logger.error('加载异常: ${e.toString()}');
} }
} }
@ -479,7 +479,7 @@ class ChatController extends GetxController {
_spotifService.pause(); _spotifService.pause();
isPlay.value = false; isPlay.value = false;
} catch (e) { } catch (e) {
Logger.error('加载异常: $e'); Logger.error('加载异常: ${e.toString()}');
} }
} }
@ -872,7 +872,7 @@ class ChatController extends GetxController {
} }
// 删除数据库中的历史记录 // 删除数据库中的历史记录
ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) { ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) {
Logger.error('清空数据库聊天记录失败: $e'); Logger.error('清空数据库聊天记录失败: ${e.toString()}');
}); });
} }
@ -1003,4 +1003,4 @@ class ChatController extends GetxController {
.abs() < .abs() <
5000); 5000);
} }
} }

11
lib/modules/jieli_test/bindings/jieli_test_binding.dart

@ -1,11 +0,0 @@
import 'package:get/get.dart';
import '../controllers/jieli_test_controller.dart';
class JieliTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<JieliTestController>(
() => JieliTestController(),
);
}
}

323
lib/modules/jieli_test/controllers/jieli_test_controller.dart

@ -1,323 +0,0 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart' hide ConnectionState;
import 'package:get/get.dart';
import 'package:jieli/jieli.dart';
/// 杰理测试控制器
class JieliTestController extends GetxController {
// Jieli插件实例
late Jieli jieli;
// 状态变量
var isScanning = false.obs;
var deviceInfo = '未获取'.obs;
var connectStatus = '未连接'.obs;
var currentDevice = Rx<BleDevice?>(null);
var scannedDevices = RxList<BleDevice>([]);
var wakeupReceived = false.obs;
var audioDataSize = 0.obs;
var receivedFrames = 0.obs;
var audioDataReceiving = false.obs;
var volumeCommandResult = ''.obs;
// 事件订阅
StreamSubscription? _scanSubscription;
StreamSubscription? _connectionSubscription;
StreamSubscription? _wakeupSubscription;
StreamSubscription? _audioSubscription;
StreamSubscription? _commandResultSubscription;
@override
void onInit() {
super.onInit();
_initializeJieli();
}
@override
void onClose() {
_cancelAllSubscriptions();
_disposeJieli();
super.onClose();
}
/// 取消所有订阅
void _cancelAllSubscriptions() {
_scanSubscription?.cancel();
_connectionSubscription?.cancel();
_wakeupSubscription?.cancel();
_audioSubscription?.cancel();
_commandResultSubscription?.cancel();
}
/// 初始化杰理SDK
void _initializeJieli() {
try {
jieli = Jieli();
_setupEventListeners();
} catch (e) {
// 初始化错误处理
}
}
/// 设置事件监听器
void _setupEventListeners() {
// 监听扫描结果
_scanSubscription = jieli.scanResults.listen((devices) {
scannedDevices.assignAll(devices);
});
// 监听连接状态
_connectionSubscription = jieli.connectionState.listen((state) {
switch (state) {
case ConnectionState.connected:
connectStatus.value = '已连接';
break;
case ConnectionState.disconnected:
connectStatus.value = '已断开';
currentDevice.value = null;
break;
case ConnectionState.connecting:
connectStatus.value = '连接中...';
break;
case ConnectionState.disconnecting:
connectStatus.value = '断开中...';
break;
case ConnectionState.error:
connectStatus.value = '连接错误';
break;
}
});
// 监听唤醒信号
_wakeupSubscription = jieli.wakeupStream.listen((wakeup) {
wakeupReceived.value = true;
Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号');
});
// 监听音频数据
_audioSubscription = jieli.audioDataStream.listen((data) {
// 更新音频数据统计
audioDataSize.value += data.length;
receivedFrames.value++;
audioDataReceiving.value = true;
});
// 监听命令结果
_commandResultSubscription = jieli.commandResultStream.listen((result) {
// 处理音量调整命令结果
if (result.commandType == Jieli.commandTypeVolume) {
volumeCommandResult.value = result.success ? '音量调整成功' : '音量调整失败';
Get.snackbar(
result.success ? '音量调整成功' : '音量调整失败',
result.success ? '设备已更新音量' : '设备未能更新音量',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: result.success ? Colors.green.withOpacity(0.7) : Colors.red.withOpacity(0.7),
colorText: Colors.white,
duration: Duration(seconds: 2),
);
}
});
}
/// 检查蓝牙状态
Future<bool> checkBluetoothStatus() async {
try {
final isEnabled = await jieli.isBluetoothEnabled();
if (!isEnabled) {
return await jieli.requestBluetoothEnable();
}
return true;
} catch (e) {
return false;
}
}
/// 开始扫描设备
Future<void> startScan() async {
if (isScanning.value) return;
final bluetoothReady = await checkBluetoothStatus();
if (!bluetoothReady) {
Get.snackbar('蓝牙未启用', '请启用蓝牙后再试');
return;
}
try {
isScanning.value = true;
// 清空之前的设备列表
scannedDevices.clear();
// 开始扫描,10秒后自动停止
final success = await jieli.startScan(scanTimeout: 10000);
if (!success) {
isScanning.value = false;
Get.snackbar('扫描失败', '启动扫描设备失败,请检查蓝牙权限');
}
// 10秒后自动更新UI状态
Future.delayed(Duration(milliseconds: 10000), () {
isScanning.value = false;
});
} catch (e) {
isScanning.value = false;
Get.snackbar('扫描错误', e.toString());
}
}
/// 停止扫描
Future<void> stopScan() async {
if (!isScanning.value) return;
try {
await jieli.stopScan();
isScanning.value = false;
} catch (e) {
// 停止扫描错误处理
}
}
/// 连接到指定设备
Future<void> connectToDevice(BleDevice device) async {
try {
// 先断开当前连接
if (currentDevice.value != null) {
await disconnectDevice();
}
// 更新UI状态
connectStatus.value = '连接中...';
// 重置音频和唤醒相关的状态
wakeupReceived.value = false;
audioDataSize.value = 0;
receivedFrames.value = 0;
audioDataReceiving.value = false;
// 连接设备
final success = await jieli.connect(device.id);
if (success) {
// 连接成功,更新当前设备
currentDevice.value = device;
deviceInfo.value = '设备名称: ${device.name}\n设备ID: ${device.id}\nRSSI: ${device.rssi} dBm';
} else {
deviceInfo.value = '连接失败';
}
} catch (e) {
deviceInfo.value = '连接错误: ${e.toString()}';
}
}
/// 断开设备连接
Future<void> disconnectDevice() async {
if (currentDevice.value == null) return;
try {
connectStatus.value = '断开中...';
final success = await jieli.disconnect();
if (success) {
deviceInfo.value = '未连接设备';
}
} catch (e) {
// 断开连接错误处理
}
}
/// 发送开始录音命令
Future<void> sendStartRecordingCommand() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
final success = await jieli.sendStartRecordingCommand();
if (success) {
Get.snackbar('命令已发送', '已发送开始录音命令');
} else {
Get.snackbar('命令发送失败', '发送开始录音命令失败');
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
}
}
/// 发送停止录音命令
Future<void> sendStopRecordingCommand() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
final success = await jieli.sendStopRecordingCommand();
if (success) {
Get.snackbar('命令已发送', '已发送停止录音命令');
} else {
Get.snackbar('命令发送失败', '发送停止录音命令失败');
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
}
}
/// 增加音量
Future<void> increaseVolume() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
volumeCommandResult.value = '发送中...';
final success = await jieli.increaseVolume();
if (!success) {
Get.snackbar('命令发送失败', '发送增加音量命令失败');
volumeCommandResult.value = '发送失败';
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
volumeCommandResult.value = '发送出错';
}
}
/// 减小音量
Future<void> decreaseVolume() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
volumeCommandResult.value = '发送中...';
final success = await jieli.decreaseVolume();
if (!success) {
Get.snackbar('命令发送失败', '发送减小音量命令失败');
volumeCommandResult.value = '发送失败';
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
volumeCommandResult.value = '发送出错';
}
}
/// 清理资源
void _disposeJieli() {
try {
jieli.dispose();
} catch (e) {
// 释放资源错误处理
}
}
}

568
lib/modules/jieli_test/views/jieli_test_view.dart

@ -1,568 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/jieli_test_controller.dart';
import 'package:jieli/jieli.dart';
class JieliTestView extends GetView<JieliTestController> {
const JieliTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
title: Text(
'杰理蓝牙测试',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
),
body: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildStatusCard(isDarkMode),
SizedBox(height: 16.h),
_buildActionButtons(isDarkMode),
SizedBox(height: 16.h),
_buildDeviceInfo(isDarkMode),
SizedBox(height: 16.h),
_buildDevicesList(isDarkMode),
],
),
),
);
}
Widget _buildStatusCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'连接状态: ',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
Obx(() => Text(
controller.connectStatus.value,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: _getStatusColor(controller.connectStatus.value),
),
)),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(() => _buildStatusChip(
label: '唤醒信号',
isActive: controller.wakeupReceived.value,
activeColor: Colors.orange,
isDarkMode: isDarkMode,
)),
Obx(() => _buildStatusChip(
label: '音频数据',
isActive: controller.audioDataReceiving.value,
activeColor: Colors.green,
isDarkMode: isDarkMode,
)),
],
),
SizedBox(height: 8.h),
Obx(() => controller.audioDataReceiving.value
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'音频数据统计:',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
SizedBox(height: 4.h),
Text(
'已接收 ${(controller.audioDataSize.value / 1024).toStringAsFixed(2)} KB',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
'共 ${controller.receivedFrames.value} 帧',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
)
: SizedBox.shrink()
),
],
),
);
}
Widget _buildStatusChip({
required String label,
required bool isActive,
required Color activeColor,
required bool isDarkMode,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: isActive
? activeColor
: (isDarkMode ? Colors.grey[800] : Colors.grey[200]),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: isActive ? Colors.white : Colors.grey,
shape: BoxShape.circle,
),
),
SizedBox(width: 6.w),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: isActive
? Colors.white
: (isDarkMode ? Colors.white70 : Colors.black54),
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
),
),
],
),
);
}
Widget _buildActionButtons(bool isDarkMode) {
return Column(
children: [
Row(
children: [
Expanded(
child: Obx(() => ElevatedButton.icon(
onPressed: controller.isScanning.value
? controller.stopScan
: controller.startScan,
icon: Icon(
controller.isScanning.value ? Icons.stop : Icons.bluetooth_searching,
size: 18.sp,
),
label: Text(
controller.isScanning.value ? '停止扫描' : '扫描设备',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: controller.isScanning.value
? Colors.red[600]
: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
)),
),
],
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.sendStartRecordingCommand,
icon: Icon(
Icons.mic,
size: 18.sp,
),
label: Text(
'开始录音',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
SizedBox(width: 8.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.sendStopRecordingCommand,
icon: Icon(
Icons.stop_circle,
size: 18.sp,
),
label: Text(
'停止录音',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
],
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.increaseVolume,
icon: Icon(
Icons.volume_up,
size: 18.sp,
),
label: Text(
'增加音量',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
SizedBox(width: 8.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.decreaseVolume,
icon: Icon(
Icons.volume_down,
size: 18.sp,
),
label: Text(
'减小音量',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
],
),
SizedBox(height: 8.h),
Obx(() => controller.volumeCommandResult.value.isNotEmpty
? Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
decoration: BoxDecoration(
color: controller.volumeCommandResult.value.contains('成功')
? Colors.green[100]
: (controller.volumeCommandResult.value == '发送中...'
? Colors.blue[100]
: Colors.red[100]),
borderRadius: BorderRadius.circular(8.r),
),
child: Text(
'音量控制: ${controller.volumeCommandResult.value}',
style: TextStyle(
fontSize: 13.sp,
color: controller.volumeCommandResult.value.contains('成功')
? Colors.green[900]
: (controller.volumeCommandResult.value == '发送中...'
? Colors.blue[900]
: Colors.red[900]),
),
textAlign: TextAlign.center,
),
)
: SizedBox.shrink()
),
],
);
}
Widget _buildDeviceInfo(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备信息:',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.black : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
),
child: Obx(() => Text(
controller.deviceInfo.value,
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
height: 1.4,
),
)),
),
SizedBox(height: 16.h),
Obx(() => controller.currentDevice.value != null
? ElevatedButton.icon(
onPressed: controller.disconnectDevice,
icon: Icon(
Icons.bluetooth_disabled,
size: 16.sp,
),
label: Text(
'断开连接',
style: TextStyle(fontSize: 13.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
)
: SizedBox.shrink()
),
],
),
);
}
Widget _buildDevicesList(bool isDarkMode) {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'扫描到的设备',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
Obx(() => Text(
'${controller.scannedDevices.length}个设备',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
)),
],
),
),
Divider(height: 1, thickness: 1, color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
Expanded(
child: Obx(() => controller.scannedDevices.isEmpty
? Center(
child: controller.isScanning.value
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16.h),
Text(
'正在扫描...',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
)
: Text(
'未扫描到设备',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
)
: ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8.h),
itemCount: controller.scannedDevices.length,
separatorBuilder: (context, index) => Divider(
height: 1,
thickness: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
),
itemBuilder: (context, index) {
final device = controller.scannedDevices[index];
return ListTile(
dense: true,
title: Text(
device.name.isEmpty ? '未知设备' : device.name,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备ID: ${device.id}',
style: TextStyle(
fontSize: 11.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
'RSSI: ${device.rssi} dBm',
style: TextStyle(
fontSize: 11.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
),
trailing: Obx(() => controller.currentDevice.value?.id == device.id
? Chip(
label: Text(
'已连接',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white,
),
),
backgroundColor: Colors.green,
padding: EdgeInsets.symmetric(horizontal: 8.w),
)
: ElevatedButton(
onPressed: () => controller.connectToDevice(device),
child: Text(
'连接',
style: TextStyle(fontSize: 12.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.r),
),
),
),
),
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
isThreeLine: true,
);
},
),
),
),
],
),
),
);
}
Color _getStatusColor(String status) {
switch (status) {
case '连接中...':
return Colors.orange;
case '已连接':
return Colors.green;
case '连接失败':
return Colors.red;
case '已断开':
return Colors.grey;
default:
return Colors.grey;
}
}
}

8
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -258,7 +258,7 @@ class MeetingDetailsController extends GetxController
Logger.debug('taskId: $taskId'); Logger.debug('taskId: $taskId');
return taskId; return taskId;
} catch (e) { } catch (e) {
Logger.error('Submit error: $e'); Logger.error('Submit error: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -290,7 +290,7 @@ class MeetingDetailsController extends GetxController
final respDic = jsonDecode(response.body); final respDic = jsonDecode(response.body);
return respDic; return respDic;
} catch (e) { } catch (e) {
Logger.error('Query error: $e'); Logger.error('Query error: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -322,7 +322,7 @@ class MeetingDetailsController extends GetxController
} }
} }
} catch (e) { } catch (e) {
Logger.error('Error occurred: $e'); Logger.error('Error occurred: ${e.toString()}');
} }
} }
@ -357,4 +357,4 @@ class MeetingDetailsController extends GetxController
} }
allText = text; allText = text;
} }
} }

124
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -22,7 +22,7 @@ class OtaUpgradeController extends GetxController {
final isUpdateAvailable = true.obs; final isUpdateAvailable = true.obs;
final RxDouble progress = 0.0.obs; final RxDouble progress = 0.0.obs;
final JlOta jla = JlOta();
// 蓝牙服务 // 蓝牙服务
final _bleManager = Get.find<BleManager>(); final _bleManager = Get.find<BleManager>();
@override @override
@ -53,68 +53,65 @@ class OtaUpgradeController extends GetxController {
} }
Future<void> _getCurrentVersion() async { Future<void> _getCurrentVersion() async {
JlOta().getPlatformVersion(); try {
//try { // 添加获取设备信息的重试机制
DeviceInfo? deviceInfo = _bleManager.deviceInfo.value;
// // 添加获取设备信息的重试机制
// DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; // 第一次检查设备信息
if (deviceInfo == null) {
// // 第一次检查设备信息 // 显示加载提示
// if (deviceInfo == null) { isFetchingDeviceInfo.value = true;
// // 显示加载提示
// isFetchingDeviceInfo.value = true; // 尝试重新获取设备信息
await _bleManager.fetchDeviceInfo();
// // 尝试重新获取设备信息
// await _bleManager.fetchDeviceInfo(); // 等待后再次获取设备信息
deviceInfo = _bleManager.deviceInfo.value;
// // 等待后再次获取设备信息 isFetchingDeviceInfo.value = false;
// deviceInfo = _bleManager.deviceInfo.value;
// isFetchingDeviceInfo.value = false; // 仍然为空则抛出异常
if (deviceInfo == null) {
// // 仍然为空则抛出异常 throw const FormatException('无法获取设备信息,请检查设备连接');
// if (deviceInfo == null) { }
// throw const FormatException('无法获取设备信息,请检查设备连接'); }
// }
// } // 检查固件信息有效性
if (deviceInfo.firmwareVersion.isEmpty ||
// // 检查固件信息有效性 deviceInfo.firmwareProductId.isEmpty) {
// if (deviceInfo.firmwareVersion.isEmpty throw const FormatException('设备固件信息不完整');
// // || }
// // deviceInfo.firmwareProductId.isEmpty
// ) { // 正常流程
// throw const FormatException('设备固件信息不完整'); currentVersion.value = deviceInfo.firmwareVersion;
// } print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
// API请求
// // 正常流程 final response = await Api.getequipmentinfo({
// currentVersion.value = "0"; //deviceInfo.firmwareVersion; "id": deviceInfo.firmwareProductId,
});
// // API请求
// final response = await Api.getequipmentinfo({ // 处理API响应
// "id": "s3", if (response['info'] == null) {
// }); throw const FormatException('API 响应格式错误');
}
// // 处理API响应
// if (response['info'] == null) { final resp = OtaConfigResp.fromJson(response);
// throw const FormatException('API 响应格式错误'); print('getequipmentinfo,resp.systemversion: ${resp.systemversion}');
// } latestVersion.value = "1";
updatepackageaddress = resp.updatepackageaddress!;
// final resp = OtaConfigResp.fromJson(response);
// latestVersion.value = resp.systemversion!; // 版本检查
// updatepackageaddress = resp.updatepackageaddress!; isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!);
} catch (e) {
// // 版本检查 // 统一错误处理
// isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!); errorMessage.value = e.toString();
// } catch (e) { Get.snackbar('错误', e.toString(),
// // 统一错误处理 snackPosition: SnackPosition.BOTTOM,
// errorMessage.value = e.toString(); backgroundColor: Colors.red,
// Get.snackbar('错误', e.toString(), colorText: Colors.white);
// snackPosition: SnackPosition.BOTTOM, // 重要:重新抛出异常以便上层处理(可选)
// backgroundColor: Colors.red, } finally {
// colorText: Colors.white); isFetchingDeviceInfo.value = false;
// // 重要:重新抛出异常以便上层处理(可选) }
// } finally {
// isFetchingDeviceInfo.value = false;
// }
} }
// 版本比较方法优化 // 版本比较方法优化
@ -295,6 +292,7 @@ class OtaUpgradeController extends GetxController {
// 开始升级 // 开始升级
void _installApp(String path) async { void _installApp(String path) async {
jla.getPlatformVersion();
// JlOtaPlugin() // JlOtaPlugin()
// final File file = File(path); // final File file = File(path);
// if (await file.exists()) { // if (await file.exists()) {

16
lib/modules/pairing/controllers/pairing_controller.dart

@ -96,7 +96,7 @@ class PairingController extends GetxController {
// 检查蓝牙状态并刷新设备 // 检查蓝牙状态并刷新设备
await _checkBluetoothAndRefresh(); await _checkBluetoothAndRefresh();
} catch (e) { } catch (e) {
Logger.error('初始化配对控制器失败: $e'); Logger.error('初始化配对控制器失败: ${e.toString()}');
statusDescription.value = 'initializationFailure'.tr; statusDescription.value = 'initializationFailure'.tr;
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -170,7 +170,7 @@ class PairingController extends GetxController {
await _bleService.registerBackgroundScan(); await _bleService.registerBackgroundScan();
_autoNavigateToHome(); _autoNavigateToHome();
} catch (e) { } catch (e) {
Logger.error('检查设备配对状态失败: $e'); Logger.error('检查设备配对状态失败: ${e.toString()}');
} }
} }
@ -186,7 +186,7 @@ class PairingController extends GetxController {
if (Platform.isAndroid) { if (Platform.isAndroid) {
permissionsToRequest.add(Permission.locationWhenInUse); permissionsToRequest.add(Permission.locationWhenInUse);
permissionsToRequest.add(Permission.microphone); // permissionsToRequest.add(Permission.microphone);
if (await _getAndroidSdkVersion() >= 31) { if (await _getAndroidSdkVersion() >= 31) {
permissionsToRequest.add(Permission.bluetoothScan); permissionsToRequest.add(Permission.bluetoothScan);
permissionsToRequest.add(Permission.bluetoothConnect); permissionsToRequest.add(Permission.bluetoothConnect);
@ -356,7 +356,7 @@ class PairingController extends GetxController {
_updateStatusDescription(); _updateStatusDescription();
} }
} catch (e) { } catch (e) {
Logger.error('Companion设备关联过程发生错误: $e'); Logger.error('Companion设备关联过程发生错误: ${e.toString()}');
_isAssociating = false; _isAssociating = false;
_userRejectedAssociation.value = true; _userRejectedAssociation.value = true;
_updateStatusDescription(); _updateStatusDescription();
@ -386,7 +386,7 @@ class PairingController extends GetxController {
final result = await BleManager.to.startAgentService(); final result = await BleManager.to.startAgentService();
Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败'); Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败');
} catch (e) { } catch (e) {
Logger.error('启动BLE后台服务发生错误: $e'); Logger.error('启动BLE后台服务发生错误: ${e.toString()}');
} }
} }
@ -396,7 +396,7 @@ class PairingController extends GetxController {
final result = await BleManager.to.stopAgentService(); final result = await BleManager.to.stopAgentService();
Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败'); Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败');
} catch (e) { } catch (e) {
Logger.error('停止BLE后台服务发生错误: $e'); Logger.error('停止BLE后台服务发生错误: ${e.toString()}');
} }
} }
@ -424,7 +424,7 @@ class PairingController extends GetxController {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth); AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
} }
} catch (e) { } catch (e) {
Logger.error('打开设置失败: $e'); Logger.error('打开设置失败: ${e.toString()}');
} }
} }
@ -447,4 +447,4 @@ class PairingController extends GetxController {
_isNavigatingToHome = true; _isNavigatingToHome = true;
Get.offAllNamed(Routes.home); Get.offAllNamed(Routes.home);
} }
} }

18
lib/modules/speech_test/controllers/speech_test_controller.dart

@ -83,7 +83,7 @@ class SpeechTestController extends GetxController {
} catch (e) { } catch (e) {
ttsStatus.value = '初始化失败'; ttsStatus.value = '初始化失败';
recognitionStatus.value = '初始化失败'; recognitionStatus.value = '初始化失败';
Logger.error('语音测试初始化失败: $e'); Logger.error('语音测试初始化失败: ${e.toString()}');
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
} }
@ -198,7 +198,7 @@ class SpeechTestController extends GetxController {
// 重新初始化服务 // 重新初始化服务
await _initServices(); await _initServices();
} catch (e) { } catch (e) {
Logger.error('切换语音服务类型失败: $e'); Logger.error('切换语音服务类型失败: ${e.toString()}');
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
} }
@ -242,7 +242,7 @@ class SpeechTestController extends GetxController {
} }
} catch (e) { } catch (e) {
ttsStatus.value = '播放异常: $e'; ttsStatus.value = '播放异常: $e';
Logger.error('单次播放异常: $e'); Logger.error('单次播放异常: ${e.toString()}');
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
} }
@ -270,7 +270,7 @@ class SpeechTestController extends GetxController {
await _ttsService.flushStream(); await _ttsService.flushStream();
} catch (e) { } catch (e) {
ttsStatus.value = '连续播放异常: $e'; ttsStatus.value = '连续播放异常: $e';
Logger.error('连续播放异常: $e'); Logger.error('连续播放异常: ${e.toString()}');
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
} }
@ -284,7 +284,7 @@ class SpeechTestController extends GetxController {
await _ttsService.stop(); await _ttsService.stop();
} catch (e) { } catch (e) {
ttsStatus.value = '停止播放异常: $e'; ttsStatus.value = '停止播放异常: $e';
Logger.error('停止播放异常: $e'); Logger.error('停止播放异常: ${e.toString()}');
} }
} }
@ -324,7 +324,7 @@ class SpeechTestController extends GetxController {
} }
} catch (e) { } catch (e) {
recognitionStatus.value = '识别异常: $e'; recognitionStatus.value = '识别异常: $e';
Logger.error('一次性识别异常: $e'); Logger.error('一次性识别异常: ${e.toString()}');
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
} }
@ -351,7 +351,7 @@ class SpeechTestController extends GetxController {
_setupAsrEventListener(stream); _setupAsrEventListener(stream);
} catch (e) { } catch (e) {
recognitionStatus.value = '启动连续识别异常: $e'; recognitionStatus.value = '启动连续识别异常: $e';
Logger.error('启动连续识别异常: $e'); Logger.error('启动连续识别异常: ${e.toString()}');
isListening.value = false; isListening.value = false;
intermediateResult.value = ''; intermediateResult.value = '';
finalResult.value = ''; finalResult.value = '';
@ -377,7 +377,7 @@ class SpeechTestController extends GetxController {
intermediateResult.value = ''; intermediateResult.value = '';
} catch (e) { } catch (e) {
recognitionStatus.value = '停止连续识别异常: $e'; recognitionStatus.value = '停止连续识别异常: $e';
Logger.error('停止连续识别异常: $e'); Logger.error('停止连续识别异常: ${e.toString()}');
} }
} }
@ -413,4 +413,4 @@ class SpeechTestController extends GetxController {
super.onClose(); super.onClose();
} }
} }

2
lib/modules/splash/controllers/splash_controller.dart

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
// import 'package:amap_flutter/amap_flutter.dart';
import 'package:deep_voice/data/models/appconfig.dart'; import 'package:deep_voice/data/models/appconfig.dart';
import 'package:deep_voice/data/models/appconfig_model.dart'; import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -9,6 +8,7 @@ import '../../../data/models/user_Info.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../core/themes/theme_service.dart'; import '../../../core/themes/theme_service.dart';
import '../../../data/services/network/api.dart'; import '../../../data/services/network/api.dart';
import 'package:amap_flutter/amap_flutter.dart';
class SplashController extends GetxController { class SplashController extends GetxController {
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();

26
lib/modules/translation/controllers/translation_controller.dart

@ -175,7 +175,7 @@ class TranslationController extends GetxController {
Logger.info('所有服务初始化完成'); Logger.info('所有服务初始化完成');
} catch (e) { } catch (e) {
Logger.error('初始化服务失败: $e'); Logger.error('初始化服务失败: ${e.toString()}');
} }
} }
@ -252,7 +252,7 @@ class TranslationController extends GetxController {
recognitionStream.listen(_handleRecognitionEvent); recognitionStream.listen(_handleRecognitionEvent);
} catch (e) { } catch (e) {
isRecognizing.value = false; isRecognizing.value = false;
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: ${e.toString()}');
} }
} }
@ -307,7 +307,7 @@ class TranslationController extends GetxController {
currentSessionId = null; currentSessionId = null;
} catch (e) { } catch (e) {
Logger.error('停止语音识别失败: $e'); Logger.error('停止语音识别失败: ${e.toString()}');
} }
} }
@ -445,7 +445,7 @@ class TranslationController extends GetxController {
} }
} }
} catch (e) { } catch (e) {
Logger.error('翻译失败: $e'); Logger.error('翻译失败: ${e.toString()}');
} finally { } finally {
isTranslating.value = false; isTranslating.value = false;
} }
@ -468,7 +468,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text); await _ttsService.speakOnce(text);
} catch (e) { } catch (e) {
Logger.error('播放翻译失败: $e'); Logger.error('播放翻译失败: ${e.toString()}');
} }
} }
@ -482,7 +482,7 @@ class TranslationController extends GetxController {
_translationDebounceTimer?.cancel(); _translationDebounceTimer?.cancel();
await _ttsService.stop(); await _ttsService.stop();
} catch (e) { } catch (e) {
Logger.error('停止操作失败: $e'); Logger.error('停止操作失败: ${e.toString()}');
} }
} }
@ -497,7 +497,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(item.translatedText); await _ttsService.speakOnce(item.translatedText);
} catch (e) { } catch (e) {
Logger.error('播放翻译项失败: $e'); Logger.error('播放翻译项失败: ${e.toString()}');
} }
} }
@ -516,7 +516,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text); await _ttsService.speakOnce(text);
} catch (e) { } catch (e) {
Logger.error('播放文本失败: $e'); Logger.error('播放文本失败: ${e.toString()}');
} }
} }
@ -531,7 +531,7 @@ class TranslationController extends GetxController {
curve: Curves.easeOut, curve: Curves.easeOut,
); );
} catch (e) { } catch (e) {
Logger.error('滚动到底部失败: $e'); Logger.error('滚动到底部失败: ${e.toString()}');
} }
} }
} }
@ -629,7 +629,7 @@ class TranslationController extends GetxController {
Logger.info('ASR服务重新初始化成功'); Logger.info('ASR服务重新初始化成功');
} catch (e) { } catch (e) {
Logger.error('重新初始化ASR服务失败: $e'); Logger.error('重新初始化ASR服务失败: ${e.toString()}');
} }
} }
@ -656,7 +656,7 @@ class TranslationController extends GetxController {
}); });
} catch (e) { } catch (e) {
_storage.remove(_historyKey); _storage.remove(_historyKey);
Logger.error('加载翻译历史失败: $e'); Logger.error('加载翻译历史失败: ${e.toString()}');
} }
} }
} }
@ -673,7 +673,7 @@ class TranslationController extends GetxController {
translationHistory.map((item) => item.toJson()).toList(); translationHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson)); _storage.write(_historyKey, jsonEncode(historyJson));
} catch (e) { } catch (e) {
Logger.error('保存翻译历史失败: $e'); Logger.error('保存翻译历史失败: ${e.toString()}');
} }
} }
@ -764,4 +764,4 @@ class TranslationItem {
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? return languageManager.getChineseNameByAsrCode(targetLanguageCode) ??
'未知语言'; '未知语言';
} }
} }

8
lib/modules/translation/controllers/translation_detail_controller.dart

@ -58,7 +58,7 @@ class TranslationDetailController extends GetxController {
loadSavedSummary(); loadSavedSummary();
}); });
} catch (e) { } catch (e) {
Logger.error('初始化详情控制器失败: $e'); Logger.error('初始化详情控制器失败: ${e.toString()}');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@ -72,7 +72,7 @@ class TranslationDetailController extends GetxController {
summaryResult.value = savedSummary; summaryResult.value = savedSummary;
} }
} catch (e) { } catch (e) {
Logger.error('加载摘要失败: $e'); Logger.error('加载摘要失败: ${e.toString()}');
} }
} }
@ -83,7 +83,7 @@ class TranslationDetailController extends GetxController {
try { try {
_storage.write(_getSummaryKey(), summary); _storage.write(_getSummaryKey(), summary);
} catch (e) { } catch (e) {
Logger.error('保存摘要失败: $e'); Logger.error('保存摘要失败: ${e.toString()}');
} }
} }
@ -174,7 +174,7 @@ class TranslationDetailController extends GetxController {
return result; return result;
} catch (e) { } catch (e) {
final errorMessage = '生成摘要失败: ${e.toString()}'; final errorMessage = '生成摘要失败: ${e.toString()}';
Logger.error(errorMessage); Logger.error('TranslationDetailController', errorMessage);
summaryResult.value = errorMessage; summaryResult.value = errorMessage;
return errorMessage; return errorMessage;
} finally { } finally {

9
lib/routes/app_pages.dart

@ -29,8 +29,6 @@ import '../modules/pairing/bindings/pairing_binding.dart';
import './app_routes.dart'; import './app_routes.dart';
import '../modules/settings/views/settings_view.dart'; import '../modules/settings/views/settings_view.dart';
import '../modules/settings/bindings/settings_binding.dart'; import '../modules/settings/bindings/settings_binding.dart';
import '../modules/jieli_test/views/jieli_test_view.dart';
import '../modules/jieli_test/bindings/jieli_test_binding.dart';
import '../modules/opus_test/views/opus_test_view.dart'; import '../modules/opus_test/views/opus_test_view.dart';
import '../modules/opus_test/bindings/opus_test_binding.dart'; import '../modules/opus_test/bindings/opus_test_binding.dart';
import '../modules/speech_test/views/speech_test_view.dart'; import '../modules/speech_test/views/speech_test_view.dart';
@ -162,13 +160,6 @@ abstract class AppPages {
transition: Transition.rightToLeft, transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250), transitionDuration: Duration(milliseconds: 250),
), ),
GetPage(
name: Routes.jieliTest,
page: () => const JieliTestView(),
binding: JieliTestBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage( GetPage(
name: Routes.opusTest, name: Routes.opusTest,
page: () => const OpusTestView(), page: () => const OpusTestView(),

6
local_plugins/agent_service/android/build.gradle.kts

@ -36,7 +36,6 @@ android {
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10")
implementation("androidx.core:core-ktx:1.8.0") implementation("androidx.core:core-ktx:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
implementation("androidx.work:work-runtime-ktx:2.8.1") implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.media:media:1.6.0") implementation("androidx.media:media:1.6.0")
@ -45,9 +44,10 @@ dependencies {
// 依赖本地其他插件 // 依赖本地其他插件
add("compileOnly", project(":azure_speech")) add("compileOnly", project(":azure_speech"))
add("compileOnly", project(":open_ai_service")) add("compileOnly", project(":open_ai_service"))
add("compileOnly", project(":deepvoice_utils"))
add("compileOnly", project(":chat_storage")) add("compileOnly", project(":chat_storage"))
add("compileOnly", project(":ble_service")) add("compileOnly", project(":ble_service"))
add("compileOnly", project(":music_service")) add("compileOnly", project(":music_service"))
add("compileOnly", project(":amap_service")) add("compileOnly", project(":volcano_speech"))
add("compileOnly", project(":bytedance_speech"))
add("compileOnly", project(":speech"))
} }

14
local_plugins/agent_service/android/settings.gradle.kts

@ -1,16 +1,2 @@
rootProject.name = "agent_service" rootProject.name = "agent_service"
// 包含项目依赖
include(":azure_speech")
include(":open_ai_service")
include(":deepvoice_utils")
include(":chat_storage")
include(":music_service")
include(":amap_service")
// 设置项目路径
project(":azure_speech").projectDir = file("../../azure_speech/android")
project(":open_ai_service").projectDir = file("../../open_ai_service/android")
project(":deepvoice_utils").projectDir = file("../../deepvoice_utils/android")
project(":chat_storage").projectDir = file("../../chat_storage/android")
project(":music_service").projectDir = file("../../music_service/android")
project(":amap_service").projectDir = file("../../amap_service/android")

469
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -4,12 +4,10 @@ import android.content.Context
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.music_service.MusicServiceStarter
import com.yunqiinnovation.amap_service.NavigationServiceStarter import android.util.Log
import com.yunqiinnovation.deepvoice_utils.FileLogger
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.Collections import java.util.Collections
@ -19,6 +17,11 @@ import android.media.MediaPlayer
import com.yunqiinnovation.agent_service.R import com.yunqiinnovation.agent_service.R
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.deep_voice.speech.tts.ITtsService
import com.deep_voice.bytedance_speech.BytedanceTTS
/** /**
* 代理服务事件监听接口 * 代理服务事件监听接口
*/ */
@ -42,22 +45,17 @@ object AgentService : CoroutineScope {
// 协程相关 // 协程相关
private val job = SupervisorJob() private val job = SupervisorJob()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job get() = Dispatchers.Main + job
// 上下文和监听器 // 上下文和监听器
private lateinit var context: Context private lateinit var context: Context
private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>()) private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>())
// 配置参数
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
private var openaiApiKey: String = ""
private var openaiBaseUrl: String = ""
private var openaiModel: String = "gpt-3.5-turbo"
// Azure服务 // Azure服务
private var azureAsrHelper: AzureAsrHelper? = null private var azureAsrHelper: AzureAsrHelper? = null
private var azureTtsHelper: AzureTtsHelper? = null
// 使用接口类型,支持不同实现
private var ttsService: ITtsService? = null
// OpenAI服务 // OpenAI服务
private lateinit var openAIService: OpenAIService private lateinit var openAIService: OpenAIService
@ -100,17 +98,63 @@ object AgentService : CoroutineScope {
*/ */
private fun initSystemPrompt() { private fun initSystemPrompt() {
systemPrompt = """ systemPrompt = """
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。以下是不同场景的具体要求:
遵循以下指导原则:
1. 保持简短精炼的回答,因为用户是通过语音与你交流 一、知识类问题回答规范
2. 优先使用中文回复,除非用户明确要求使用其他语言 核心目标:用简洁语言提供准确信息,兼顾易读性与专业性。
3. 当用户问题不明确时,礼貌地请求更多信息
4. 避免过长的列表,尽量将信息分成小段 1. 内容细节处理
5. 不要使用需要视觉展示的元素(如表格、图表或代码块) • 精准性优先:围绕问题主体,核心信息需全面、准确,避免模糊表述或错误。
6. 记住用户之前的对话内容,保持对话连贯 ◦ 例:回答"地球公转周期"时,需明确"约365.25天"而非"一年左右"。
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 • 背景与类比:对复杂概念(如"量子纠缠"),可补充关键背景("量子力学中的现象")并类比("类似远距离同步的状态"),帮助用户快速理解。
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 • 需求宽泛时:若问题涉及范围广(如"介绍人工智能"),先概述主要领域(技术、应用、影响),再分点简要说重点,避免信息过载。
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 • 禁止过度延伸:非用户主动询问,不添加无关内容(如问"光速"时,不展开讨论"相对论")。
2. 格式排版规则
• 标题与重点:章节标题、关键结论需加粗(如一、核心原理),突出层级。
• 列表使用:
◦ 有序列表(1. 2. 3.):用于步骤、顺序类内容(如"实验流程")。
◦ 无序列表(- ):用于并列项(如"优点:- 高效 - 精准")。
◦ 禁止嵌套列表:如需多层级,用**三级标题(###)**分隔,下接一级列表。
• 段落处理:辅助解释用自然段,每段不超过3行,避免大段文字堆砌。
二、文案创作与内容生成规范
核心目标:根据体裁需求,平衡丰富度与阅读体验,确保逻辑流畅、重点突出。
1. 篇幅与内容要求
• 场景适配:
◦ 短文案(如标语、摘要):控制在50字内,用词简练有力。
◦ 长内容(如报告、故事):需结构清晰,每部分围绕核心展开,适当添加案例/数据支撑。
• 避免极端化:既不堆砌空泛词汇,也不遗漏关键信息。
2. 格式与排版
• 默认体裁:无特殊要求时用自然段,适当换行分隔话题。
• 结构化体裁(如策划案、简历):
◦ 用**分级标题**划分板块。
◦ 关键数据/结论加粗。
◦ 并列信息用列表呈现。
三、聊天陪伴与情感互动规范
核心目标:营造轻松友好的对话氛围,成为用户可信赖的交流伙伴。
1. 沟通风格设定
• 人格化表达:避免机械回答,用口语化词汇,适当加入表情符号增强亲切感。
• 主动引导话题:根据用户输入延伸讨论。
• 适度分享:自然提及日常,拉近距离。
2. 情绪识别与回应
• 捕捉潜在需求:识别情绪,先共情再询问细节。
• 针对性建议:结合用户处境提供具体方案。
• 积极价值观:传递乐观态度,避免消极表述。
四、能力与工具使用说明
1. 内容处理能力:图片识别、可执行总结、翻译、分析等任务。
2. 信息搜索能力:实时数据、新闻、专业知识等需搜索时,会明确告知用户。
五、特殊场景处理规则
1. 安全与合规:禁止回答敏感问题,遇此类问题需礼貌拒绝。
2. 用户指令优先:若用户明确要求特定风格,优先遵循其需求。
3. 多轮澄清机制:若问题模糊,需主动询问细节,避免假设性回答。
""".trimIndent() """.trimIndent()
} }
/** /**
@ -129,85 +173,184 @@ object AgentService : CoroutineScope {
this.openAIService = OpenAIService(context) this.openAIService = OpenAIService(context)
this.chatStorageHelper = ChatStorageHelper(context) this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context) this.audioPlayer = AudioPlayer(context)
// 从配置中获取参数
azureSpeechKey = config["azureSpeechKey"] as? String ?: ""
azureSpeechRegion = config["azureSpeechRegion"] as? String ?: ""
openaiApiKey = config["openaiApiKey"] as? String ?: ""
openaiBaseUrl = config["openaiBaseUrl"] as? String ?: ""
openaiModel = config["openaiModel"] as? String ?: "gpt-3.5-turbo"
val mcpServer = config["mcpServer"] as? String ?: ""
// 设置系统提示词 // 设置系统提示词
val customSystemPrompt = config["systemPrompt"] as? String // val customSystemPrompt = config["systemPrompt"] as? String
if (!customSystemPrompt.isNullOrBlank()) { // if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt // systemPrompt = customSystemPrompt
} else { // } else {
initSystemPrompt() // initSystemPrompt()
} // }
initSystemPrompt()
// 检查必要参数
if (azureSpeechKey.isEmpty() || azureSpeechRegion.isEmpty() || openaiApiKey.isEmpty()) {
FileLogger.e(TAG, "初始化失败:关键配置参数缺失")
return false
}
// 初始化Azure ASR // 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(context).apply { azureAsrHelper = AzureAsrHelper(context).apply {
initialize( initialize(
subscriptionKey = azureSpeechKey, subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = azureSpeechRegion, region = config["azureSpeechRegion"]?.toString() ?: "",
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE else AzureAsrHelper.AudioSourceType.MICROPHONE
) )
} }
// 初始化Azure TTS // 初始化TTS服务
azureTtsHelper = AzureTtsHelper(context).apply { initializeTtsEngine(config)
initialize(
subscriptionKey = azureSpeechKey,
region = azureSpeechRegion
)
// 设置TTS回调
setTtsCallback(object : AzureTtsHelper.TtsCallback {
override fun onSynthesisStarted() {
isTtsSpeaking = true
restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started"))
}
override fun onSynthesizing() {}
override fun onSynthesisCompleted() {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed"))
}
override fun onSynthesisCanceled() {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
}
})
}
// 初始化OpenAI服务 // 初始化OpenAI服务
openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel, mcpServer) openAIService.initialize(
config["openaiApiKey"]?.toString() ?: "",
config["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "",
config["mcpServer"]?.toString() ?: ""
)
// 加载最近的聊天记录 // 加载最近的聊天记录
loadChatHistory() loadChatHistory()
isInitialized = true isInitialized = true
FileLogger.d(TAG, "代理服务初始化成功") Log.d(TAG, "代理服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "初始化失败: ${e.message}") Log.e(TAG, "初始化失败: ${e.message}")
return false return false
} }
} }
/**
* 释放所有资源
*/
fun dispose() {
try {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
audioPlayer?.release()
audioPlayer = null
azureAsrHelper?.dispose()
// 释放TTS服务
ttsService?.release()
ttsService = null
job.cancel()
clearListeners()
isInitialized = false
Log.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
Log.e(TAG, "释放资源异常: ${e.message}")
}
}
/**
* 初始化TTS引擎
*/
private fun initializeTtsEngine(config: Map<String, Any>) {
// 停止当前TTS服务(如果有)
if (ttsService != null) {
ttsService?.stop()
}
try {
val ttsEngine = config["ttsEngine"]?.toString() ?: "volcano" // 默认使用火山引擎
val ttsLanguage = config["ttsLanguage"]?.toString() ?: "zh-CN"
// 根据配置选择合适的TTS引擎
if (ttsEngine == "azure") {
// 使用Azure TTS
val azureSpeechKey = config["azureSpeechKey"]?.toString() ?: ""
val azureSpeechRegion = config["azureSpeechRegion"]?.toString() ?: ""
// 创建并初始化Azure TTS Helper
val azureTtsHelper = com.yunqiinnovation.azure_speech.AzureTtsHelper(context)
ttsService = azureTtsHelper
// 直接初始化,无需协程包装
val success = azureTtsHelper.initialize(
ttsAppId = "", // Azure TTS不需要appId
ttsAppToken = azureSpeechKey,
ttsResource = azureSpeechRegion,
language = ttsLanguage
)
if (success) {
Log.d(TAG, "Azure TTS服务初始化成功")
} else {
Log.e(TAG, "Azure TTS服务初始化失败")
}
} else {
// 默认使用火山引擎TTS
// 创建并初始化BytedanceTTS
// 从配置中获取TTS参数
val ttsAppId = config["volcanoAppId"]?.toString() ?: ""
val ttsAppToken = config["volcanoToken"]?.toString() ?: ""
val bytedanceTts = BytedanceTTS(context)
ttsService = bytedanceTts
// 初始化火山TTS
val success = bytedanceTts.initialize(
ttsAppId = ttsAppId,
ttsAppToken = ttsAppToken,
ttsResource = "", // 火山TTS不需要resource
language = ttsLanguage
)
if (success) {
Log.d(TAG, "火山TTS服务初始化请求已发送")
} else {
Log.e(TAG, "火山TTS服务初始化失败")
}
}
// 添加TTS事件监听
ttsService?.addListener(object : TtsEventListener {
override fun onEvent(event: TtsEvent) {
when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> {
isTtsSpeaking = true
restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started"))
}
TtsEventType.SYNTHESIS_COMPLETED -> {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed"))
}
TtsEventType.SYNTHESIS_CANCELED -> {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
}
TtsEventType.ERROR -> {
isTtsSpeaking = false
restartIdleCheck()
val params = event.params
val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR"
val message = params["errorMessage"] as? String ?: "未知错误"
sendEvent("error", mapOf(
"code" to "TTS_ERROR",
"message" to "语音合成错误: $code, $message"
))
}
else -> {
// 处理其他类型的事件
Log.d(TAG, "TTS事件: ${event.type}")
}
}
}
})
Log.d(TAG, "TTS服务配置完成")
} catch (e: Exception) {
Log.e(TAG, "初始化TTS引擎失败: ${e.message}")
}
}
/** /**
* 添加事件监听器 * 添加事件监听器
* @param listener 要添加的监听器 * @param listener 要添加的监听器
@ -250,7 +393,7 @@ object AgentService : CoroutineScope {
} catch (e: CancellationException) { } catch (e: CancellationException) {
// 协程被取消,正常行为 // 协程被取消,正常行为
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "空闲检测异常: ${e.message}") Log.e(TAG, "空闲检测异常: ${e.message}")
} }
} }
} }
@ -278,7 +421,7 @@ object AgentService : CoroutineScope {
*/ */
fun startRecognition(isExternalActive: Boolean): Boolean { fun startRecognition(isExternalActive: Boolean): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化") Log.e(TAG, "服务未初始化")
return false return false
} }
@ -286,7 +429,7 @@ object AgentService : CoroutineScope {
// 检查录音权限 // 检查录音权限
if (!isExternalActive && !checkRecordAudioPermission()) { if (!isExternalActive && !checkRecordAudioPermission()) {
FileLogger.e(TAG, "需要录音权限") Log.e(TAG, "需要录音权限")
sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
return false return false
} }
@ -300,7 +443,7 @@ object AgentService : CoroutineScope {
} else { } else {
AzureAsrHelper.AudioSourceType.MICROPHONE AzureAsrHelper.AudioSourceType.MICROPHONE
} }
FileLogger.d(TAG, "开始语音识别, 音频源类型: $audioSourceType") Log.d(TAG, "开始语音识别, 音频源类型: $audioSourceType")
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) { if (recognizing.isNotEmpty()) {
@ -372,7 +515,7 @@ object AgentService : CoroutineScope {
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
BleService.closeCodec() BleService.closeCodec()
FileLogger.e(TAG, "语音识别错误: $error") Log.e(TAG, "语音识别错误: $error")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "RECOGNITION_ERROR", "code" to "RECOGNITION_ERROR",
"message" to error "message" to error
@ -382,7 +525,7 @@ object AgentService : CoroutineScope {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
isRecognitionActive = false isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败: ${e.message}") Log.e(TAG, "启动语音识别失败: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "RECOGNITION_START_ERROR", "code" to "RECOGNITION_START_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -405,9 +548,9 @@ object AgentService : CoroutineScope {
BleService.closeCodec() BleService.closeCodec()
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
FileLogger.d(TAG, "语音识别已停止") Log.d(TAG, "语音识别已停止")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常: ${e.message}") Log.e(TAG, "停止语音识别异常: ${e.message}")
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
} }
@ -447,9 +590,9 @@ object AgentService : CoroutineScope {
openAIService.cancelCurrentStream() openAIService.cancelCurrentStream()
// 记录日志 // 记录日志
FileLogger.d(TAG, "AI流输出已停止") Log.d(TAG, "AI流输出已停止")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止AI流输出异常", e) Log.e(TAG, "停止AI流输出异常", e)
// 确保状态被重置,即使发生异常 // 确保状态被重置,即使发生异常
isAiStreaming = false isAiStreaming = false
currentAiJob = null currentAiJob = null
@ -462,13 +605,13 @@ object AgentService : CoroutineScope {
*/ */
fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { fun processTextInput(text: String, speakResponse: Boolean = false): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化") Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false return false
} }
if (text.isEmpty()) { if (text.isEmpty()) {
FileLogger.e(TAG, "文本输入不能为空") Log.e(TAG, "文本输入不能为空")
sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空")) sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空"))
return false return false
} }
@ -495,7 +638,7 @@ object AgentService : CoroutineScope {
text: String, text: String,
speakResponse: Boolean = true speakResponse: Boolean = true
) { ) {
FileLogger.d(TAG, "用户问题: $text") Log.d(TAG, "用户问题: $text")
// 创建用户文本消息并处理 // 创建用户文本消息并处理
val userMessage = openAIService.createUserMessage(text) val userMessage = openAIService.createUserMessage(text)
@ -514,7 +657,7 @@ object AgentService : CoroutineScope {
text: String = "", text: String = "",
speakResponse: Boolean = false speakResponse: Boolean = false
) { ) {
FileLogger.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}") Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}")
// 创建带图片的用户消息并处理 // 创建带图片的用户消息并处理
val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) val userMessage = openAIService.createUserMessageWithImage(text, imageBase64)
@ -546,7 +689,6 @@ object AgentService : CoroutineScope {
// 设置状态为正在流式输出 // 设置状态为正在流式输出
isAiStreaming = true isAiStreaming = true
// 使用历史记录作为上下文发送到OpenAI // 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder() val responseBuilder = StringBuilder()
var metadata:String = "" var metadata:String = ""
@ -593,24 +735,25 @@ object AgentService : CoroutineScope {
addToHistoryMessages(userMessage) addToHistoryMessages(userMessage)
} }
openAIService.sendMessageStream( openAIService.sendMessageStream(
messages = messagesWithSystemPrompt, messages = messagesWithSystemPrompt,
callback = object : OpenAIService.StreamCallback { callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) { override fun onToken(token: String) {
responseBuilder.append(token) responseBuilder.append(token)
if (speakResponse) { if (speakResponse) {
azureTtsHelper?.speakStream(token) // 直接调用TTS,无需协程包装
ttsService?.speakStream(token)
} }
// 发送流式回复token // 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token)) sendEvent("assistant_token", mapOf("token" to token))
} }
override fun onComplete() { override fun onComplete() {
FileLogger.d(TAG, "AI完整回复: $responseBuilder") Log.d(TAG, "AI完整回复: $responseBuilder")
// 视情况决定是否朗读回复 // 视情况决定是否朗读回复
if (speakResponse) { if (speakResponse) {
azureTtsHelper?.flushStream() // 直接调用TTS,无需协程包装
ttsService?.flushStream()
} }
val response = responseBuilder.toString() val response = responseBuilder.toString()
@ -638,7 +781,7 @@ object AgentService : CoroutineScope {
} }
override fun onError(e: Exception) { override fun onError(e: Exception) {
FileLogger.e(TAG, "AI处理出错", e) Log.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "AI_ERROR", "code" to "AI_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -661,7 +804,7 @@ object AgentService : CoroutineScope {
stopRecognition() stopRecognition()
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理函数调用事件失败", e) Log.e(TAG, "处理函数调用事件失败", e)
} }
} }
@ -671,15 +814,15 @@ object AgentService : CoroutineScope {
"function_call" to functionCall.toString(), "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
)) ))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult") Log.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoHandleFcunCallResult(functionCallResult); metadata = autoPlayMusic(functionCallResult);
} }
} }
) )
} catch (e: Exception) { } catch (e: Exception) {
val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR" val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR"
FileLogger.e(TAG, "AI处理出错", e) Log.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to errorType, "code" to errorType,
"message" to e.message.toString() "message" to e.message.toString()
@ -722,9 +865,9 @@ object AgentService : CoroutineScope {
} }
} }
FileLogger.d(TAG, "已加载${recentMessages.size}条历史记录") Log.d(TAG, "已加载${recentMessages.size}条历史记录")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "加载聊天历史失败: ${e.message}") Log.e(TAG, "加载聊天历史失败: ${e.message}")
} }
} }
@ -755,8 +898,8 @@ object AgentService : CoroutineScope {
isTtsSpeaking = true isTtsSpeaking = true
restartIdleCheck() // 状态变化,重启检测 restartIdleCheck() // 状态变化,重启检测
// 播放文本 // 直接调用TTS,无需协程包装
azureTtsHelper?.speakText(text) ttsService?.speakOnce(text)
} }
/** /**
@ -764,7 +907,8 @@ object AgentService : CoroutineScope {
*/ */
fun stopTts() { fun stopTts() {
if (isTtsSpeaking) { if (isTtsSpeaking) {
azureTtsHelper?.stopSpeaking() ttsService?.stop()
isTtsSpeaking = false isTtsSpeaking = false
restartIdleCheck() // 状态变化,重启检测 restartIdleCheck() // 状态变化,重启检测
sendEvent("tts_stopped", mapOf("status" to "stopped")) sendEvent("tts_stopped", mapOf("status" to "stopped"))
@ -775,6 +919,7 @@ object AgentService : CoroutineScope {
* 保存聊天消息 * 保存聊天消息
*/ */
private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) { private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) {
// 显式指定IO调度器,数据库操作应在后台线程执行,即使父协程在Main上下文中
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
try { try {
// 保存用户消息 // 保存用户消息
@ -794,15 +939,15 @@ object AgentService : CoroutineScope {
) )
// if (assistantMessageId != -1L) { // if (assistantMessageId != -1L) {
// FileLogger.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") // Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId")
// } else { // } else {
// FileLogger.e(TAG, "保存助手消息失败") // Log.e(TAG, "保存助手消息失败")
// } // }
} else { } else {
FileLogger.e(TAG, "保存用户消息失败") Log.e(TAG, "保存用户消息失败")
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "保存聊天记录异常", e) Log.e(TAG, "保存聊天记录异常", e)
} }
} }
} }
@ -821,19 +966,17 @@ object AgentService : CoroutineScope {
historyMessages.remove(0) historyMessages.remove(0)
} }
} }
FileLogger.d(TAG, "聊天历史已清除") Log.d(TAG, "聊天历史已清除")
} else { } else {
FileLogger.e(TAG, "清除聊天历史失败") Log.e(TAG, "清除聊天历史失败")
} }
withContext(Dispatchers.Main) { // 由于launch已在Main上下文中,可以直接调用回调
callback(success) callback(success)
}
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "清除聊天历史异常: ${e.message}") Log.e(TAG, "清除聊天历史异常: ${e.message}")
withContext(Dispatchers.Main) { // 由于launch已在Main上下文中,可以直接调用回调
callback(false) callback(false)
}
} }
} }
} }
@ -842,57 +985,19 @@ object AgentService : CoroutineScope {
* 发送事件 * 发送事件
*/ */
private fun sendEvent(eventName: String, data: Map<String, Any>) { private fun sendEvent(eventName: String, data: Map<String, Any>) {
// 检查当前是否在主线程 // 使用协程确保在主线程上执行
if (Looper.myLooper() == Looper.getMainLooper()) { launch {
// 已在主线程,直接发送 // 我们已在主线程上下文中启动协程,无需再切换线程
listeners.forEach { listener -> listeners.forEach { listener ->
try { try {
listener.onEvent(eventName, data) listener.onEvent(eventName, data)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e) Log.e(TAG, "向监听器发送事件失败", e)
}
} }
} else {
// 不在主线程,切换到主线程
Handler(Looper.getMainLooper()).post {
listeners.forEach { listener ->
try {
listener.onEvent(eventName, data)
} catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e)
}
}
} }
} }
} }
/**
* 释放所有资源
*/
fun dispose() {
try {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
audioPlayer?.release()
audioPlayer = null
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
job.cancel()
clearListeners()
isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源异常: ${e.message}")
}
}
/** /**
* 检查录音权限 * 检查录音权限
*/ */
@ -907,13 +1012,13 @@ object AgentService : CoroutineScope {
*/ */
fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean { fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化") Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false return false
} }
if (imagePath.isEmpty()) { if (imagePath.isEmpty()) {
FileLogger.e(TAG, "图片路径不能为空") Log.e(TAG, "图片路径不能为空")
sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空")) sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空"))
return false return false
} }
@ -945,7 +1050,7 @@ object AgentService : CoroutineScope {
// 处理包含图片的消息 // 处理包含图片的消息
processImageWithOpenAI(imageBase64, text, speakResponse) processImageWithOpenAI(imageBase64, text, speakResponse)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理图片失败: ${e.message}") Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "IMAGE_PROCESSING_ERROR", "code" to "IMAGE_PROCESSING_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -993,9 +1098,9 @@ object AgentService : CoroutineScope {
} }
} }
} }
FileLogger.d(TAG, "开始播放音频资源") Log.d(TAG, "开始播放音频资源")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e) Log.e(TAG, "播放音频资源异常: ${e.message}", e)
release() release()
} }
} }
@ -1017,17 +1122,17 @@ object AgentService : CoroutineScope {
} }
} }
} }
/* /*
* 自动播放音乐 * 自动播放音乐
* */ * */
fun autoHandleFcunCallResult(functionCallResult: JSONObject): String{ fun autoHandleFcunCallResult(functionCallResult: JSONObject): String{
val metaStr = functionCallResult.optString("meta") val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) { if (metaStr.isNotEmpty()) {
val meta = JSONObject(metaStr) val meta = JSONObject(metaStr)
FileLogger.d(TAG, "mcp调用结果: $metaStr") Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片 if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music") val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic") Log.d(TAG, "检查到音乐卡片: $cardMusic")
val id = cardMusic.optString("id", "") val id = cardMusic.optString("id", "")
val url = cardMusic.optString("url", "") val url = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "") val name = cardMusic.optString("name", "")
@ -1049,13 +1154,13 @@ object AgentService : CoroutineScope {
playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image)) playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
} }
if (playlist.isNotEmpty()) { if (playlist.isNotEmpty()) {
FileLogger.w(TAG, "自动播放音乐列表 ${playlist}") Log.w(TAG, "自动播放音乐列表 ${playlist}")
processMusicPlayList(playlist) processMusicPlayList(playlist)
} else { } else {
FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目") Log.w(TAG, "card_musiclist 中没有有效的音乐条目")
} }
} else { } else {
FileLogger.w(TAG, "card_musiclist.musics 不是一个数组") Log.w(TAG, "card_musiclist.musics 不是一个数组")
} }
}else if (meta.has("card_navigation")) { //导航服务 }else if (meta.has("card_navigation")) { //导航服务
val cardNavigation = meta.getJSONObject("card_navigation") val cardNavigation = meta.getJSONObject("card_navigation")
@ -1080,7 +1185,7 @@ object AgentService : CoroutineScope {
*/ */
fun processMusicPlay(song: Map<String,String>){ fun processMusicPlay(song: Map<String,String>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用 // 在其他 Service、BroadcastReceiver 或 Application 中调用
FileLogger.i(TAG, "启动音乐服务 播放音乐 $song") Log.i(TAG, "启动音乐服务 播放音乐 $song")
MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song) MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song)
} }
@ -1096,10 +1201,10 @@ object AgentService : CoroutineScope {
fun processMusicPlayList(songs:List<Map<String,String>>){ fun processMusicPlayList(songs:List<Map<String,String>>){
// 如果 playlist 中有有效的歌曲,开始播放 // 如果 playlist 中有有效的歌曲,开始播放
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
FileLogger.i(TAG, "启动音乐服务 播放音乐列表 ${songs}") Log.i(TAG, "启动音乐服务 播放音乐列表 ${songs}")
MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs) MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs)
} else { } else {
FileLogger.i(TAG, "音乐列表为空,未启动播放服务") Log.i(TAG, "音乐列表为空,未启动播放服务")
} }
} }

30
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -8,7 +8,7 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import org.json.JSONObject import org.json.JSONObject
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -33,7 +33,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
// 添加监听器 // 添加监听器
AgentService.addListener(this) AgentService.addListener(this)
FileLogger.d(TAG, "插件已加载") Log.d(TAG, "插件已加载")
} }
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
@ -46,7 +46,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
return return
} }
FileLogger.d(TAG, "启动BleMediaService,配置参数: $config") Log.d(TAG, "启动BleMediaService,配置参数: $config")
// 创建Intent启动BleMediaService // 创建Intent启动BleMediaService
val intent = Intent(context, BleMediaService::class.java) val intent = Intent(context, BleMediaService::class.java)
@ -59,6 +59,8 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) } config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) }
config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) } config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) }
config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) } config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) }
config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) }
config["volcanoToken"]?.toString()?.let { intent.putExtra("volcanoToken", it) }
// 启动前台服务 // 启动前台服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -69,7 +71,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动AgentService失败", e) Log.e(TAG, "启动AgentService失败", e)
result.error("SERVICE_START_ERROR", "启动AgentService失败: ${e.message}", null) result.error("SERVICE_START_ERROR", "启动AgentService失败: ${e.message}", null)
} }
} }
@ -79,7 +81,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
context.stopService(intent) context.stopService(intent)
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止AgentService失败", e) Log.e(TAG, "停止AgentService失败", e)
result.error("SERVICE_STOP_ERROR", "停止AgentService失败: ${e.message}", null) result.error("SERVICE_STOP_ERROR", "停止AgentService失败: ${e.message}", null)
} }
} }
@ -88,7 +90,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
val isStarted = AgentService.startRecognition(false) val isStarted = AgentService.startRecognition(false)
result.success(isStarted) result.success(isStarted)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动对话失败", e) Log.e(TAG, "启动对话失败", e)
result.error("START_ERROR", "启动对话失败: ${e.message}", null) result.error("START_ERROR", "启动对话失败: ${e.message}", null)
} }
} }
@ -97,7 +99,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.stopRecognition() AgentService.stopRecognition()
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止对话失败", e) Log.e(TAG, "停止对话失败", e)
result.error("STOP_ERROR", "停止对话失败: ${e.message}", null) result.error("STOP_ERROR", "停止对话失败: ${e.message}", null)
} }
} }
@ -118,7 +120,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(isProcessing) result.success(isProcessing)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理文本输入失败", e) Log.e(TAG, "处理文本输入失败", e)
result.error("TEXT_PROCESS_ERROR", "处理文本输入失败: ${e.message}", null) result.error("TEXT_PROCESS_ERROR", "处理文本输入失败: ${e.message}", null)
} }
} }
@ -141,7 +143,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(isProcessing) result.success(isProcessing)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理图片输入失败", e) Log.e(TAG, "处理图片输入失败", e)
result.error("IMAGE_PROCESS_ERROR", "处理图片输入失败: ${e.message}", null) result.error("IMAGE_PROCESS_ERROR", "处理图片输入失败: ${e.message}", null)
} }
} }
@ -156,7 +158,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.speakText(text) AgentService.speakText(text)
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "语音合成失败", e) Log.e(TAG, "语音合成失败", e)
result.error("TTS_ERROR", "语音合成失败: ${e.message}", null) result.error("TTS_ERROR", "语音合成失败: ${e.message}", null)
} }
} }
@ -165,7 +167,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.stopTts() AgentService.stopTts()
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败", e) Log.e(TAG, "停止语音合成失败", e)
result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null) result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null)
} }
} }
@ -175,7 +177,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(success) result.success(success)
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "清除聊天历史失败", e) Log.e(TAG, "清除聊天历史失败", e)
result.error("CLEAR_HISTORY_ERROR", "清除聊天历史失败: ${e.message}", null) result.error("CLEAR_HISTORY_ERROR", "清除聊天历史失败: ${e.message}", null)
} }
} }
@ -184,7 +186,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.interruptCurrentResponse() AgentService.interruptCurrentResponse()
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "中断响应失败", e) Log.e(TAG, "中断响应失败", e)
result.error("INTERRUPT_ERROR", "中断响应失败: ${e.message}", null) result.error("INTERRUPT_ERROR", "中断响应失败: ${e.message}", null)
} }
} }
@ -193,7 +195,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.dispose() AgentService.dispose()
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "释放资源失败", e) Log.e(TAG, "释放资源失败", e)
result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null) result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null)
} }
} }

28
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -3,7 +3,7 @@ package com.yunqiinnovation.agent_service
import android.content.Context import android.content.Context
import com.yunqiinnovation.agent_service.AgentService import com.yunqiinnovation.agent_service.AgentService
import com.yunqiinnovation.agent_service.AgentServiceListener import com.yunqiinnovation.agent_service.AgentServiceListener
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst import com.yunqiinnovation.ble_service.BleConst
@ -29,11 +29,11 @@ object BleAgent : BleService.Callback, AgentServiceListener {
if (isInitialized) return true if (isInitialized) return true
try { try {
FileLogger.d(TAG, "初始化语音代理..., $config") Log.d(TAG, "初始化语音代理..., $config")
// 1. 初始化AgentService // 1. 初始化AgentService
if (!AgentService.initialize(context, config, true)) { if (!AgentService.initialize(context, config, true)) {
FileLogger.e(TAG, "语音服务初始化失败") Log.e(TAG, "语音服务初始化失败")
return false return false
} }
@ -41,20 +41,20 @@ object BleAgent : BleService.Callback, AgentServiceListener {
if (BleService.initialize(context)) { if (BleService.initialize(context)) {
if (BleService.addCallback(this)) { if (BleService.addCallback(this)) {
isRegisteredToBle = true isRegisteredToBle = true
FileLogger.d(TAG, "已注册BLE回调") Log.d(TAG, "已注册BLE回调")
} else { } else {
FileLogger.w(TAG, "BLE回调注册失败") Log.w(TAG, "BLE回调注册失败")
} }
} else { } else {
FileLogger.e(TAG, "BLE服务初始化失败") Log.e(TAG, "BLE服务初始化失败")
return false return false
} }
isInitialized = true isInitialized = true
FileLogger.d(TAG, "语音代理初始化成功") Log.d(TAG, "语音代理初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "初始化语音代理异常", e) Log.e(TAG, "初始化语音代理异常", e)
return false return false
} }
} }
@ -64,7 +64,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
*/ */
fun startListening(): Boolean { fun startListening(): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "语音代理未初始化") Log.e(TAG, "语音代理未初始化")
return false return false
} }
@ -106,7 +106,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
AgentService.dispose() AgentService.dispose()
isInitialized = false isInitialized = false
FileLogger.d(TAG, "语音代理资源已释放") Log.d(TAG, "语音代理资源已释放")
} }
//============================================================================================ //============================================================================================
@ -119,16 +119,16 @@ object BleAgent : BleService.Callback, AgentServiceListener {
override fun onConnectionStateChanged(state: Int) { override fun onConnectionStateChanged(state: Int) {
// 可选:处理连接状态变化 // 可选:处理连接状态变化
if (state == BleConst.STATE_CONNECTED) { if (state == BleConst.STATE_CONNECTED) {
FileLogger.d(TAG, "Ble设备已连接,准备好语音交互") Log.d(TAG, "Ble设备已连接,准备好语音交互")
} else if (state == BleConst.STATE_DISCONNECTED) { } else if (state == BleConst.STATE_DISCONNECTED) {
FileLogger.d(TAG, "Ble设备已断开,停止语音服务") Log.d(TAG, "Ble设备已断开,停止语音服务")
// 设备断开连接,停止语音服务 // 设备断开连接,停止语音服务
// stopListening() // stopListening()
} }
} }
override fun onAudioDataReceived(data: ByteArray) { override fun onAudioDataReceived(data: ByteArray) {
// FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}") // Log.d(TAG, "onAudioDataReceived, data: ${data.size}")
AgentService.pushAudioData(data) AgentService.pushAudioData(data)
// 可选:处理音频数据 // 可选:处理音频数据
} }
@ -138,7 +138,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
* 在收到唤醒信号时启动语音识别 * 在收到唤醒信号时启动语音识别
*/ */
override fun onWakeupSignalReceived() { override fun onWakeupSignalReceived() {
FileLogger.d(TAG, "收到唤醒信号,启动语音识别") Log.d(TAG, "收到唤醒信号,启动语音识别")
// 停止当前TTS,避免冲突 // 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse() AgentService.interruptCurrentResponse()

57
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -21,7 +21,7 @@ import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media.session.MediaButtonReceiver import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.agent_service.R import com.yunqiinnovation.agent_service.R
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
@ -56,6 +56,8 @@ class BleMediaService : Service(), CoroutineScope {
private const val KEY_OPENAI_MODEL = "openaiModel" private const val KEY_OPENAI_MODEL = "openaiModel"
private const val KEY_SYSTEM_PROMPT = "systemPrompt" private const val KEY_SYSTEM_PROMPT = "systemPrompt"
private const val KEY_MCP_SERVER = "mcpServer" private const val KEY_MCP_SERVER = "mcpServer"
private const val KEY_VOLCANO_APP_ID = "volcanoAppId"
private const val KEY_VOLCANO_TOKEN = "volcanoToken"
// 用于跟踪服务是否正在运行 // 用于跟踪服务是否正在运行
private val isRunning = AtomicBoolean(false) private val isRunning = AtomicBoolean(false)
@ -89,8 +91,7 @@ class BleMediaService : Service(), CoroutineScope {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FileLogger.init(this) Log.i(TAG, "BleMediaService正在创建...")
FileLogger.i(TAG, "BleMediaService正在创建...")
// 设置服务状态 // 设置服务状态
isRunning.set(true) isRunning.set(true)
@ -111,11 +112,11 @@ class BleMediaService : Service(), CoroutineScope {
audioPlayer?.play(R.raw.silence, 0f) audioPlayer?.play(R.raw.silence, 0f)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED) setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
FileLogger.i(TAG, "媒体服务已启动") Log.i(TAG, "媒体服务已启动")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// FileLogger.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}") Log.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}")
// 保存当前Intent,用于后续初始化 // 保存当前Intent,用于后续初始化
currentIntent = intent currentIntent = intent
@ -159,36 +160,36 @@ class BleMediaService : Service(), CoroutineScope {
// 如果Intent中有参数,则保存到SharedPreferences // 如果Intent中有参数,则保存到SharedPreferences
if (configFromIntent.isNotEmpty()) { if (configFromIntent.isNotEmpty()) {
FileLogger.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences")
saveConfigToPreferences(configFromIntent, prefs) saveConfigToPreferences(configFromIntent, prefs)
} }
// 合并Intent和SharedPreferences中的参数 // 合并Intent和SharedPreferences中的参数
val config = getConfigFromPreferences(prefs) val config = getConfigFromPreferences(prefs)
FileLogger.d(TAG, "当前配置参数: $config") Log.d(TAG, "当前配置参数: $config")
// 检查必要的配置是否存在 // 检查必要的配置是否存在
if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) { if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) {
FileLogger.w(TAG, "语音代理初始化失败: API密钥未配置") Log.w(TAG, "语音代理初始化失败: API密钥未配置")
return return
} }
if (isChatAgentInitialized) { if (isChatAgentInitialized) {
FileLogger.d(TAG, "ChatAgent已经初始化,跳过") Log.d(TAG, "ChatAgent已经初始化,跳过")
return return
} }
if(config.isNotEmpty()) { if(config.isNotEmpty()) {
// 初始化ChatAgent // 初始化ChatAgent
FileLogger.i(TAG, "开始初始化ChatAgent") Log.i(TAG, "开始初始化ChatAgent")
val success = BleAgent.initialize(applicationContext, config) val success = BleAgent.initialize(applicationContext, config)
if (success) { if (success) {
isChatAgentInitialized = true isChatAgentInitialized = true
FileLogger.i(TAG, "语音交互代理初始化成功") Log.i(TAG, "语音交互代理初始化成功")
} else { } else {
FileLogger.e(TAG, "语音交互代理初始化失败") Log.e(TAG, "语音交互代理初始化失败")
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "初始化语音交互代理时发生异常", e) Log.e(TAG, "初始化语音交互代理时发生异常", e)
} }
} }
@ -206,8 +207,10 @@ class BleMediaService : Service(), CoroutineScope {
it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value } it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value }
it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value } it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value }
it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value } it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value }
it.getStringExtra(KEY_VOLCANO_APP_ID)?.let { value -> config[KEY_VOLCANO_APP_ID] = value }
it.getStringExtra(KEY_VOLCANO_TOKEN)?.let { value -> config[KEY_VOLCANO_TOKEN] = value }
} }
FileLogger.i(TAG, "从Intent中提取到配置参数: $config") Log.i(TAG, "从Intent中提取到配置参数: $config")
return config return config
} }
@ -236,7 +239,9 @@ class BleMediaService : Service(), CoroutineScope {
KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"), KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"),
KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"), KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"),
KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"), KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"),
KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, "") KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""),
KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""),
KEY_VOLCANO_TOKEN to getConfigString(prefs, KEY_VOLCANO_TOKEN, "")
) )
} }
@ -250,15 +255,15 @@ class BleMediaService : Service(), CoroutineScope {
override fun onBind(intent: Intent?): IBinder? = null override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() { override fun onDestroy() {
FileLogger.i(TAG, "BleMediaService正在销毁...") Log.i(TAG, "BleMediaService正在销毁...")
// 取消注册媒体按钮接收器 // 取消注册媒体按钮接收器
try { try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver) audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.i(TAG, "媒体按钮接收器已取消注册") Log.i(TAG, "媒体按钮接收器已取消注册")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e) Log.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e)
} }
// 释放媒体会话 // 释放媒体会话
@ -319,16 +324,16 @@ class BleMediaService : Service(), CoroutineScope {
try { try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver) audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") Log.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
} }
} }
private fun checkBleConnection() { private fun checkBleConnection() {
FileLogger.i(TAG, "checkBleConnection收到连接请求") Log.i(TAG, "checkBleConnection收到连接请求")
try { try {
// 示例:使用协程异步处理连接 // 示例:使用协程异步处理连接
@ -338,7 +343,7 @@ class BleMediaService : Service(), CoroutineScope {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "连接设备失败: ${e.message}", e) Log.e(TAG, "连接设备失败: ${e.message}", e)
} }
} }
@ -350,7 +355,7 @@ class BleMediaService : Service(), CoroutineScope {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, createNotification()) notificationManager.notify(NOTIFICATION_ID, createNotification())
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "更新通知异常: ${e.message}") Log.e(TAG, "更新通知异常: ${e.message}")
} }
} }
@ -451,7 +456,7 @@ class BleMediaService : Service(), CoroutineScope {
// // 播放完成后自动循环播放,保持媒体会话活跃 // // 播放完成后自动循环播放,保持媒体会话活跃
// setOnCompletionListener { // setOnCompletionListener {
// FileLogger.i(TAG, "静音资源播放完成,再次播放保持会话活跃") // Log.i(TAG, "静音资源播放完成,再次播放保持会话活跃")
// play(resId, volume) // play(resId, volume)
// } // }
@ -460,9 +465,9 @@ class BleMediaService : Service(), CoroutineScope {
start() start()
} }
FileLogger.i(TAG, "开始播放静音资源") Log.i(TAG, "开始播放静音资源")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "播放静音资源异常: ${e.message}", e) Log.e(TAG, "播放静音资源异常: ${e.message}", e)
release() release()
} }
} }

14
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt

@ -3,7 +3,7 @@ package com.yunqiinnovation.agent_service
import android.content.Context import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey import androidx.security.crypto.MasterKey
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
/** /**
* 通用工具类,包含安全存储等功能 * 通用工具类,包含安全存储等功能
@ -59,10 +59,10 @@ object Utils {
// 提交更改 // 提交更改
editor.apply() editor.apply()
FileLogger.d(TAG, "数据已安全保存到加密存储中") Log.d(TAG, "数据已安全保存到加密存储中")
true true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "保存数据到加密存储失败", e) Log.e(TAG, "保存数据到加密存储失败", e)
false false
} }
} }
@ -97,10 +97,10 @@ object Utils {
} }
} }
FileLogger.d(TAG, "已从加密存储加载所有数据") Log.d(TAG, "已从加密存储加载所有数据")
resultMap resultMap
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "从加密存储加载所有数据失败", e) Log.e(TAG, "从加密存储加载所有数据失败", e)
emptyMap() emptyMap()
} }
} }
@ -134,10 +134,10 @@ object Utils {
.putString(KEY_SESSION_ID, sessionId) .putString(KEY_SESSION_ID, sessionId)
.apply() .apply()
FileLogger.d(TAG, "已清除所有安全存储的数据") Log.d(TAG, "已清除所有安全存储的数据")
true true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "清除安全存储数据失败", e) Log.e(TAG, "清除安全存储数据失败", e)
false false
} }
} }

14
local_plugins/agent_service/pubspec.yaml

@ -10,20 +10,6 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
# 依赖本地插件
open_ai_service:
path: ../open_ai_service
azure_speech:
path: ../azure_speech
deepvoice_utils:
path: ../deepvoice_utils
chat_storage:
path: ../chat_storage
music_service:
path: ../music_service
amap_service:
path: ../amap_service
get: ^4.6.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

5
local_plugins/azure_speech/android/build.gradle.kts

@ -52,6 +52,7 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
dependencies { dependencies {
// 添加Microsoft语音SDK // 添加Microsoft语音SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.30.0") implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.43.0")
implementation(project(":ble_service")) implementation(project(":speech"))
} }

51
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -10,7 +10,7 @@ import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl import android.media.audiofx.AutomaticGainControl
import android.os.Process import android.os.Process
import com.yunqiinnovation.azure_speech.utils.FileLogger import android.util.Log
import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler import com.microsoft.cognitiveservices.speech.util.EventHandler
@ -84,7 +84,7 @@ class AzureAsrHelper(private val context: Context) {
try { try {
// 检查配置是否为空 // 检查配置是否为空
if (subscriptionKey.isEmpty() || region.isEmpty()) { if (subscriptionKey.isEmpty() || region.isEmpty()) {
FileLogger.e(tag, "Azure 配置信息不完整") Log.e(tag, "Azure 配置信息不完整")
return false return false
} }
@ -124,7 +124,7 @@ class AzureAsrHelper(private val context: Context) {
// 创建识别器 // 创建识别器
return setupRecognizer() return setupRecognizer()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "初始化失败: ${e.message}") Log.e(tag, "初始化失败: ${e.message}")
return false return false
} }
} }
@ -184,7 +184,7 @@ class AzureAsrHelper(private val context: Context) {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "创建识别器失败: ${e.message}") Log.e(tag, "创建识别器失败: ${e.message}")
stopAudioProcessing() stopAudioProcessing()
return false return false
} }
@ -201,8 +201,8 @@ class AzureAsrHelper(private val context: Context) {
// 创建音频配置 - 正确使用fromStreamInput方法,只传入回调 // 创建音频配置 - 正确使用fromStreamInput方法,只传入回调
audioConfig = AudioConfig.fromStreamInput(microphoneStream) audioConfig = AudioConfig.fromStreamInput(microphoneStream)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "设置麦克风流失败: ${e.message}") Log.e(tag, "设置麦克风流失败: ${e.message}")
e.printStackTrace() microphoneStream = null
} }
} }
@ -217,8 +217,8 @@ class AzureAsrHelper(private val context: Context) {
// 创建音频配置 // 创建音频配置
audioConfig = AudioConfig.fromStreamInput(externalAudioStream) audioConfig = AudioConfig.fromStreamInput(externalAudioStream)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "设置外部音频流失败: ${e.message}") Log.e(tag, "设置外部音频流失败: ${e.message}")
e.printStackTrace() externalAudioStream = null
} }
} }
@ -368,23 +368,13 @@ class AzureAsrHelper(private val context: Context) {
// 取消事件 // 取消事件
recognizer?.canceled?.addEventListener( recognizer?.canceled?.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event -> EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = try { val errorDetails = event.errorDetails ?: "未知错误"
event.errorDetails ?: "未知错误"
} catch (e: Exception) {
"未知错误"
}
val reason = event.reason.toString() val reason = event.reason.toString()
FileLogger.e(tag, "识别取消: $errorDetails")
isContinuousRecognitionActive = false
stopAudioProcessing()
// 直接在当前线程调用回调
callback.onCanceled(reason, errorDetails) callback.onCanceled(reason, errorDetails)
callback.onError("识别取消: $errorDetails") // 兼容旧接口
} }
) )
} }
/** /**
* 停止连续语音识别 * 停止连续语音识别
@ -393,7 +383,7 @@ class AzureAsrHelper(private val context: Context) {
*/ */
fun stopContinuousRecognition(): Boolean { fun stopContinuousRecognition(): Boolean {
if (speechConfig == null) { if (speechConfig == null) {
FileLogger.e(tag, "语音服务未初始化") Log.e(tag, "语音服务未初始化")
return false return false
} }
@ -403,7 +393,7 @@ class AzureAsrHelper(private val context: Context) {
try { try {
if (recognizer == null) { if (recognizer == null) {
FileLogger.w(tag, "识别器为空,重置状态") Log.w(tag, "识别器为空,重置状态")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
return true return true
} }
@ -422,7 +412,7 @@ class AzureAsrHelper(private val context: Context) {
} catch (e: Exception) { } catch (e: Exception) {
// 强制重置状态 // 强制重置状态
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
FileLogger.e(tag, "停止连续识别失败: ${e.message}") Log.e(tag, "停止连续识别失败: ${e.message}")
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
@ -432,7 +422,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.close() recognizer?.close()
recognizer = null recognizer = null
} catch (ex: Exception) { } catch (ex: Exception) {
FileLogger.e(tag, "关闭识别器失败: ${ex.message}") Log.e(tag, "关闭识别器失败: ${ex.message}")
} }
return false return false
@ -515,7 +505,7 @@ class AzureAsrHelper(private val context: Context) {
it.close() it.close()
microphoneStream = null microphoneStream = null
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "关闭麦克风流失败: ${e.message}") Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
} }
} }
@ -525,7 +515,7 @@ class AzureAsrHelper(private val context: Context) {
it.close() it.close()
externalAudioStream = null externalAudioStream = null
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "关闭外部音频流失败: ${e.message}") Log.e(tag, "关闭外部音频流失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
} }
} }
@ -623,12 +613,13 @@ class AzureAsrHelper(private val context: Context) {
try { try {
val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1 val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1
if (result < 0) { if (result < 0) {
FileLogger.e(tag, "读取音频数据失败: $result") Log.e(tag, "读取音频数据失败: $result")
return 0
} }
return result return result
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "读取音频异常: ${e.message}") Log.e(tag, "读取音频异常: ${e.message}")
return -1 return 0
} }
} }
@ -696,7 +687,7 @@ class AzureAsrHelper(private val context: Context) {
Thread.currentThread().interrupt() Thread.currentThread().interrupt()
return 0 return 0
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "读取外部音频数据失败: ${e.message}") Log.e(tag, "读取外部音频数据失败: ${e.message}")
return 0 return 0
} }
} }

224
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -11,21 +11,16 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.audio.*
import com.yunqiinnovation.ble_service.BleService import com.deep_voice.speech.tts.TtsEvent
import com.yunqiinnovation.ble_service.BleConst import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
/** AzureSpeechPlugin */ /** AzureSpeechPlugin */
class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { class AzureSpeechPlugin: FlutterPlugin {
private val tag = "AzureSpeechPlugin" private val tag = "AzureSpeechPlugin"
private lateinit var context: Context private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// ASR相关 // ASR相关
private lateinit var asrChannel : MethodChannel private lateinit var asrChannel : MethodChannel
@ -38,9 +33,8 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
private lateinit var ttsEventChannel: EventChannel private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null private var ttsEventSink: EventChannel.EventSink? = null
private lateinit var azureTtsHelper: AzureTtsHelper private lateinit var azureTtsHelper: AzureTtsHelper
// 是否已注册BLE回调 // 是否已添加TTS事件监听器
private var isRegisteredToBle = false private var isTtsListenerAdded = false
// ASR 事件发送方法 // ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) { private fun sendAsrEvent(event: Map<String, Any>) {
@ -66,7 +60,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
} }
// 确保在主线程上发送事件 // 确保在主线程上发送事件
mainHandler.post { mainHandler.post {
try { try {
ttsEventSink?.success(event) ttsEventSink?.success(event)
@ -104,6 +97,7 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler { ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events ttsEventSink = events
setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器
} }
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
@ -114,19 +108,53 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
// 初始化Azure语音服务 // 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context) azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context) azureAsrHelper = AzureAsrHelper(context)
}
// 2. 初始化BleService并注册回调 /**
if (BleService.initialize(context)) { * 设置TTS事件监听器
if (BleService.addCallback(this)) { */
isRegisteredToBle = true private fun setupTtsEventListener() {
FileLogger.d(tag, "已注册BLE回调") if (!isTtsListenerAdded) {
} else { // 添加TTS事件监听器
FileLogger.w(tag, "BLE回调注册失败") azureTtsHelper.addListener(object : TtsEventListener {
} override fun onEvent(event: TtsEvent) {
} else { // 将TTS事件转换为Flutter可理解的格式
FileLogger.e(tag, "BLE服务初始化失败") val eventMap = when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> mapOf(
"type" to "synthesis_started"
)
TtsEventType.SYNTHESIS_COMPLETED -> mapOf(
"type" to "synthesis_completed"
)
TtsEventType.SYNTHESIS_CANCELED -> {
val params = event.params
mapOf(
"type" to "synthesis_canceled",
"reason" to (params["reason"] ?: ""),
"errorDetails" to (params["errorDetails"] ?: "")
)
} }
TtsEventType.ERROR -> {
val params = event.params
mapOf(
"type" to "error",
"errorCode" to (params["errorCode"] ?: "UNKNOWN_ERROR"),
"errorMessage" to (params["errorMessage"] ?: "未知错误")
)
}
else -> mapOf(
"type" to event.type.toString().lowercase(),
"params" to event.params
)
}
// 发送事件
sendTtsEvent(eventMap)
}
})
isTtsListenerAdded = true
}
} }
// ASR方法处理器 // ASR方法处理器
@ -265,6 +293,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
azureAsrHelper.dispose() azureAsrHelper.dispose()
result.success(true) result.success(true)
} }
"pushAudioData" -> {
val data = call.argument<ByteArray>("data") ?: return result.error("INVALID_ARGUMENTS", "音频数据不能为空", null)
azureAsrHelper.pushAudioData(data)
result.success(true)
}
else -> { else -> {
result.notImplemented() result.notImplemented()
} }
@ -279,34 +312,36 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
"initialize" -> { "initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: "" val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: "" val region = call.argument<String>("region") ?: ""
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false // 默认使用自定义播放器 val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false
// 先释放现有资源 try {
azureTtsHelper.dispose() // 初始化TTS引擎(使用新的接口方法)
val success = azureTtsHelper.initialize(
// 创建TTS回调 ttsAppId = "", // Azure TTS不需要appId
val ttsCallback = createTtsCallback() ttsAppToken = subscriptionKey,
ttsResource = region,
// 初始化TTS引擎 language = "zh-CN"
val success = azureTtsHelper.initialize( )
subscriptionKey = subscriptionKey,
region = region,
callback = ttsCallback
)
// 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
if (success && useCustomAudioOutput) {
// 创建SimpleAudioPlayer实例
val customPlayer = SimpleAudioPlayer(context)
// 获取并设置音频输出流 // 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
val outputStream = customPlayer.getAudioOutputStream() if (success && useCustomAudioOutput) {
if (outputStream != null) { // 创建SimpleAudioPlayer实例
azureTtsHelper.setCustomAudioOutputStream(outputStream) val customPlayer = SimpleAudioPlayer(context)
// 获取并设置音频输出流
val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream)
}
} }
// 确保设置了TTS事件监听器
setupTtsEventListener()
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", "TTS初始化失败: ${e.message}", null)
} }
result.success(success)
} }
"setVoice" -> { "setVoice" -> {
val voiceName = call.argument<String>("voiceName") ?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null) val voiceName = call.argument<String>("voiceName") ?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
@ -315,18 +350,28 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
"speakText" -> { "speakText" -> {
val text = call.argument<String>("text") ?: return result.error("INVALID_ARGUMENTS", "文本不能为空", null) val text = call.argument<String>("text") ?: return result.error("INVALID_ARGUMENTS", "文本不能为空", null)
// 直接传递参数给speakText方法 // 直接调用speakOnce方法
val success = azureTtsHelper.speakText(text) try {
result.success(success) val success = azureTtsHelper.speakOnce(text)
result.success(success)
} catch (e: Exception) {
result.error("SPEAK_ERROR", "语音合成失败: ${e.message}", null)
}
} }
"stopSpeaking" -> { "stopSpeaking" -> {
result.success(azureTtsHelper.stopSpeaking()) // 直接调用stop方法
try {
val success = azureTtsHelper.stop()
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", "停止语音合成失败: ${e.message}", null)
}
} }
"isSpeaking" -> { "isSpeaking" -> {
result.success(azureTtsHelper.isSpeaking()) result.success(azureTtsHelper.isSpeaking())
} }
"dispose" -> { "dispose" -> {
azureTtsHelper.dispose() azureTtsHelper.release()
result.success(true) result.success(true)
} }
else -> { else -> {
@ -334,37 +379,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
} }
} }
} }
/**
* 创建TTS回调
*/
private fun createTtsCallback(): AzureTtsHelper.TtsCallback {
return object : AzureTtsHelper.TtsCallback {
override fun onSynthesisStarted() {
sendTtsEvent(mapOf(
"type" to "synthesis_started"
))
}
override fun onSynthesizing() {
sendTtsEvent(mapOf(
"type" to "synthesizing"
))
}
override fun onSynthesisCompleted() {
sendTtsEvent(mapOf(
"type" to "synthesis_completed"
))
}
override fun onSynthesisCanceled() {
sendTtsEvent(mapOf(
"type" to "synthesis_canceled"
))
}
}
}
} }
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
@ -374,49 +388,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
ttsEventChannel.setStreamHandler(null) ttsEventChannel.setStreamHandler(null)
try { try {
// 取消所有协程
job.cancel()
// 取消BLE回调注册
if (isRegisteredToBle) {
BleService.removeCallback(this)
isRegisteredToBle = false
}
// 释放资源 // 释放资源
azureTtsHelper.dispose() azureTtsHelper.release()
azureAsrHelper.dispose() azureAsrHelper.dispose()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "释放资源失败: ${e.message}") FileLogger.e(tag, "释放资源失败: ${e.message}")
} }
} }
override fun onScanResult(devices: List<Map<String, Any>>) {
}
override fun onConnectionStateChanged(state: Int) {
// 可选:处理连接状态变化
if (state == BleConst.STATE_CONNECTED) {
FileLogger.d(tag, "Ble设备已连接,准备好语音交互")
} else if (state == BleConst.STATE_DISCONNECTED) {
FileLogger.d(tag, "Ble设备已断开,停止语音服务")
// 设备断开连接,停止语音服务
// stopListening()
}
}
override fun onAudioDataReceived(data: ByteArray) {
// FileLogger.d(tag, "AzureSpeechPlugin onAudioDataReceived, data: ${data.size}")
azureAsrHelper.pushAudioData(data)
}
override fun onWakeupSignalReceived() {
}
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
}
} }

298
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -4,10 +4,14 @@ import android.content.Context
import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.audio.*
import com.yunqiinnovation.azure_speech.utils.FileLogger import com.yunqiinnovation.azure_speech.utils.FileLogger
import com.deep_voice.speech.tts.ITtsService
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.deep_voice.speech.tts.AudioDataListener
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import kotlin.coroutines.CoroutineContext
/** /**
* Azure TTS Helper * Azure TTS Helper
@ -15,18 +19,13 @@ import kotlin.coroutines.CoroutineContext
* 基于微软Azure语音服务的TTS实现 * 基于微软Azure语音服务的TTS实现
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis
*/ */
class AzureTtsHelper(private val context: Context) : CoroutineScope { class AzureTtsHelper(private val context: Context) : ITtsService {
companion object { companion object {
private const val TAG = "AzureTtsHelper" private const val TAG = "AzureTtsHelper"
private const val DEFAULT_LANGUAGE = "zh-CN" private const val DEFAULT_LANGUAGE = "zh-CN"
private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
} }
// 协程上下文
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
// Azure语音服务配置 // Azure语音服务配置
private var speechConfig: SpeechConfig? = null private var speechConfig: SpeechConfig? = null
private var synthesizer: SpeechSynthesizer? = null private var synthesizer: SpeechSynthesizer? = null
@ -39,8 +38,9 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
private var currentPitch = "0%" private var currentPitch = "0%"
private var currentVolume = "100%" private var currentVolume = "100%"
// 全局回调 // 事件监听器列表
private var ttsCallback: TtsCallback? = null private val eventListeners = mutableListOf<TtsEventListener>()
private val audioDataListeners = mutableListOf<AudioDataListener>()
// 自定义音频输出流 // 自定义音频输出流
private var customAudioOutputStream: PushAudioOutputStream? = null private var customAudioOutputStream: PushAudioOutputStream? = null
@ -48,54 +48,28 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
// 流式文本处理的缓冲区 // 流式文本处理的缓冲区
private val streamBuffer = StringBuilder() private val streamBuffer = StringBuilder()
private var lastSpeakTime = 0L private var lastSpeakTime = 0L
/**
* 简化的TTS回调接口
*/
interface TtsCallback {
/**
* 合成开始
*/
fun onSynthesisStarted() {}
/**
* 合成中
*/
fun onSynthesizing() {}
/**
* 合成完成
*/
fun onSynthesisCompleted() {}
/**
* 合成取消
*/
fun onSynthesisCanceled() {}
}
/** /**
* 初始化TTS引擎 * 初始化TTS引擎
* *
* @param subscriptionKey Azure语音服务订阅密钥 * @param ttsAppId 未使用,保留为空即可
* @param region Azure语音服务区域 * @param ttsAppToken Azure语音服务订阅密钥
* @param callback 可选,全局TTS回调接口 * @param ttsResource Azure语音服务区域
* @param language 语言代码,如"zh-CN"
* @return 是否初始化成功 * @return 是否初始化成功
*/ */
fun initialize( override fun initialize(
subscriptionKey: String, ttsAppId: String,
region: String, ttsAppToken: String,
callback: TtsCallback? = null ttsResource: String,
language: String
): Boolean { ): Boolean {
try { try {
// 释放之前的资源 // 释放之前的资源
dispose() dispose()
// 存储全局回调
ttsCallback = callback
// 创建语音配置 // 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region) speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource)
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm) speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm)
speechConfig?.setSpeechSynthesisVoiceName(currentVoice) speechConfig?.setSpeechSynthesisVoiceName(currentVoice)
@ -115,7 +89,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
setupEventListeners() setupEventListeners()
isInitialized = true isInitialized = true
FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$region") FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -148,12 +122,51 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
} }
/** /**
* 设置TTS回调 * 添加TTS事件监听器
* */
* @param callback TTS回调接口 override fun addListener(listener: TtsEventListener) {
if (!eventListeners.contains(listener)) {
eventListeners.add(listener)
}
}
/**
* 移除TTS事件监听器
*/
override fun removeListener(listener: TtsEventListener) {
eventListeners.remove(listener)
}
/**
* 添加音频数据监听器
*/
override fun addAudioDataListener(listener: AudioDataListener) {
if (!audioDataListeners.contains(listener)) {
audioDataListeners.add(listener)
}
}
/**
* 移除音频数据监听器
*/
override fun removeAudioDataListener(listener: AudioDataListener) {
audioDataListeners.remove(listener)
}
/**
* 触发事件通知
*/ */
fun setTtsCallback(callback: TtsCallback?) { private fun notifyEvent(eventType: TtsEventType, params: Map<String, Any> = emptyMap()) {
ttsCallback = callback val event = TtsEvent(eventType, params)
// 复制列表防止并发修改异常
val listeners = ArrayList(eventListeners)
for (listener in listeners) {
try {
listener.onEvent(event)
} catch (e: Exception) {
FileLogger.e(TAG, "通知事件异常: ${e.message}")
}
}
} }
/** /**
@ -163,33 +176,29 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
synthesizer?.apply { synthesizer?.apply {
// 合成开始事件 // 合成开始事件
SynthesisStarted?.addEventListener { _, _ -> SynthesisStarted?.addEventListener { _, _ ->
ttsCallback?.onSynthesisStarted() notifyEvent(TtsEventType.SYNTHESIS_STARTED)
}
// 合成中事件
Synthesizing?.addEventListener { _, _ ->
ttsCallback?.onSynthesizing()
} }
// 合成完成事件 // 合成完成事件
SynthesisCompleted?.addEventListener { _, _ -> SynthesisCompleted?.addEventListener { _, _ ->
isSpeaking = false isSpeaking = false
ttsCallback?.onSynthesisCompleted() notifyEvent(TtsEventType.SYNTHESIS_COMPLETED)
} }
// 合成取消事件 // 合成取消事件
SynthesisCanceled?.addEventListener { _, _ -> SynthesisCanceled?.addEventListener { _, eventArgs ->
FileLogger.d(TAG, "语音合成取消") FileLogger.d(TAG, "语音合成取消")
isSpeaking = false isSpeaking = false
ttsCallback?.onSynthesisCanceled()
notifyEvent(TtsEventType.SYNTHESIS_CANCELED)
} }
} }
} }
/** /**
* 设置语音 * 设置语音角色
*/ */
fun setVoice(voiceName: String): Boolean { override fun setVoice(voiceName: String): Boolean {
if (!isInitialized) return false if (!isInitialized) return false
try { try {
@ -199,6 +208,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置语音失败: ${e.message}") FileLogger.e(TAG, "设置语音失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "VOICE_SET_FAILED",
"errorMessage" to "设置语音失败: ${e.message}"
))
return false return false
} }
} }
@ -224,6 +237,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
setupEventListeners() setupEventListeners()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "重新创建合成器失败: ${e.message}") FileLogger.e(TAG, "重新创建合成器失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "RECREATE_FAILED",
"errorMessage" to "重新创建合成器失败: ${e.message}"
))
} }
} }
@ -240,6 +257,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置语音参数失败: ${e.message}") FileLogger.e(TAG, "设置语音参数失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "PARAMS_SET_FAILED",
"errorMessage" to "设置语音参数失败: ${e.message}"
))
return false return false
} }
} }
@ -255,50 +276,52 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
* 生成SSML * 生成SSML
*/ */
private fun generateSsml(rawText: String): String { private fun generateSsml(rawText: String): String {
// 1. 定义要静音的符号和表情符号列表 // 1. 定义要静音的符号和表情符号列表
val symbolsToMute = listOf( val symbolsToMute = listOf(
"#", "*", "@", "%", "^", "&", "#", "*", "@", "%", "^", "&",
"😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉" "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
) )
// 2. 转义 XML 保留字符 // 2. 转义 XML 保留字符
val escapedText = rawText val escapedText = rawText
.replace("&", "&amp;") .replace("&", "&amp;")
.replace("<", "&lt;") .replace("<", "&lt;")
.replace(">", "&gt;") .replace(">", "&gt;")
// 3. 静音处理特殊符号和表情符号 // 3. 静音处理特殊符号和表情符号
// 使用空白替换法,直接将符号替换为空格 // 使用空白替换法,直接将符号替换为空格
var processedText = escapedText var processedText = escapedText
symbolsToMute.forEach { sym -> symbolsToMute.forEach { sym ->
processedText = processedText.replace(sym, "") processedText = processedText.replace(sym, "")
} }
// 4. 构造简化的SSML文档,减少嵌套层级 // 4. 构造简化的SSML文档,减少嵌套层级
return """ return """
<speak version="1.0" <speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis" xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="https://www.w3.org/2001/mstts" xmlns:mstts="https://www.w3.org/2001/mstts"
xml:lang="zh-CN"> xml:lang="zh-CN">
<voice name="$currentVoice"> <voice name="$currentVoice">
<mstts:express-as style="cheerful"> <mstts:express-as style="cheerful">
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume"> <prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume">
<say-as interpret-as="text">$processedText</say-as> <say-as interpret-as="text">$processedText</say-as>
</prosody> </prosody>
</mstts:express-as> </mstts:express-as>
</voice> </voice>
</speak> </speak>
""".trimIndent() """.trimIndent()
} }
/** /**
* 合成文本为语音 * 单次播放文本(非流式)
*
* @param text 要合成的文本
* @return 是否成功开始合成
*/ */
fun speakText(text: String): Boolean { override fun speakOnce(text: String): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "TTS引擎未初始化")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
return false return false
} }
@ -308,38 +331,47 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
isSpeaking = true isSpeaking = true
// 使用纯异步方式 // 直接调用SpeakSsmlAsync,SDK内部已经是异步的
synthesizer?.SpeakSsmlAsync(ssml) synthesizer?.SpeakSsmlAsync(ssml)
// 返回true表示已开始合成请求
return true return true
} catch (e: Exception) { } catch (e: Exception) {
isSpeaking = false isSpeaking = false
FileLogger.e(TAG, "语音合成异常: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "SYNTHESIS_ERROR",
"errorMessage" to "语音合成异常: ${e.message}"
))
return false return false
} }
} }
/** /**
* 处理流式文本并在遇到标点符号时播放 * 流式合成文本
*
* @param text 收到的文本流片段
* @return 是否成功处理
*/ */
fun speakStream(text: String): Boolean { override fun speakStream(text: String): Boolean {
if (!isInitialized || text.isBlank()) { if (!isInitialized || text.isBlank()) {
if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
}
return false return false
} }
try { try {
// 添加新文本到缓冲区 // 添加新文本到缓冲区
streamBuffer.append(text) streamBuffer.append(text)
// 增加500ms防抖逻辑 // 增加500ms防抖逻辑
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastSpeakTime < 600) { if (currentTime - lastSpeakTime < 600) {
return true return true
} }
lastSpeakTime = currentTime lastSpeakTime = currentTime
val currentText = streamBuffer.toString() val currentText = streamBuffer.toString()
// 定义标点符号列表 // 定义标点符号列表
@ -363,24 +395,30 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
streamBuffer.delete(0, lastPunctuationIndex + 1) streamBuffer.delete(0, lastPunctuationIndex + 1)
// 播放提取的文本 // 播放提取的文本
return speakText(textToSpeak) return speakOnce(textToSpeak)
} }
// 如果没有找到标点符号,则等待更多文本 // 如果没有找到标点符号,则等待更多文本
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "流式语音合成失败: ${e.message}") FileLogger.e(TAG, "流式语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "STREAM_FAILED",
"errorMessage" to "流式语音合成失败: ${e.message}"
))
return false return false
} }
} }
/** /**
* 播放剩余的流式文本 * 播放剩余的流式文本
*
* @return 是否成功播放剩余文本
*/ */
fun flushStream(): Boolean { override fun flushStream(): Boolean {
if (!isInitialized) { if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
return false return false
} }
@ -397,9 +435,13 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
} }
// 播放剩余文本 // 播放剩余文本
return speakText(remainingText) return speakOnce(remainingText)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "刷新流式文本失败: ${e.message}") FileLogger.e(TAG, "刷新流式文本失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "FLUSH_FAILED",
"errorMessage" to "刷新流式文本失败: ${e.message}"
))
return false return false
} }
} }
@ -407,7 +449,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
/** /**
* 停止当前语音合成 * 停止当前语音合成
*/ */
fun stopSpeaking(): Boolean { override fun stop(): Boolean {
if (!isInitialized) return false if (!isInitialized) return false
try { try {
@ -416,6 +458,11 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
isSpeaking = false isSpeaking = false
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "STOP_FAILED",
"errorMessage" to "停止语音合成失败: ${e.message}"
))
return false return false
} }
} }
@ -423,11 +470,25 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
/** /**
* 释放资源 * 释放资源
*/ */
fun dispose() { override fun release() {
dispose()
}
/**
* 释放资源(内部方法)
*/
private fun dispose() {
try { try {
stopSpeaking() // 停止播放
stop()
// 关闭合成器
synthesizer?.close() synthesizer?.close()
synthesizer = null
// 关闭配置
speechConfig?.close() speechConfig?.close()
speechConfig = null
// 关闭自定义音频输出流 // 关闭自定义音频输出流
try { try {
@ -437,15 +498,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
// 忽略关闭异常 // 忽略关闭异常
} }
// 清理引用
ttsCallback = null
// 清空流缓冲区 // 清空流缓冲区
streamBuffer.clear() streamBuffer.clear()
// 取消所有协程 // 重置状态
job.cancel()
isInitialized = false isInitialized = false
isSpeaking = false isSpeaking = false
} catch (e: Exception) { } catch (e: Exception) {

2
local_plugins/ble_service/android/build.gradle.kts

@ -30,5 +30,5 @@ dependencies {
implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.core:core-ktx:1.13.1")
// 使用从本地Maven仓库引用的AAR依赖 // 使用从本地Maven仓库引用的AAR依赖
implementation("com.jieli.audio:jl_audio_decode:2.0.0") implementation("com.jieli.audio:jl_audio_decode:2.0.0")
implementation(project(":deepvoice_utils")) implementation("com.jieli.jl_bt_ota:jl_bt_ota:1.9.2")
} }

BIN
local_plugins/ble_service/android/libs/jl_audio_decode_V2.0.0_20005.aar

Binary file not shown.

BIN
local_plugins/ble_service/android/libs/jl_bt_ota_V1.9.2-release.aar

Binary file not shown.

74
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt

@ -17,7 +17,7 @@ import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.os.ParcelUuid import android.os.ParcelUuid
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import java.util.regex.Pattern import java.util.regex.Pattern
/** /**
@ -71,7 +71,7 @@ class BleCompanionManager(private val context: Context) {
*/ */
fun savePairedMacAddress(macAddress: String) { fun savePairedMacAddress(macAddress: String) {
sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply() sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply()
FileLogger.i(TAG, "已保存配对设备MAC: $macAddress") Log.i(TAG, "已保存配对设备MAC: $macAddress")
} }
/** /**
@ -79,7 +79,7 @@ class BleCompanionManager(private val context: Context) {
*/ */
fun clearPairedMacAddress() { fun clearPairedMacAddress() {
sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply() sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply()
FileLogger.i(TAG, "已清除配对设备MAC") Log.i(TAG, "已清除配对设备MAC")
} }
/** /**
@ -110,7 +110,7 @@ class BleCompanionManager(private val context: Context) {
idField.get(association).toString() idField.get(association).toString()
} ?: emptyList() } ?: emptyList()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) Log.e(TAG, "获取关联ID失败: ${e.message}", e)
emptyList() emptyList()
} }
} else { } else {
@ -135,7 +135,7 @@ class BleCompanionManager(private val context: Context) {
macAddress: String? = null macAddress: String? = null
): Boolean { ): Boolean {
if (!isCompanionDeviceManagerSupported()) { if (!isCompanionDeviceManagerSupported()) {
FileLogger.e(TAG, "设备不支持CompanionDeviceManager") Log.e(TAG, "设备不支持CompanionDeviceManager")
return false return false
} }
@ -148,7 +148,7 @@ class BleCompanionManager(private val context: Context) {
// 使用"^设备名称$"格式创建精确匹配的正则表达式 // 使用"^设备名称$"格式创建精确匹配的正则表达式
val exactNamePattern = Pattern.compile("^$it$") val exactNamePattern = Pattern.compile("^$it$")
deviceFilterBuilder.setNamePattern(exactNamePattern) deviceFilterBuilder.setNamePattern(exactNamePattern)
FileLogger.i(TAG, "设置严格名称匹配: $it") Log.i(TAG, "设置严格名称匹配: $it")
} }
// 添加服务UUID // 添加服务UUID
@ -156,9 +156,9 @@ class BleCompanionManager(private val context: Context) {
try { try {
val uuid = ParcelUuid(java.util.UUID.fromString(it)) val uuid = ParcelUuid(java.util.UUID.fromString(it))
deviceFilterBuilder.addServiceUuid(uuid, null) deviceFilterBuilder.addServiceUuid(uuid, null)
FileLogger.i(TAG, "设置服务UUID匹配: $it") Log.i(TAG, "设置服务UUID匹配: $it")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置服务UUID失败: ${e.message}", e) Log.e(TAG, "设置服务UUID失败: ${e.message}", e)
} }
} }
@ -167,7 +167,7 @@ class BleCompanionManager(private val context: Context) {
try { try {
// 检查是否是有效的MAC地址格式 // 检查是否是有效的MAC地址格式
if (!isValidMacAddress(it)) { if (!isValidMacAddress(it)) {
FileLogger.e(TAG, "无效的MAC地址格式: $it") Log.e(TAG, "无效的MAC地址格式: $it")
return false return false
} }
@ -176,15 +176,15 @@ class BleCompanionManager(private val context: Context) {
// 设置MAC地址严格匹配 // 设置MAC地址严格匹配
deviceFilterBuilder.setAddress(it) deviceFilterBuilder.setAddress(it)
FileLogger.i(TAG, "设置MAC地址严格匹配: $it") Log.i(TAG, "设置MAC地址严格匹配: $it")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置MAC地址失败: ${e.message}", e) Log.e(TAG, "设置MAC地址失败: ${e.message}", e)
return false return false
} }
} ?: run { } ?: run {
// 如果没有提供MAC地址也没有提供设备名称,则返回错误 // 如果没有提供MAC地址也没有提供设备名称,则返回错误
if (deviceNamePattern == null && serviceUuid == null) { if (deviceNamePattern == null && serviceUuid == null) {
FileLogger.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一") Log.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一")
return false return false
} }
} }
@ -209,9 +209,9 @@ class BleCompanionManager(private val context: Context) {
0, 0,
0 0
) )
FileLogger.i(TAG, "已启动设备选择对话框") Log.i(TAG, "已启动设备选择对话框")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
} }
} }
@ -228,28 +228,28 @@ class BleCompanionManager(private val context: Context) {
0, 0,
0 0
) )
FileLogger.i(TAG, "已启动设备选择对话框") Log.i(TAG, "已启动设备选择对话框")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
} }
} }
// API 33+ (Android 13+) 使用 onAssociationCreated // API 33+ (Android 13+) 使用 onAssociationCreated
@RequiresApi(Build.VERSION_CODES.TIRAMISU) @RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onAssociationCreated(associationInfo: AssociationInfo) { override fun onAssociationCreated(associationInfo: AssociationInfo) {
FileLogger.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}") Log.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}")
// 保存关联ID // 保存关联ID
try { try {
val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress") val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress")
val mac = macAddressMethod.invoke(associationInfo) as? String val mac = macAddressMethod.invoke(associationInfo) as? String
mac?.let { savePairedMacAddress(it) } mac?.let { savePairedMacAddress(it) }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
} }
} }
override fun onFailure(error: CharSequence?) { override fun onFailure(error: CharSequence?) {
FileLogger.e(TAG, "设备关联失败: $error") Log.e(TAG, "设备关联失败: $error")
} }
}, },
null null
@ -257,7 +257,7 @@ class BleCompanionManager(private val context: Context) {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动设备关联失败: ${e.message}", e) Log.e(TAG, "启动设备关联失败: ${e.message}", e)
return false return false
} }
} }
@ -276,7 +276,7 @@ class BleCompanionManager(private val context: Context) {
return false return false
} }
FileLogger.i(TAG, "收到关联结果: resultCode=$resultCode") Log.i(TAG, "收到关联结果: resultCode=$resultCode")
if (resultCode == Activity.RESULT_OK && data != null) { if (resultCode == Activity.RESULT_OK && data != null) {
// 获取选中的蓝牙设备 // 获取选中的蓝牙设备
@ -295,7 +295,7 @@ class BleCompanionManager(private val context: Context) {
val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID" val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID"
data.getStringExtra(extraAssociationIdKey) data.getStringExtra(extraAssociationIdKey)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) Log.e(TAG, "获取关联ID失败: ${e.message}", e)
null null
} }
} else { } else {
@ -309,20 +309,20 @@ class BleCompanionManager(private val context: Context) {
// 验证MAC地址格式 // 验证MAC地址格式
if (!isValidMacAddress(deviceMac)) { if (!isValidMacAddress(deviceMac)) {
FileLogger.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac") Log.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac")
return false return false
} }
// 检查是否与预期的MAC地址匹配 // 检查是否与预期的MAC地址匹配
if (pendingMacAddress != null && pendingMacAddress != deviceMac) { if (pendingMacAddress != null && pendingMacAddress != deviceMac) {
FileLogger.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配") Log.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配")
// 如果要严格匹配MAC,可以在这里返回false // 如果要严格匹配MAC,可以在这里返回false
return false return false
} }
// 匹配成功后,保存新的MAC地址 // 匹配成功后,保存新的MAC地址
savePairedMacAddress(deviceMac) savePairedMacAddress(deviceMac)
FileLogger.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId") Log.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId")
// 清除临时MAC地址 // 清除临时MAC地址
pendingMacAddress = null pendingMacAddress = null
@ -330,10 +330,10 @@ class BleCompanionManager(private val context: Context) {
return true return true
} else { } else {
FileLogger.e(TAG, "无法获取关联的蓝牙设备信息") Log.e(TAG, "无法获取关联的蓝牙设备信息")
} }
} else { } else {
FileLogger.w(TAG, "设备关联被取消或失败: resultCode=$resultCode") Log.w(TAG, "设备关联被取消或失败: resultCode=$resultCode")
// 清除临时MAC地址 // 清除临时MAC地址
pendingMacAddress = null pendingMacAddress = null
} }
@ -350,7 +350,7 @@ class BleCompanionManager(private val context: Context) {
try { try {
val mac = getPairedMacAddress() val mac = getPairedMacAddress()
if (mac.isNullOrEmpty()) { if (mac.isNullOrEmpty()) {
FileLogger.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址") Log.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址")
return return
} }
@ -359,7 +359,7 @@ class BleCompanionManager(private val context: Context) {
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner == null) { if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器") Log.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器")
return return
} }
@ -391,9 +391,9 @@ class BleCompanionManager(private val context: Context) {
// 启动后台扫描 // 启动后台扫描
bluetoothLeScanner.startScan(listOf(filter), settings, pendingIntent) bluetoothLeScanner.startScan(listOf(filter), settings, pendingIntent)
FileLogger.i(TAG, "已注册后台扫描,目标设备MAC: $mac") Log.i(TAG, "已注册后台扫描,目标设备MAC: $mac")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e) Log.e(TAG, "注册后台扫描失败: ${e.message}", e)
} }
} }
@ -407,7 +407,7 @@ class BleCompanionManager(private val context: Context) {
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner == null) { if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器") Log.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器")
return return
} }
@ -428,9 +428,9 @@ class BleCompanionManager(private val context: Context) {
// 停止后台扫描 // 停止后台扫描
bluetoothLeScanner.stopScan(pendingIntent) bluetoothLeScanner.stopScan(pendingIntent)
FileLogger.i(TAG, "已取消后台扫描") Log.i(TAG, "已取消后台扫描")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "取消后台扫描失败: ${e.message}", e) Log.e(TAG, "取消后台扫描失败: ${e.message}", e)
} }
} }
@ -454,13 +454,13 @@ class BleCompanionManager(private val context: Context) {
} else { } else {
companionDeviceManager?.disassociate(id) companionDeviceManager?.disassociate(id)
} }
FileLogger.i(TAG, "已解除设备关联: $id") Log.i(TAG, "已解除设备关联: $id")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "解除设备关联失败: $id, ${e.message}", e) Log.e(TAG, "解除设备关联失败: $id, ${e.message}", e)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "清除所有关联失败: ${e.message}", e) Log.e(TAG, "清除所有关联失败: ${e.message}", e)
} }
} }

15
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt

@ -6,7 +6,7 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
/** /**
* BleCompanionService * BleCompanionService
@ -19,13 +19,12 @@ class BleCompanionService : CompanionDeviceService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FileLogger.init(applicationContext) Log.i(TAG, "BleCompanionService已创建")
FileLogger.i(TAG, "BleCompanionService已创建")
} }
override fun onDeviceAppeared(info: AssociationInfo) { override fun onDeviceAppeared(info: AssociationInfo) {
super.onDeviceAppeared(info) super.onDeviceAppeared(info)
FileLogger.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}") Log.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}")
// 当配对设备出现时,启动前台服务 // 当配对设备出现时,启动前台服务
try { try {
@ -49,21 +48,21 @@ class BleCompanionService : CompanionDeviceService() {
val mac = macAddressMethod.invoke(info) as? String val mac = macAddressMethod.invoke(info) as? String
mac?.let { putExtra("mac", it) } mac?.let { putExtra("mac", it) }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
} }
} }
// 启动前台服务 // 启动前台服务
startForegroundService(serviceIntent) startForegroundService(serviceIntent)
FileLogger.i(TAG, "已启动服务") Log.i(TAG, "已启动服务")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动服务失败: ${e.message}", e) Log.e(TAG, "启动服务失败: ${e.message}", e)
} }
} }
override fun onDeviceDisappeared(info: AssociationInfo) { override fun onDeviceDisappeared(info: AssociationInfo) {
super.onDeviceDisappeared(info) super.onDeviceDisappeared(info)
FileLogger.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}") Log.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}")
// 可以选择停止服务或执行其他清理操作 // 可以选择停止服务或执行其他清理操作
} }

282
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -8,7 +8,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.* import android.os.*
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -30,6 +30,10 @@ import com.jieli.jl_audio_decode.callback.OnStateCallback
import com.jieli.jl_audio_decode.opus.OpusManager import com.jieli.jl_audio_decode.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException import com.jieli.jl_audio_decode.exceptions.OpusException
import com.yunqiinnovation.ble_service.BleCompanionManager import com.yunqiinnovation.ble_service.BleCompanionManager
import com.yunqiinnovation.ble_service.OTAManager
import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure;
import com.jieli.jl_bt_ota.constant.ErrorCode;
import com.jieli.jl_bt_ota.constant.StateCode;
/** /**
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能 * BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能
@ -96,6 +100,8 @@ object BleService {
private var opusManager: OpusManager? = null private var opusManager: OpusManager? = null
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
// ota解码器实例
var otaManager: OTAManager? = null
// 初始化状态 // 初始化状态
private var isInitialized = false private var isInitialized = false
@ -111,6 +117,19 @@ object BleService {
try { try {
this.context = appContext.applicationContext this.context = appContext.applicationContext
otaManager = OTAManager(this.context).apply {
// 设置数据回调
setDataCallback(object : OTAManager.DataCallback {
override fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) {
data?.let {
Log.i("BleService", "收到数据:${it.size} 字节")
Log.i("BleService", "认证交互数据${it.contentToString()}")
// 移除次数限制,除非明确需要
otaManager?.onReceiveDeviceData(device, it)
}
}
})
}
// 初始化蓝牙管理器和适配器 // 初始化蓝牙管理器和适配器
bluetoothManager = bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
@ -120,21 +139,21 @@ object BleService {
// 初始化Handler // 初始化Handler
scanHandler = Handler(Looper.getMainLooper()) scanHandler = Handler(Looper.getMainLooper())
// 初始化OpusManager // 初始化OpusManager和OTAManager
try { try {
opusManager = OpusManager() opusManager = OpusManager()
startOpusStreamDecoding() startOpusStreamDecoding()
FileLogger.d(TAG, "OpusManager初始化成功") Log.d(TAG, "OpusManager初始化成功")
} catch (e: OpusException) { } catch (e: OpusException) {
FileLogger.e(TAG, "OpusManager初始化失败: ${e.message}", e) Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
// 根据需要决定是否因为Opus初始化失败而返回false // 根据需要决定是否因为Opus初始化失败而返回false
} }
isInitialized = true isInitialized = true
FileLogger.d(TAG, "BLE服务初始化成功") Log.d(TAG, "BLE服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "BLE服务初始化失败: ${e.message}", e) Log.e(TAG, "BLE服务初始化失败: ${e.message}", e)
return false return false
} }
} }
@ -147,7 +166,7 @@ object BleService {
fun addCallback(callback: Callback): Boolean { fun addCallback(callback: Callback): Boolean {
if (!callbacks.contains(callback)) { if (!callbacks.contains(callback)) {
callbacks.add(callback) callbacks.add(callback)
FileLogger.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}") Log.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}")
return true return true
} }
return false return false
@ -161,7 +180,7 @@ object BleService {
fun removeCallback(callback: Callback): Boolean { fun removeCallback(callback: Callback): Boolean {
val removed = callbacks.remove(callback) val removed = callbacks.remove(callback)
if (removed) { if (removed) {
FileLogger.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}") Log.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}")
} }
return removed return removed
} }
@ -171,7 +190,7 @@ object BleService {
*/ */
fun clearCallbacks() { fun clearCallbacks() {
callbacks.clear() callbacks.clear()
FileLogger.d(TAG, "已清除所有BLE回调") Log.d(TAG, "已清除所有BLE回调")
} }
// ====================================================================================================== // ======================================================================================================
@ -184,7 +203,7 @@ object BleService {
*/ */
fun startScan(timeout: Long = 15000): Boolean { fun startScan(timeout: Long = 15000): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "BLE服务未初始化") Log.e(TAG, "BLE服务未初始化")
return false return false
} }
@ -192,7 +211,7 @@ object BleService {
val scanner = bluetoothLeScanner ?: return false val scanner = bluetoothLeScanner ?: return false
if (isScanning) return false if (isScanning) return false
FileLogger.i(TAG, "开始主动扫描BLE设备...") Log.i(TAG, "开始主动扫描BLE设备...")
scanResults.clear() scanResults.clear()
val filter = ScanFilter.Builder() val filter = ScanFilter.Builder()
@ -213,13 +232,13 @@ object BleService {
// 设置扫描超时 // 设置扫描超时
scanHandler.postDelayed({ scanHandler.postDelayed({
FileLogger.d(TAG, "扫描超时,停止扫描") Log.d(TAG, "扫描超时,停止扫描")
stopScan() stopScan()
}, timeout) }, timeout)
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动扫描失败: ${e.message}", e) Log.e(TAG, "启动扫描失败: ${e.message}", e)
return false return false
} }
} }
@ -233,7 +252,7 @@ object BleService {
val scanner = bluetoothLeScanner ?: return false val scanner = bluetoothLeScanner ?: return false
if (!isScanning) return false if (!isScanning) return false
FileLogger.i(TAG, "停止主动扫描") Log.i(TAG, "停止主动扫描")
try { try {
scanner.stopScan(scanCallback) scanner.stopScan(scanCallback)
@ -243,7 +262,7 @@ object BleService {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止扫描失败: ${e.message}", e) Log.e(TAG, "停止扫描失败: ${e.message}", e)
return false return false
} }
} }
@ -277,12 +296,12 @@ object BleService {
val idx = scanResults.indexOfFirst { it.device.address == dev.address } val idx = scanResults.indexOfFirst { it.device.address == dev.address }
if (idx >= 0) scanResults[idx] = result else scanResults.add(result) if (idx >= 0) scanResults[idx] = result else scanResults.add(result)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "上报扫描结果异常: ${e.message}", e) Log.e(TAG, "上报扫描结果异常: ${e.message}", e)
} }
} }
override fun onScanFailed(errorCode: Int) { override fun onScanFailed(errorCode: Int) {
FileLogger.e(TAG, "扫描失败 error=$errorCode") Log.e(TAG, "扫描失败 error=$errorCode")
isScanning = false isScanning = false
} }
} }
@ -296,7 +315,7 @@ object BleService {
val serviceUuids = val serviceUuids =
scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无" scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无"
FileLogger.d( Log.d(
TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " + TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " +
"RSSI=${result.rssi}, 服务UUID=[$serviceUuids]" "RSSI=${result.rssi}, 服务UUID=[$serviceUuids]"
) )
@ -305,12 +324,12 @@ object BleService {
val scanData = scanRecord?.bytes val scanData = scanRecord?.bytes
if (scanData != null) { if (scanData != null) {
val hexString = scanData.joinToString("") { String.format("%02X", it) } val hexString = scanData.joinToString("") { String.format("%02X", it) }
FileLogger.d(TAG, "广播数据: $hexString") Log.d(TAG, "广播数据: $hexString")
// 解析服务数据 // 解析服务数据
scanRecord.serviceData?.forEach { (uuid, data) -> scanRecord.serviceData?.forEach { (uuid, data) ->
val dataHex = data.joinToString("") { String.format("%02X", it) } val dataHex = data.joinToString("") { String.format("%02X", it) }
FileLogger.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex") Log.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex")
} }
} }
} }
@ -326,32 +345,33 @@ object BleService {
try { try {
// 创建 BleCompanionManager 实例 // 创建 BleCompanionManager 实例
val companionManager = BleCompanionManager(context) val companionManager = BleCompanionManager(context)
// 获取已配对的 MAC 地址 // 获取已配对的 MAC 地址
val pairedMac = companionManager.getPairedMacAddress() val pairedMac = companionManager.getPairedMacAddress()
if (pairedMac.isNullOrEmpty()) { if (pairedMac.isNullOrEmpty()) {
FileLogger.w(TAG, "未找到已配对的设备 MAC 地址") Log.w(TAG, "未找到已配对的设备 MAC 地址")
return return
} }
FileLogger.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接") Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
// 调用现有的 connect 方法 // 调用现有的 connect 方法
connect(pairedMac) connect(pairedMac)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "自动连接已配对设备失败: ${e.message}", e) Log.e(TAG, "自动连接已配对设备失败: ${e.message}", e)
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
} }
} }
/** /**
* 连接到指定地址的BLE设备 * 连接到指定地址的BLE设备
* @param address 设备MAC地址 * @param address 设备MAC地址
*/ */
fun connect(address: String) { fun connect(address: String) {
if (connectionState.value == BleConst.STATE_CONNECTED) { if (connectionState.value == BleConst.STATE_CONNECTED) {
FileLogger.w(TAG, "已经连接中,忽略连接请求") Log.w(TAG, "已经连接中,忽略连接请求")
return return
} }
@ -365,6 +385,8 @@ object BleService {
disconnectGatt() disconnectGatt()
try { try {
// 创建新连接 // 创建新连接
val device = adapter.getRemoteDevice(address) val device = adapter.getRemoteDevice(address)
updateConnectionState(BleConst.STATE_CONNECTING) updateConnectionState(BleConst.STATE_CONNECTING)
@ -372,8 +394,10 @@ object BleService {
bluetoothGatt = device.connectGatt( bluetoothGatt = device.connectGatt(
context, false, gattCallback, BluetoothDevice.TRANSPORT_LE context, false, gattCallback, BluetoothDevice.TRANSPORT_LE
) )
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "连接设备失败: ${e.message}", e) Log.e(TAG, "连接设备失败: ${e.message}", e)
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
} }
} }
@ -395,7 +419,7 @@ object BleService {
gatt.disconnect() gatt.disconnect()
gatt.close() gatt.close()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "关闭GATT连接异常: ${e.message}", e) Log.e(TAG, "关闭GATT连接异常: ${e.message}", e)
} }
bluetoothGatt = null bluetoothGatt = null
notifyChar = null notifyChar = null
@ -404,6 +428,45 @@ object BleService {
} }
} }
fun startOTA() {
try {
otaManager?.startOTA(CustomUpdateCallback(context, null))
Log.i(TAG, "OTA 升级已启动")
} catch (e: IllegalStateException) {
Log.e(TAG, "OTA 启动失败: ${e.message}")
disconnectGatt()
}
}
// 配置OTA
fun configureOTA() {
try {
val firmwarePath =
context.getExternalFilesDir("downloads/ota_upgrades")?.path + "/1.ufw"
val file = File(firmwarePath)
if (!file.exists()) {
// throw IOException("Firmware file not found")
Log.i(TAG, "Firmware file not found")
}
val bluetoothOption = BluetoothOTAConfigure().apply {
priority = BluetoothOTAConfigure.PREFER_BLE
isUseReconnect = true
isUseAuthDevice = true
mtu = 512
isNeedChangeMtu = false // 确保MTU请求生效
isUseJLServer = false
firmwareFilePath = firmwarePath
}
otaManager?.configure(bluetoothOption) ?: run {
Log.e(TAG, "OTA Manager未初始化")
}
} catch (e: Exception) {
Log.e(TAG, "OTA配置失败: ${e.message}")
// 处理错误
}
}
/** /**
* 更新连接状态并通知回调 * 更新连接状态并通知回调
*/ */
@ -416,16 +479,37 @@ object BleService {
* GATT回调 * GATT回调
*/ */
private val gattCallback = object : BluetoothGattCallback() { private val gattCallback = object : BluetoothGattCallback() {
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "MTU 更新成功: $mtu")
configureOTA()
startOTA()
} else {
Log.e(TAG, "MTU 更新失败: status=$status")
}
}
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) { override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
when { when {
status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> { status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> {
updateConnectionState(BleConst.STATE_CONNECTED) updateConnectionState(BleConst.STATE_CONNECTED)
g.discoverServices() g.discoverServices()
Log.i(TAG, "连接成功,开始 MTU 协商")
otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_OK)
g.requestMtu(512) // 触发 MTU 修改流程
} }
newState == BluetoothProfile.STATE_DISCONNECTED -> { newState == BluetoothProfile.STATE_DISCONNECTED -> {
updateConnectionState(BleConst.STATE_DISCONNECTED) updateConnectionState(BleConst.STATE_DISCONNECTED)
disconnectGatt() disconnectGatt()
otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING)
g.close()
otaManager?.release();
} }
else -> { else -> {
@ -452,7 +536,7 @@ object BleService {
audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID)
if (notifyChar == null || writeChar == null) { if (notifyChar == null || writeChar == null) {
FileLogger.e(TAG, "未找到主服务所需特征") Log.e(TAG, "未找到主服务所需特征")
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
return return
} }
@ -462,9 +546,9 @@ object BleService {
// 设置音频服务的通知(如果存在) // 设置音频服务的通知(如果存在)
if (audioChar != null) { if (audioChar != null) {
setupNotifications(g, audioChar) setupNotifications(g, audioChar)
FileLogger.i(TAG, "音频服务特征找到并设置通知") Log.i(TAG, "音频服务特征找到并设置通知")
} else { } else {
FileLogger.w(TAG, "音频服务特征未找到") Log.w(TAG, "音频服务特征未找到")
} }
// 设置主服务的通知 // 设置主服务的通知
setupNotifications(g, notifyChar) setupNotifications(g, notifyChar)
@ -480,10 +564,12 @@ object BleService {
when (c.uuid) { when (c.uuid) {
// 音频特征数据 // 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID -> { BleConst.RECEIVE_AUDIO_CHAR_UUID -> {
// Log.i(TAG, "收到音频特征数据")
processAudioData(data) processAudioData(data)
} }
// 通知特征数据(命令和控制) // 通知特征数据(命令和控制)
BleConst.NOTIFY_CHAR_UUID -> { BleConst.NOTIFY_CHAR_UUID -> {
// Log.i(TAG, "收到通知特征数据(命令和控制)")
// 判断数据类型 // 判断数据类型
when { when {
// 设备响应 (0xBB开头) // 设备响应 (0xBB开头)
@ -496,7 +582,7 @@ object BleService {
} }
// 其他未知数据 // 其他未知数据
else -> { else -> {
FileLogger.d( Log.d(
TAG, TAG,
"收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}" "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}"
) )
@ -505,7 +591,7 @@ object BleService {
} }
// 其他未知特征 // 其他未知特征
else -> { else -> {
FileLogger.d( Log.d(
TAG, TAG,
"收到未知特征数据: ${c.uuid}, 数据=${ "收到未知特征数据: ${c.uuid}, 数据=${
data.joinToString(", ") { data.joinToString(", ") {
@ -520,7 +606,7 @@ object BleService {
override fun onCharacteristicWrite( override fun onCharacteristicWrite(
g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int
) { ) {
FileLogger.i( Log.i(
TAG, TAG,
"写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}" "写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}"
) )
@ -530,7 +616,7 @@ object BleService {
g: BluetoothGatt, d: BluetoothGattDescriptor, status: Int g: BluetoothGatt, d: BluetoothGattDescriptor, status: Int
) { ) {
if (status == BluetoothGatt.GATT_SUCCESS) if (status == BluetoothGatt.GATT_SUCCESS)
FileLogger.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}") Log.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}")
} }
} }
@ -539,7 +625,7 @@ object BleService {
*/ */
private fun setupNotifications(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) { private fun setupNotifications(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) {
if (characteristic == null || characteristic.properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) { if (characteristic == null || characteristic.properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) {
FileLogger.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}") Log.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}")
return return
} }
@ -558,10 +644,10 @@ object BleService {
if (opusManager?.isDecodeStream == true) { if (opusManager?.isDecodeStream == true) {
opusManager?.writeAudioStream(data) opusManager?.writeAudioStream(data)
} else { } else {
// FileLogger.d(TAG, "Opus解码流未启动,忽略音频数据") // Log.d(TAG, "Opus解码流未启动,忽略音频数据")
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理音频数据异常: ${e.message}", e) Log.e(TAG, "处理音频数据异常: ${e.message}", e)
} }
} }
@ -570,12 +656,12 @@ object BleService {
* 处理设备响应数据 - 帧头为0xBB * 处理设备响应数据 - 帧头为0xBB
*/ */
private fun processDeviceResponse(data: ByteArray) { private fun processDeviceResponse(data: ByteArray) {
FileLogger.d( Log.d(
TAG, TAG,
"收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" "收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}"
) )
if (data.size < 3) { if (data.size < 3) {
FileLogger.e(TAG, "设备响应数据格式错误:数据长度过短") Log.e(TAG, "设备响应数据格式错误:数据长度过短")
return return
} }
@ -584,7 +670,7 @@ object BleService {
// 判断命令失败响应(长度为0) // 判断命令失败响应(长度为0)
if (data.size == 4 && data[2].toInt() and 0xFF == 0) { if (data.size == 4 && data[2].toInt() and 0xFF == 0) {
FileLogger.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败")
// 创建失败结果Map // 创建失败结果Map
val resultMap = mapOf( val resultMap = mapOf(
"success" to false, "success" to false,
@ -600,7 +686,7 @@ object BleService {
// 检查数据长度 // 检查数据长度
if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC
FileLogger.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return return
} }
@ -610,7 +696,7 @@ object BleService {
val receivedCrc = data[data.size - 1].toInt() and 0xFF val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) { if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
FileLogger.e( Log.e(
TAG, TAG,
"设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
) )
@ -623,7 +709,7 @@ object BleService {
System.arraycopy(data, 3, responseData, 0, length) System.arraycopy(data, 3, responseData, 0, length)
} }
FileLogger.d( Log.d(
TAG, TAG,
"收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${
responseData.joinToString(", ") { responseData.joinToString(", ") {
@ -646,18 +732,22 @@ object BleService {
if (responseData.isNotEmpty()) { if (responseData.isNotEmpty()) {
// 转换为字符串前,确认是否是有效的ASCII字符 // 转换为字符串前,确认是否是有效的ASCII字符
val version = val version =
responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } responseData
.map { it.toInt() and 0xFF } // 转为无符号整数 (0-255)
.dropWhile { it == 0xFF } // 仅丢弃开头的连续 255
.joinToString(".") { it.toString() }
resultMap["version"] = version resultMap["version"] = version
FileLogger.i(TAG, "接收到目标版本号: $version") Log.i(TAG, "接收到目标版本号: $version")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
} }
BleConst.CMD_GET_PRODUCT_ID -> { BleConst.CMD_GET_PRODUCT_ID -> {
val resultMap = mutableMapOf<String, Any>( val resultMap = mutableMapOf<String, Any>(
"success" to true, "success" to true,
"command" to "productInfo", "command" to "productId",
"rawData" to responseData "rawData" to responseData
) )
@ -666,7 +756,7 @@ object BleService {
val productId = val productId =
responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } responseData.joinToString(".") { (it.toInt() and 0xFF).toString() }
resultMap["productId"] = productId resultMap["productId"] = productId
FileLogger.i(TAG, "接收到产品ID: $productId") Log.i(TAG, "接收到产品ID: $productId")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -682,7 +772,7 @@ object BleService {
if (responseData.isNotEmpty()) { if (responseData.isNotEmpty()) {
val colorId = (responseData[0].toInt() and 0xFF).toString() val colorId = (responseData[0].toInt() and 0xFF).toString()
resultMap["colorId"] = colorId resultMap["colorId"] = colorId
FileLogger.i(TAG, "接收到耳机颜色ID: $colorId") Log.i(TAG, "接收到耳机颜色ID: $colorId")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -719,7 +809,7 @@ object BleService {
resultMap["caseCharging"] = caseCharging resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
FileLogger.i( Log.i(
TAG, "接收到电量信息: 左耳=${leftEarBattery}% " + TAG, "接收到电量信息: 左耳=${leftEarBattery}% " +
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
@ -763,7 +853,7 @@ object BleService {
resultMap["statusDesc"] = statusDesc resultMap["statusDesc"] = statusDesc
resultMap["channelDesc"] = channelDesc resultMap["channelDesc"] = channelDesc
FileLogger.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") Log.i(TAG, "编解码控制响应: $statusDesc, $channelDesc")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -777,7 +867,7 @@ object BleService {
"rawData" to responseData, "rawData" to responseData,
"isUnknown" to true "isUnknown" to true
) )
FileLogger.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}") Log.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}")
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
} }
} }
@ -787,12 +877,12 @@ object BleService {
* 处理设备主动上报数据 - 帧头为0xCC * 处理设备主动上报数据 - 帧头为0xCC
*/ */
private fun processDeviceNotification(data: ByteArray) { private fun processDeviceNotification(data: ByteArray) {
FileLogger.d( Log.d(
TAG, TAG,
"收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" "收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}"
) )
if (data.size < 3) { if (data.size < 3) {
FileLogger.e(TAG, "设备主动上报数据格式错误:数据长度过短") Log.e(TAG, "设备主动上报数据格式错误:数据长度过短")
return return
} }
@ -802,7 +892,7 @@ object BleService {
// 检查数据长度 // 检查数据长度
if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC
FileLogger.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") Log.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return return
} }
@ -812,7 +902,7 @@ object BleService {
val receivedCrc = data[data.size - 1].toInt() and 0xFF val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) { if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
FileLogger.e( Log.e(
TAG, TAG,
"设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" "设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
) )
@ -825,7 +915,7 @@ object BleService {
System.arraycopy(data, 3, notifyData, 0, length) System.arraycopy(data, 3, notifyData, 0, length)
} }
FileLogger.d( Log.d(
TAG, TAG,
"收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${ "收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${
notifyData.joinToString(", ") { notifyData.joinToString(", ") {
@ -870,7 +960,7 @@ object BleService {
resultMap["caseCharging"] = caseCharging resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
FileLogger.i( Log.i(
TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " + TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " +
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
@ -882,7 +972,7 @@ object BleService {
} }
BleConst.CMD_WAKE_UP -> { BleConst.CMD_WAKE_UP -> {
FileLogger.i(TAG, "收到唤醒指令") Log.i(TAG, "收到唤醒指令")
// 接收到唤醒信号,打开编码器 (设备侧) // 接收到唤醒信号,打开编码器 (设备侧)
openEncoder() openEncoder()
notifyWakeupSignalReceived() notifyWakeupSignalReceived()
@ -898,7 +988,7 @@ object BleService {
"isUnknown" to true, "isUnknown" to true,
"isNotification" to true // 标记为主动上报 "isNotification" to true // 标记为主动上报
) )
FileLogger.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}") Log.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}")
notifyDeviceInfoReceived(infoType, resultMap) notifyDeviceInfoReceived(infoType, resultMap)
} }
} }
@ -915,13 +1005,13 @@ object BleService {
packetSize: Int = 40 // 确认设备端Opus编码帧长 packetSize: Int = 40 // 确认设备端Opus编码帧长
): Boolean { ): Boolean {
if (opusManager == null) { if (opusManager == null) {
FileLogger.e(TAG, "OpusManager未初始化,无法开始解码") Log.e(TAG, "OpusManager未初始化,无法开始解码")
return false return false
} }
// 如果已经在解码流,先停止 // 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) { if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream() opusManager?.stopDecodeStream()
FileLogger.d(TAG, "已停止正在进行的Opus解码流") Log.d(TAG, "已停止正在进行的Opus解码流")
} }
val option = OpusOption() val option = OpusOption()
@ -930,29 +1020,29 @@ object BleService {
.setSampleRate(sampleRate) .setSampleRate(sampleRate)
.setPacketSize(packetSize) .setPacketSize(packetSize)
FileLogger.d(TAG, "准备开始Opus数据流解码, 参数: $option") Log.d(TAG, "准备开始Opus数据流解码, 参数: $option")
var streamStartedSuccessfully = false var streamStartedSuccessfully = false
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) { override fun onDecodeStream(data: ByteArray?) {
if (data != null) { if (data != null) {
// FileLogger.d(TAG, "Opus解码数据: ${data.size} bytes") // Log.d(TAG, "Opus解码数据: ${data.size} bytes")
notifyAudioDataReceived(data) notifyAudioDataReceived(data)
} }
} }
override fun onStart() { override fun onStart() {
streamStartedSuccessfully = true streamStartedSuccessfully = true
FileLogger.i(TAG, "Opus数据流解码已开始") Log.i(TAG, "Opus数据流解码已开始")
// 可以通过回调通知上层解码已开始 // 可以通过回调通知上层解码已开始
} }
override fun onComplete(outPath: String?) { override fun onComplete(outPath: String?) {
FileLogger.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)") Log.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)")
} }
override fun onError(code: Int, message: String?) { override fun onError(code: Int, message: String?) {
FileLogger.e(TAG, "Opus数据流解码错误: [$code] $message") Log.e(TAG, "Opus数据流解码错误: [$code] $message")
// 可以通过回调通知上层解码错误 // 可以通过回调通知上层解码错误
} }
}) })
@ -965,10 +1055,10 @@ object BleService {
private fun stopOpusStreamDecoding(): Boolean { private fun stopOpusStreamDecoding(): Boolean {
if (opusManager?.isDecodeStream == true) { if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream() opusManager?.stopDecodeStream()
FileLogger.i(TAG, "已停止Opus数据流解码") Log.i(TAG, "已停止Opus数据流解码")
return true return true
} }
FileLogger.d(TAG, "Opus数据流未在解码或OpusManager未初始化") Log.d(TAG, "Opus数据流未在解码或OpusManager未初始化")
return false return false
} }
@ -980,7 +1070,7 @@ object BleService {
* 获取耳机版本号 * 获取耳机版本号
*/ */
fun getDeviceVersion(): Boolean { fun getDeviceVersion(): Boolean {
FileLogger.i(TAG, "获取耳机版本号...") Log.i(TAG, "获取耳机版本号...")
return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -988,7 +1078,7 @@ object BleService {
* 获取产品ID * 获取产品ID
*/ */
fun getProductId(): Boolean { fun getProductId(): Boolean {
FileLogger.i(TAG, "获取耳机产品ID...") Log.i(TAG, "获取耳机产品ID...")
return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -996,7 +1086,7 @@ object BleService {
* 获取耳机颜色ID * 获取耳机颜色ID
*/ */
fun getColorId(): Boolean { fun getColorId(): Boolean {
FileLogger.i(TAG, "获取耳机颜色ID...") Log.i(TAG, "获取耳机颜色ID...")
return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -1004,7 +1094,7 @@ object BleService {
* 获取耳机电量信息 * 获取耳机电量信息
*/ */
fun getBatteryInfo(): Boolean { fun getBatteryInfo(): Boolean {
FileLogger.i(TAG, "获取耳机电量信息...") Log.i(TAG, "获取耳机电量信息...")
return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -1012,7 +1102,7 @@ object BleService {
* 控制编解码 - 关闭编解码 * 控制编解码 - 关闭编解码
*/ */
fun closeCodec(): Boolean { fun closeCodec(): Boolean {
FileLogger.i(TAG, "关闭编解码...") Log.i(TAG, "关闭编解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), BleConst.CMD_CONTROL_CODEC.toByte(),
@ -1028,7 +1118,7 @@ object BleService {
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openDecoder(): Boolean { fun openDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码 (设备侧)...")
// val opusStarted = startOpusStreamDecoding( // val opusStarted = startOpusStreamDecoding(
// hasHeader = false, // BLE传输的Opus数据通常无文件头 // hasHeader = false, // BLE传输的Opus数据通常无文件头
@ -1041,7 +1131,7 @@ object BleService {
// ) // )
FileLogger.i(TAG, "打开解码...") Log.i(TAG, "打开解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_DECODE_ON.toByte(), BleConst.CODEC_CONTROL_DECODE_ON.toByte(),
@ -1055,9 +1145,9 @@ object BleService {
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openA2DPDecoder(): Boolean { fun openA2DPDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码 (设备侧)...")
// FileLogger.i(TAG, "打开解码...") // Log.i(TAG, "打开解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(),
@ -1071,7 +1161,7 @@ object BleService {
* 控制编解码 - A2DP播放 * 控制编解码 - A2DP播放
*/ */
fun playA2DP(): Boolean { fun playA2DP(): Boolean {
FileLogger.i(TAG, "切换至A2DP播放模式...") Log.i(TAG, "切换至A2DP播放模式...")
return sendCommand(BleConst.CMD_PLAY_A2DP.toByte()) return sendCommand(BleConst.CMD_PLAY_A2DP.toByte())
} }
@ -1079,7 +1169,7 @@ object BleService {
* 控制编解码 - 打开编码 * 控制编解码 - 打开编码
*/ */
fun openEncoder(): Boolean { fun openEncoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码 (设备侧)...")
// 目前仅发送命令通知设备开始编码。 // 目前仅发送命令通知设备开始编码。
@ -1099,11 +1189,11 @@ object BleService {
*/ */
fun setChannelMode(channelMode: Int): Boolean { fun setChannelMode(channelMode: Int): Boolean {
when (channelMode) { when (channelMode) {
BleConst.AUDIO_CHANNEL_LEFT -> FileLogger.i(TAG, "设置左声道模式...") BleConst.AUDIO_CHANNEL_LEFT -> Log.i(TAG, "设置左声道模式...")
BleConst.AUDIO_CHANNEL_RIGHT -> FileLogger.i(TAG, "设置右声道模式...") BleConst.AUDIO_CHANNEL_RIGHT -> Log.i(TAG, "设置右声道模式...")
BleConst.AUDIO_CHANNEL_STEREO -> FileLogger.i(TAG, "设置立体声模式...") BleConst.AUDIO_CHANNEL_STEREO -> Log.i(TAG, "设置立体声模式...")
else -> { else -> {
FileLogger.e(TAG, "无效的声道模式: $channelMode") Log.e(TAG, "无效的声道模式: $channelMode")
return false return false
} }
} }
@ -1121,7 +1211,7 @@ object BleService {
* 音量增大 * 音量增大
*/ */
fun volumeUp(): Boolean { fun volumeUp(): Boolean {
FileLogger.i(TAG, "设备音量增加...") Log.i(TAG, "设备音量增加...")
return sendCommand(BleConst.CMD_VOLUME_UP.toByte()) return sendCommand(BleConst.CMD_VOLUME_UP.toByte())
} }
@ -1129,7 +1219,7 @@ object BleService {
* 音量减小 * 音量减小
*/ */
fun volumeDown(): Boolean { fun volumeDown(): Boolean {
FileLogger.i(TAG, "设备音量减小...") Log.i(TAG, "设备音量减小...")
return sendCommand(BleConst.CMD_VOLUME_DOWN.toByte()) return sendCommand(BleConst.CMD_VOLUME_DOWN.toByte())
} }
@ -1183,7 +1273,7 @@ object BleService {
it.release() it.release()
} }
opusManager = null opusManager = null
FileLogger.d(TAG, "OpusManager已释放") Log.d(TAG, "OpusManager已释放")
isInitialized = false // 标记为未初始化 isInitialized = false // 标记为未初始化
} }
@ -1199,7 +1289,7 @@ object BleService {
*/ */
private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean {
if (!checkConn()) { if (!checkConn()) {
FileLogger.e(TAG, "发送命令失败: 设备未连接") Log.e(TAG, "发送命令失败: 设备未连接")
return false return false
} }
@ -1230,7 +1320,7 @@ object BleService {
cmdPacket[3 + cmdLength] = crc cmdPacket[3 + cmdLength] = crc
// 记录命令详情 // 记录命令详情
FileLogger.d( Log.d(
TAG, TAG,
"发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " + "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " +
"数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " +
@ -1241,7 +1331,7 @@ object BleService {
writeChar!!.value = cmdPacket writeChar!!.value = cmdPacket
return bluetoothGatt?.writeCharacteristic(writeChar) ?: false return bluetoothGatt?.writeCharacteristic(writeChar) ?: false
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "发送命令异常: ${e.message}", e) Log.e(TAG, "发送命令异常: ${e.message}", e)
return false return false
} }
} }
@ -1274,7 +1364,7 @@ object BleService {
try { try {
callback.onScanResult(devices) callback.onScanResult(devices)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发扫描结果回调异常", e) Log.e(TAG, "分发扫描结果回调异常", e)
} }
} }
} }
@ -1287,7 +1377,7 @@ object BleService {
try { try {
callback.onConnectionStateChanged(state) callback.onConnectionStateChanged(state)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发连接状态回调异常", e) Log.e(TAG, "分发连接状态回调异常", e)
} }
} }
} }
@ -1300,7 +1390,7 @@ object BleService {
try { try {
callback.onAudioDataReceived(data) callback.onAudioDataReceived(data)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发音频数据回调异常", e) Log.e(TAG, "分发音频数据回调异常", e)
} }
} }
} }
@ -1313,7 +1403,7 @@ object BleService {
try { try {
callback.onWakeupSignalReceived() callback.onWakeupSignalReceived()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发唤醒信号回调异常", e) Log.e(TAG, "分发唤醒信号回调异常", e)
} }
} }
} }
@ -1326,7 +1416,7 @@ object BleService {
try { try {
callback.onDeviceInfoReceived(infoType, infoData) callback.onDeviceInfoReceived(infoType, infoData)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发设备信息回调异常", e) Log.e(TAG, "分发设备信息回调异常", e)
} }
} }
} }

11
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt

@ -15,7 +15,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.PluginRegistry
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
/** /**
* BleServicePlugin * BleServicePlugin
@ -52,8 +52,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
private var activity: android.app.Activity? = null private var activity: android.app.Activity? = null
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
FileLogger.init(flutterPluginBinding.applicationContext) Log.d(TAG, "BleServicePlugin onAttachedToEngine")
FileLogger.d(TAG, "BleServicePlugin onAttachedToEngine")
// 设置方法通道 // 设置方法通道
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.ble_service") methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.ble_service")
@ -150,14 +149,14 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
try { try {
eventSink.success(data) eventSink.success(data)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "$errorPrefix: ${e.message}", e) Log.e(TAG, "$errorPrefix: ${e.message}", e)
} }
} else { } else {
mainHandler.post { mainHandler.post {
try { try {
eventSink.success(data) eventSink.success(data)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "$errorPrefix: ${e.message}", e) Log.e(TAG, "$errorPrefix: ${e.message}", e)
} }
} }
} }
@ -175,7 +174,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
} }
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
FileLogger.d(TAG, "Method call: ${call.method}") Log.d(TAG, "Method call: ${call.method}")
when (call.method) { when (call.method) {
"isConnected" -> { "isConnected" -> {

13
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt

@ -5,7 +5,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
/** /**
* BleWakeReceiver * BleWakeReceiver
@ -15,8 +15,7 @@ class BleWakeReceiver : BroadcastReceiver() {
private val TAG = "BleWakeReceiver" private val TAG = "BleWakeReceiver"
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
FileLogger.init(context) Log.i(TAG, "收到BLE扫描广播: ${intent.action}")
FileLogger.i(TAG, "收到BLE扫描广播: ${intent.action}")
if (intent.action == BleConst.ACTION_SCAN_RESULT) { if (intent.action == BleConst.ACTION_SCAN_RESULT) {
// 从扫描结果中提取MAC地址 // 从扫描结果中提取MAC地址
@ -44,7 +43,7 @@ class BleWakeReceiver : BroadcastReceiver() {
val serviceClass = try { val serviceClass = try {
Class.forName("com.yunqiinnovation.agent_service.BleMediaService") Class.forName("com.yunqiinnovation.agent_service.BleMediaService")
} catch (e: ClassNotFoundException) { } catch (e: ClassNotFoundException) {
FileLogger.i(TAG, "BleMediaService不存在") Log.i(TAG, "BleMediaService不存在")
return return
} }
@ -57,13 +56,13 @@ class BleWakeReceiver : BroadcastReceiver() {
// 启动前台服务 // 启动前台服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent) context.startForegroundService(serviceIntent)
FileLogger.i(TAG, "已启动前台服务: ${serviceClass.name}") Log.i(TAG, "已启动前台服务: ${serviceClass.name}")
} else { } else {
context.startService(serviceIntent) context.startService(serviceIntent)
FileLogger.i(TAG, "已启动服务: ${serviceClass.name}") Log.i(TAG, "已启动服务: ${serviceClass.name}")
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动BLE服务失败: ${e.message}", e) Log.e(TAG, "启动BLE服务失败: ${e.message}", e)
} }
} }
} }

116
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt

@ -0,0 +1,116 @@
package com.yunqiinnovation.ble_service
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothProfile
import android.content.Context
import com.jieli.jl_bt_ota.constant.ErrorCode;
import com.jieli.jl_bt_ota.constant.StateCode;
import com.jieli.jl_bt_ota.impl.BluetoothOTAManager;
import com.jieli.jl_bt_ota.interfaces.BtEventCallback;
import com.jieli.jl_bt_ota.interfaces.IActionCallback;
import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback;
import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure;
import com.jieli.jl_bt_ota.model.base.BaseError;
import com.jieli.jl_bt_ota.model.response.TargetInfoResponse;
import android.util.Log
import java.util.*
class CustomUpdateCallback(
val context: Context,
val device: BluetoothDevice?
) : IUpgradeCallback {
companion object {
private const val TAG = "JlOtaPlugin"
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota"
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events"
}
override fun onStartOTA() {
Log.d(TAG, "onStartOTA开始升级")
// viewModel.otaStateMLD.value = OTAStart(device)
}
override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) {
Log.d(TAG, "onNeedReconnect需要重新链接")
}
override fun onProgress(type: Int, progress: Float) {
Log.d(TAG, "onProgress正在进展")
//viewModel.otaStateMLD.value = OTAWorking(device, type, progress)
}
override fun onStopOTA() {
Log.d(TAG, "onStartOTA停止升级")
}
override fun onCancelOTA() {
Log.d(TAG, "onCancelOTA取消升级")
}
override fun onError(error: BaseError?) {
val errorMessage = error?.message ?: "Unknown error"
Log.e(TAG, "升级失败: ${error?.message ?: "未知错误"}")
// 如果需要打印完整错误对象信息(假设BaseError有重写toString())
Log.d(TAG, "完整错误对象: ${error ?: "null"}")
}
}
class OTAManager(context: Context) : BluetoothOTAManager(context) {
// 定义回调接口
interface DataCallback {
fun onDataReceived(device: BluetoothDevice?, data: ByteArray?)
}
// 保存回调的实例
private var dataCallback: DataCallback? = null
// 设置回调的方法
fun setDataCallback(callback: DataCallback) {
this.dataCallback = callback
}
override fun getConnectedDevice(): BluetoothDevice? {
// 这里可以添加空实现的注释说明
Log.i(TAG, "ota数据发送getConnectedDevice")
return null
}
override fun getConnectedBluetoothGatt(): BluetoothGatt? {
// 这里可以添加空实现的注释说明
Log.i(TAG, "ota数据发送getConnectedBluetoothGatt")
return null
}
override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) {
// 这里可以添加空实现的注释说明
Log.i(TAG, "ota数据发送connectBluetoothDevice")
}
override fun disconnectBluetoothDevice(bluetoothDevice: BluetoothDevice?) {
// 这里可以添加空实现的注释说明
Log.i(TAG, "ota数据发送disconnectBluetoothDevice")
}
/** 数据发送方法(协议无关) */
override fun sendDataToDevice(
bluetoothDevice: BluetoothDevice?,
bytes: ByteArray?
): Boolean {
Log.i(TAG, "ota数据发送${bluetoothDevice},${bytes?.size}")
// 触发回调,将数据回传给调用者
dataCallback?.onDataReceived(bluetoothDevice, bytes)
// context.onReceiveDeviceData(device, data);
return true
}
}

28
local_plugins/bytedance_speech/android/build.gradle.kts

@ -3,11 +3,28 @@ version = "1.0.0"
plugins { plugins {
id("com.android.library") id("com.android.library")
id("org.jetbrains.kotlin.android") kotlin("android")
}
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.1.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
}
}
repositories {
google()
mavenCentral()
} }
android { android {
namespace = "com.yunqiinnovation.ble_service" namespace = "com.deep_voice.bytedance_speech"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
@ -22,15 +39,18 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
languageVersion = "1.7"
apiVersion = "1.7"
} }
} }
dependencies { dependencies {
implementation(kotlin("stdlib-jdk7"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation("com.google.code.gson:gson:2.10.1") implementation("com.google.code.gson:gson:2.10.1")
implementation(project(":speech"))
} }

2
local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.deep_voice.bytedance_speech"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />

392
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt

@ -1,6 +1,7 @@
package com.deep_voice.bytedance_speech package com.deep_voice.bytedance_speech
import android.content.Context import android.content.Context
import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonObject import com.google.gson.JsonObject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -13,6 +14,7 @@ import okhttp3.WebSocketListener
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level import okhttp3.logging.HttpLoggingInterceptor.Level
import okio.ByteString import okio.ByteString
import org.json.JSONObject
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -31,6 +33,7 @@ class BytedanceASR(
private val statusCallback: (String) -> Unit private val statusCallback: (String) -> Unit
) { ) {
companion object { companion object {
private const val TAG = "BytedanceASR"
// 协议常量 // 协议常量
private const val PROTOCOL_VERSION = 0x01 private const val PROTOCOL_VERSION = 0x01
private const val DEFAULT_HEADER_SIZE = 0x01 private const val DEFAULT_HEADER_SIZE = 0x01
@ -80,7 +83,7 @@ class BytedanceASR(
private var supportedLanguages: List<String> = listOf("zh-CN") private var supportedLanguages: List<String> = listOf("zh-CN")
// 状态控制 // 状态控制
private var isRunning = false private var isRecognizing = false
private var isLastPacket = false private var isLastPacket = false
private var isContinuousMode = false private var isContinuousMode = false
private var isInitialized = false private var isInitialized = false
@ -97,10 +100,10 @@ class BytedanceASR(
} }
client = OkHttpClient.Builder() client = OkHttpClient.Builder()
.pingInterval(50, TimeUnit.SECONDS) .pingInterval(30, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor) .addInterceptor(loggingInterceptor)
.readTimeout(100, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(100, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS)
.build() .build()
} }
@ -146,7 +149,7 @@ class BytedanceASR(
return@withContext false return@withContext false
} }
if (isRunning) { if (isRecognizing) {
errorCallback("BUSY", "语音识别正在进行中") errorCallback("BUSY", "语音识别正在进行中")
return@withContext false return@withContext false
} }
@ -163,12 +166,12 @@ class BytedanceASR(
// 等待连接建立 // 等待连接建立
var attempts = 0 var attempts = 0
while (!isRunning && attempts < 10) { while (!isRecognizing && attempts < 10) {
delay(300) delay(300)
attempts++ attempts++
} }
if (!isRunning) { if (!isRecognizing) {
errorCallback("CONNECTION_ERROR", "无法建立连接") errorCallback("CONNECTION_ERROR", "无法建立连接")
return@withContext false return@withContext false
} }
@ -197,7 +200,7 @@ class BytedanceASR(
return@withContext false return@withContext false
} }
if (isRunning) { if (isRecognizing) {
errorCallback("BUSY", "语音识别正在进行中") errorCallback("BUSY", "语音识别正在进行中")
return@withContext false return@withContext false
} }
@ -223,7 +226,7 @@ class BytedanceASR(
*/ */
suspend fun stopContinuousRecognition(): Boolean = withContext(Dispatchers.IO) { suspend fun stopContinuousRecognition(): Boolean = withContext(Dispatchers.IO) {
try { try {
if (!isRunning || !isContinuousMode) { if (!isRecognizing || !isContinuousMode) {
return@withContext true // 已经停止或非连续模式 return@withContext true // 已经停止或非连续模式
} }
@ -242,7 +245,7 @@ class BytedanceASR(
*/ */
suspend fun pushAudioData(audioData: ByteArray): Boolean = withContext(Dispatchers.IO) { suspend fun pushAudioData(audioData: ByteArray): Boolean = withContext(Dispatchers.IO) {
try { try {
if (!isRunning) { if (!isRecognizing) {
errorCallback("NOT_RUNNING", "语音识别未启动") errorCallback("NOT_RUNNING", "语音识别未启动")
return@withContext false return@withContext false
} }
@ -277,10 +280,10 @@ class BytedanceASR(
/** /**
* 开始语音识别 * 开始语音识别
*/ */
private suspend fun start() = withContext(Dispatchers.IO) { suspend fun start() = withContext(Dispatchers.IO) {
if (isRunning) return@withContext if (isRecognizing) return@withContext
isRunning = true isRecognizing = true
isLastPacket = false isLastPacket = false
seq = 1 seq = 1
@ -303,114 +306,81 @@ class BytedanceASR(
webSocket = client.newWebSocket(request, object : WebSocketListener() { webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) { override fun onOpen(webSocket: WebSocket, response: Response) {
val logId = response.header("X-Tt-Logid") val logId = response.header("X-Tt-Logid")
println("ASR WebSocket连接成功, X-Tt-Logid: $logId") Log.d(TAG, "ASR WebSocket连接成功, X-Tt-Logid: $logId")
// 发送初始配置请求 // 发送初始配置请求
sendInitialRequest(webSocket) sendInitialRequest(webSocket)
} }
override fun onMessage(webSocket: WebSocket, bytes: ByteString) { override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "收到文本消息: $text")
try { try {
val data = bytes.toByteArray() val json = JSONObject(text)
val response = parseResponse(data) // 解析结果
resultCallback(json.optString("text", ""), false)
// 根据消息类型处理响应
when (response.messageType) {
FULL_SERVER_RESPONSE -> {
// 解析识别结果
val isFinal = response.messageTypeSpecificFlags == NEG_WITH_SEQUENCE
val resultText = if (response.payload.isNotEmpty()) {
val decompressedData = if (response.compression == GZIP) {
gzipDecompress(response.payload)
} else {
response.payload
}
val jsonStr = String(decompressedData)
extractTextFromJson(jsonStr)
} else {
""
}
if (resultText.isNotEmpty()) {
// 处理单次识别模式
if (!isContinuousMode) {
onceResultBuilder.append(resultText)
}
// 回调识别结果
resultCallback(resultText, isFinal)
}
// 如果是最后一个包,关闭连接
if (isFinal) {
// 单次识别完成回调
if (!isContinuousMode && singleRecognitionCallback != null) {
val finalResult = onceResultBuilder.toString()
singleRecognitionCallback?.invoke(finalResult)
onceResultBuilder.clear()
singleRecognitionCallback = null
}
if (!isContinuousMode) {
isRunning = false
updateStatus(STATUS_STOPPED)
webSocket.close(1000, "Finished")
}
}
}
SERVER_ERROR_RESPONSE -> {
val errorMessage = String(response.payload)
errorCallback(response.sequence.toString(), errorMessage)
isRunning = false
updateStatus(STATUS_ERROR)
webSocket.close(1000, "Error")
}
}
} catch (e: Exception) { } catch (e: Exception) {
errorCallback("PARSE_ERROR", e.message ?: "解析响应失败") Log.e(TAG, "解析消息失败", e)
updateStatus(STATUS_ERROR)
} }
} }
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
isRunning = false // Log.d(TAG, "收到二进制消息")
errorCallback("CONNECTION_ERROR", t.message ?: "连接失败") // 在实际实现中解析二进制消息
updateStatus(STATUS_ERROR) }
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "连接关闭中: $code, $reason")
isRecognizing = false
} }
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
isRunning = false Log.d(TAG, "连接已关闭: $code, $reason")
updateStatus(STATUS_STOPPED) isRecognizing = false
println("ASR WebSocket连接关闭: $code, $reason") }
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "连接失败", t)
errorCallback("CONNECTION_ERROR", t.message ?: "连接失败")
isRecognizing = false
} }
}) })
} }
/** /**
* 发送音频数据 * 向服务器发送音频数据
*/ */
private suspend fun feedAudioData(audioData: ByteArray) = withContext(Dispatchers.IO) { suspend fun feedAudioData(audioData: ByteArray) {
if (!isRunning || isLastPacket) return@withContext if (!isRecognizing || webSocket == null) {
Log.w(TAG, "未开始识别或WebSocket未连接")
return
}
webSocket?.let { ws -> try {
// 发送音频请求 // 在实际实现中需要按照火山引擎的协议格式封装音频数据
sendAudioRequest(ws, audioData, false) val byteString = ByteString.of(*audioData)
webSocket?.send(byteString)
} catch (e: Exception) {
Log.e(TAG, "发送音频数据失败", e)
} }
} }
/** /**
* 停止识别 * 停止语音识别
*/ */
private suspend fun stop() = withContext(Dispatchers.IO) { suspend fun stop() {
if (!isRunning || isLastPacket) return@withContext if (!isRecognizing) {
return
isLastPacket = true }
webSocket?.let { ws -> try {
// 发送最后一个音频包 // 发送最后一帧音频数据,标记结束
sendAudioRequest(ws, ByteArray(0), true) // 在实际实现中需要按照协议发送结束标记
isRecognizing = false
webSocket?.close(1000, "正常关闭")
webSocket = null
} catch (e: Exception) {
Log.e(TAG, "停止识别失败", e)
} }
} }
@ -420,7 +390,7 @@ class BytedanceASR(
fun release() { fun release() {
webSocket?.cancel() webSocket?.cancel()
webSocket = null webSocket = null
isRunning = false isRecognizing = false
isLastPacket = false isLastPacket = false
updateStatus(STATUS_STOPPED) updateStatus(STATUS_STOPPED)
} }
@ -429,123 +399,133 @@ class BytedanceASR(
* 发送初始配置请求 * 发送初始配置请求
*/ */
private fun sendInitialRequest(webSocket: WebSocket) { private fun sendInitialRequest(webSocket: WebSocket) {
// 构建请求参数 try {
val user = JsonObject().apply { // 构建请求参数
addProperty("uid", "test") val user = JsonObject().apply {
} addProperty("uid", "test")
}
val audio = JsonObject().apply {
addProperty("format", "pcm") val audio = JsonObject().apply {
addProperty("sample_rate", sampleRate) addProperty("format", "pcm")
addProperty("bits", bits) addProperty("sample_rate", sampleRate)
addProperty("channel", channels) addProperty("bits", bits)
addProperty("codec", "raw") addProperty("channel", channels)
} addProperty("codec", "raw")
}
val request = JsonObject().apply {
addProperty("model_name", "bigmodel") val request = JsonObject().apply {
addProperty("enable_punc", enablePunctuation) addProperty("model_name", "bigmodel")
} addProperty("enable_punc", enablePunctuation)
}
val payload = JsonObject().apply {
add("user", user) val payload = JsonObject().apply {
add("audio", audio) add("user", user)
add("request", request) add("audio", audio)
add("request", request)
}
val payloadStr = payload.toString()
Log.d(TAG, "ASR初始请求: $payloadStr")
// 压缩负载
val compressedPayload = gzipCompress(payloadStr.toByteArray())
// 构建消息头
val header = getHeader(
FULL_CLIENT_REQUEST,
POS_SEQUENCE,
JSON,
GZIP,
0
)
// 构建消息序列号
val seqBytes = intToBytes(seq)
// 构建负载大小
val payloadSizeBytes = intToBytes(compressedPayload.size)
// 组装完整请求数据
val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size)
var offset = 0
System.arraycopy(header, 0, requestBytes, offset, header.size)
offset += header.size
System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size)
offset += seqBytes.size
System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size)
offset += payloadSizeBytes.size
System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size)
// 发送请求
val byteString = ByteString.of(*requestBytes)
webSocket.send(byteString)
} catch (e: Exception) {
Log.e(TAG, "发送初始请求失败", e)
} }
val payloadStr = payload.toString()
println("ASR初始请求: $payloadStr")
// 压缩负载
val compressedPayload = gzipCompress(payloadStr.toByteArray())
// 构建消息头
val header = getHeader(
FULL_CLIENT_REQUEST,
POS_SEQUENCE,
JSON,
GZIP,
0
)
// 构建消息序列号
val seqBytes = intToBytes(seq)
// 构建负载大小
val payloadSizeBytes = intToBytes(compressedPayload.size)
// 组装完整请求数据
val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size)
var offset = 0
System.arraycopy(header, 0, requestBytes, offset, header.size)
offset += header.size
System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size)
offset += seqBytes.size
System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size)
offset += payloadSizeBytes.size
System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size)
// 发送请求
webSocket.send(ByteString.of(requestBytes))
} }
/** /**
* 发送音频数据请求 * 发送音频数据请求
*/ */
private fun sendAudioRequest(webSocket: WebSocket, audioData: ByteArray, isLast: Boolean) { private fun sendAudioRequest(webSocket: WebSocket, audioData: ByteArray, isLast: Boolean) {
seq++ try {
seq++
// 如果是最后一个包,使用负序列号
val actualSeq = if (isLast) -seq else seq // 如果是最后一个包,使用负序列号
val actualSeq = if (isLast) -seq else seq
// 设置消息标志
val flag = if (isLast) NEG_WITH_SEQUENCE else POS_SEQUENCE // 设置消息标志
val flag = if (isLast) NEG_WITH_SEQUENCE else POS_SEQUENCE
// 构建消息头
val header = getHeader( // 构建消息头
AUDIO_ONLY_REQUEST, val header = getHeader(
flag, AUDIO_ONLY_REQUEST,
NO_SERIALIZATION, flag,
GZIP, NO_SERIALIZATION,
0 GZIP,
) 0
)
// 构建消息序列号
val seqBytes = intToBytes(actualSeq) // 构建消息序列号
val seqBytes = intToBytes(actualSeq)
// 压缩音频数据
val compressedPayload = if (audioData.isNotEmpty()) { // 压缩音频数据
gzipCompress(audioData) val compressedPayload = if (audioData.isNotEmpty()) {
} else { gzipCompress(audioData)
ByteArray(0) } else {
} ByteArray(0)
}
// 构建负载大小
val payloadSizeBytes = intToBytes(compressedPayload.size) // 构建负载大小
val payloadSizeBytes = intToBytes(compressedPayload.size)
// 组装完整请求数据
val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size) // 组装完整请求数据
var offset = 0 val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size)
var offset = 0
System.arraycopy(header, 0, requestBytes, offset, header.size)
offset += header.size System.arraycopy(header, 0, requestBytes, offset, header.size)
offset += header.size
System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size)
offset += seqBytes.size System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size)
offset += seqBytes.size
System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size)
offset += payloadSizeBytes.size System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size)
offset += payloadSizeBytes.size
if (compressedPayload.isNotEmpty()) {
System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size) if (compressedPayload.isNotEmpty()) {
System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size)
}
// 发送请求
val byteString = ByteString.of(*requestBytes)
webSocket.send(byteString)
} catch (e: Exception) {
Log.e(TAG, "发送音频请求失败", e)
} }
// 发送请求
webSocket.send(ByteString.of(requestBytes))
} }
/** /**
@ -619,7 +599,7 @@ class BytedanceASR(
return "" return ""
} catch (e: Exception) { } catch (e: Exception) {
println("解析JSON失败: ${e.message}") Log.e(TAG, "解析JSON失败", e)
return "" return ""
} }
} }

329
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt

@ -0,0 +1,329 @@
package com.deep_voice.bytedance_speech
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import android.os.Build
import android.util.Log
import com.deep_voice.speech.tts.AudioDataListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.CancellationException
/**
* 字节跳动语音合成音频播放器
*
* 使用AudioTrack播放PCM格式音频流,基于Kotlin协程实现异步处理
*/
class BytedanceAudioPlayer : AudioDataListener, CoroutineScope {
companion object {
private const val TAG = "BytedanceAudioPlayer"
private const val SAMPLE_RATE = 24000 // 样本率
private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO // 单声道
private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT // 16位PCM
private const val IDLE_TIMEOUT_MS = 800L // 无数据超时时间
}
// 协程相关
private val job = SupervisorJob()
override val coroutineContext = Dispatchers.IO + job
// 音频处理相关
private var audioTrack: AudioTrack? = null
private val isPlaying = AtomicBoolean(false)
private val isPaused = AtomicBoolean(false)
// 播放状态
private val playStarted = AtomicBoolean(false)
private val completionReported = AtomicBoolean(false)
// 使用简单的并发队列,保证线程安全
private val audioDataQueue = ConcurrentLinkedQueue<ByteArray>()
// 播放状态监听
private var onPlayStarted: (() -> Unit)? = null
private var onPlayCompleted: (() -> Unit)? = null
private var onError: ((String) -> Unit)? = null
// 处理协程
private var processingJob: Job? = null
private var lastDataTime = 0L
init {
Log.d(TAG, "BytedanceAudioPlayer初始化")
}
/**
* 开始播放
*/
fun start() {
if (isPlaying.getAndSet(true)) return
isPaused.set(false)
playStarted.set(false)
completionReported.set(false)
lastDataTime = System.currentTimeMillis()
processingJob = launch {
try {
initAudioTrack()
// 处理队列中的音频数据
while (isActive && isPlaying.get()) {
processQueuedAudio()
// 检查是否播放完成
checkPlaybackCompletion()
// 队列为空时短暂延迟
if (audioDataQueue.isEmpty()) {
delay(10)
}
}
} catch (e: CancellationException) {
// 协程被取消,正常行为
} catch (e: Exception) {
Log.e(TAG, "播放错误: ${e.message}")
onError?.invoke("播放错误: ${e.message}")
stopInternal(false)
}
}
}
/**
* 暂停播放
*/
fun pause() {
if (!isPlaying.get() || isPaused.getAndSet(true)) return
audioTrack?.pause()
}
/**
* 恢复播放
*/
fun resume() {
if (!isPlaying.get() || !isPaused.getAndSet(false)) return
audioTrack?.play()
}
/**
* 停止播放
*/
fun stop() {
Log.d(TAG, "停止播放")
stopInternal(true)
}
/**
* 内部停止处理
*/
private fun stopInternal(reportCompletion: Boolean) {
if (!isPlaying.getAndSet(false)) return
isPaused.set(false)
processingJob?.cancel()
audioTrack?.stop()
// 不在stopInternal中释放资源,只停止播放
// 清空队列
audioDataQueue.clear()
// 播放完成通知
if (reportCompletion && playStarted.get() && !completionReported.getAndSet(true)) {
launch(Dispatchers.Main) {
onPlayCompleted?.invoke()
}
}
}
/**
* 释放资源
*/
fun release() {
stopInternal(false)
// 在release方法中释放AudioTrack资源
audioTrack?.release()
audioTrack = null
// 释放协程资源
job.cancel()
}
/**
* 设置播放开始回调
*/
fun setOnPlayStarted(listener: () -> Unit) {
onPlayStarted = listener
}
/**
* 设置播放完成回调
*/
fun setOnPlayCompleted(listener: () -> Unit) {
onPlayCompleted = listener
}
/**
* 设置错误回调
*/
fun setOnError(listener: (String) -> Unit) {
onError = listener
}
/**
* 接收音频数据(实现AudioDataListener接口)
*/
override fun onAudioData(data: ByteArray) {
if (data.isEmpty()) return
lastDataTime = System.currentTimeMillis()
if (!isPlaying.get()) {
start()
}
// 添加到队列 - 简单有效,保证FIFO顺序
audioDataQueue.add(data.copyOf())
}
/**
* 检查是否正在播放
*/
fun isPlaying(): Boolean = isPlaying.get() && !isPaused.get()
/**
* 处理队列中的音频数据
*/
private suspend fun processQueuedAudio() {
if (isPaused.get() || !isPlaying.get() || !isActive) return
// 获取并处理队列中的数据
val data = audioDataQueue.poll() ?: return
// 写入音频数据
val result = audioTrack?.write(data, 0, data.size) ?: 0
if (result > 0) {
// 标记播放开始
if (!playStarted.getAndSet(true)) {
withContext(Dispatchers.Main) {
onPlayStarted?.invoke()
}
}
} else if (result < 0) {
// 处理错误
handleAudioTrackError(result)
}
}
/**
* 检查播放是否完成(超时无数据)
*/
private suspend fun checkPlaybackCompletion() {
val currentTime = System.currentTimeMillis()
// 超时判断 - 队列为空且超过超时时间
if (playStarted.get() && audioDataQueue.isEmpty() &&
currentTime - lastDataTime > IDLE_TIMEOUT_MS && isPlaying.get()) {
// 播放完成
if (!completionReported.getAndSet(true)) {
withContext(Dispatchers.Main) {
onPlayCompleted?.invoke()
}
stopInternal(false) // 已报告,不需要再次报告
}
}
}
/**
* 处理AudioTrack错误
*/
private fun handleAudioTrackError(result: Int) {
val errorMsg = when(result) {
AudioTrack.ERROR_INVALID_OPERATION -> "无效操作"
AudioTrack.ERROR_BAD_VALUE -> "参数错误"
AudioTrack.ERROR_DEAD_OBJECT -> "对象已销毁"
else -> "未知错误"
}
// 对象已销毁时尝试重建
if (result == AudioTrack.ERROR_DEAD_OBJECT) {
try {
audioTrack?.release()
initAudioTrack()
} catch (e: Exception) {
Log.e(TAG, "重建AudioTrack失败: ${e.message}")
throw e
}
} else {
Log.e(TAG, "AudioTrack错误: $errorMsg")
}
}
/**
* 初始化AudioTrack
*/
private fun initAudioTrack() {
val minBufferSize = AudioTrack.getMinBufferSize(
SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT
)
if (minBufferSize == AudioTrack.ERROR || minBufferSize == AudioTrack.ERROR_BAD_VALUE) {
throw IllegalStateException("无法获取有效的音频缓冲区大小")
}
// 使用较大的缓冲区提高稳定性
val bufferSize = minBufferSize * 4
// 创建AudioTrack
audioTrack = if (Build.VERSION.SDK_INT >= 23) { // Android M (6.0)
AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AUDIO_FORMAT)
.setSampleRate(SAMPLE_RATE)
.setChannelMask(CHANNEL_CONFIG)
.build()
)
.setBufferSizeInBytes(bufferSize)
.setTransferMode(AudioTrack.MODE_STREAM)
.build()
} else {
@Suppress("DEPRECATION")
AudioTrack(
AudioManager.STREAM_MUSIC,
SAMPLE_RATE,
CHANNEL_CONFIG,
AUDIO_FORMAT,
bufferSize,
AudioTrack.MODE_STREAM
)
}
// 检查初始化状态
if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) {
throw IllegalStateException("AudioTrack初始化失败")
}
// 开始播放
audioTrack?.play()
}
}

670
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt

@ -3,217 +3,176 @@ package com.deep_voice.bytedance_speech
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import androidx.annotation.NonNull import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.deep_voice.speech.tts.AudioDataListener
import java.util.concurrent.ConcurrentLinkedQueue
/** 火山引擎语音识别和语音合成插件 */ /**
* 字节跳动语音服务Flutter插件
* 提供TTS和ASR能力,通过独立的事件通道进行通信
*/
class BytedanceSpeechPlugin : FlutterPlugin { class BytedanceSpeechPlugin : FlutterPlugin {
private val TAG = "BytedanceSpeechPlugin" companion object {
private lateinit var context: Context private const val TAG = "BytedanceSpeechPlugin"
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val mainHandler = Handler(Looper.getMainLooper()) // 通道名称
private const val TTS_METHOD_CHANNEL = "com.deep_voice.bytedance_tts"
private const val ASR_METHOD_CHANNEL = "com.deep_voice.bytedance_asr"
// 事件通道
private const val TTS_EVENT_CHANNEL = "com.deep_voice.bytedance_tts/events"
private const val TTS_AUDIO_CHANNEL = "com.deep_voice.bytedance_tts/audio"
private const val ASR_EVENT_CHANNEL = "com.deep_voice.bytedance_asr/events"
}
// ASR相关 // Flutter插件相关
private lateinit var ttsChannel: MethodChannel
private lateinit var asrChannel: MethodChannel private lateinit var asrChannel: MethodChannel
private lateinit var asrEventChannel: EventChannel private lateinit var context: Context
private var asrEventSink: EventChannel.EventSink? = null
private var asrInstance: BytedanceASR? = null
// TTS相关 // TTS事件通道
private lateinit var ttsChannel: MethodChannel
private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null private var ttsEventSink: EventChannel.EventSink? = null
private var ttsInstance: BytedanceTTS? = null private var ttsAudioSink: EventChannel.EventSink? = null
// ASR事件发送方法 // ASR事件通道
private fun sendAsrEvent(event: Map<String, Any>) { private var asrEventSink: EventChannel.EventSink? = null
println("发送ASR事件: $event")
if (asrEventSink == null) {
println("无法发送ASR事件:事件通道未准备好")
return
}
mainHandler.post {
try {
asrEventSink?.success(event)
println("ASR事件发送成功")
} catch (e: Exception) {
println("发送ASR事件失败: ${e.message}")
}
}
}
// TTS事件发送方法 // 语音服务实例
private fun sendTtsEvent(event: Map<String, Any>) { private var bytedanceTTS: BytedanceTTS? = null
println("发送TTS事件: $event") private var bytedanceASR: BytedanceASR? = null
if (ttsEventSink == null) {
println("无法发送TTS事件:事件通道未准备好") // 协程作用域
return private val job = SupervisorJob()
} private val scope = CoroutineScope(Dispatchers.Main + job)
mainHandler.post { // 主线程Handler
try { private val mainHandler = Handler(Looper.getMainLooper())
ttsEventSink?.success(event)
println("TTS事件发送成功")
} catch (e: Exception) {
println("发送TTS事件失败: ${e.message}")
}
}
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext context = flutterPluginBinding.applicationContext
// 初始化ASR通道 // 初始化TTS方法通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr") ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, TTS_METHOD_CHANNEL)
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR方法通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, ASR_METHOD_CHANNEL)
asrChannel.setMethodCallHandler(AsrMethodHandler()) asrChannel.setMethodCallHandler(AsrMethodHandler())
// 初始化TTS通道 // 设置TTS事件通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts") setupTtsEventChannels(flutterPluginBinding)
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR事件通道 // 设置ASR事件通道
asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr_events") setupAsrEventChannel(flutterPluginBinding)
asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { Log.d(TAG, "字节跳动语音插件已附加到Flutter引擎")
asrEventSink = events }
}
/**
override fun onCancel(arguments: Any?) { * 设置TTS相关的事件通道
asrEventSink = null */
private fun setupTtsEventChannels(binding: FlutterPlugin.FlutterPluginBinding) {
// TTS事件通道
EventChannel(binding.binaryMessenger, TTS_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events
Log.d(TAG, "TTS事件流已建立连接")
}
override fun onCancel(arguments: Any?) {
ttsEventSink = null
Log.d(TAG, "TTS事件流已取消")
}
} }
}) )
// 初始化TTS事件通道 // TTS音频数据通道
ttsEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts_events") EventChannel(binding.binaryMessenger, TTS_AUDIO_CHANNEL).setStreamHandler(
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler { object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events ttsAudioSink = events
Log.d(TAG, "TTS音频流已建立连接")
}
override fun onCancel(arguments: Any?) {
ttsAudioSink = null
Log.d(TAG, "TTS音频流已取消")
}
} }
)
override fun onCancel(arguments: Any?) { }
ttsEventSink = null
/**
* 设置ASR相关的事件通道
*/
private fun setupAsrEventChannel(binding: FlutterPlugin.FlutterPluginBinding) {
// ASR事件通道
EventChannel(binding.binaryMessenger, ASR_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
asrEventSink = events
Log.d(TAG, "ASR事件流已建立连接")
}
override fun onCancel(arguments: Any?) {
asrEventSink = null
Log.d(TAG, "ASR事件流已取消")
}
} }
}) )
} }
// ASR方法处理器 /**
inner class AsrMethodHandler : MethodCallHandler { * TTS方法处理器
*/
inner class TtsMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) { when (call.method) {
"initialize" -> { "initialize" -> {
val appId = call.argument<String>("appId") ?: "" val ttsAppId = call.argument<String>("ttsAppId") ?: ""
val accessToken = call.argument<String>("accessToken") ?: "" val ttsAppToken = call.argument<String>("ttsAppToken") ?: ""
val streamMode = call.argument<Boolean>("streamMode") ?: true val language = call.argument<String>("language") ?: "zh-CN"
val enablePunctuation = call.argument<Boolean>("enablePunctuation") ?: true
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val bits = call.argument<Int>("bits") ?: 16
val channels = call.argument<Int>("channels") ?: 1
try { initializeTTS(ttsAppId, ttsAppToken, language, result)
asrInstance = BytedanceASR(
context = context,
appId = appId,
accessToken = accessToken,
streamMode = streamMode,
enablePunctuation = enablePunctuation,
sampleRate = sampleRate,
bits = bits,
channels = channels,
resultCallback = { text, isFinal ->
val eventType = if (isFinal) "result" else "recognizing"
sendAsrEvent(mapOf(
"type" to eventType,
"text" to text,
"isFinal" to isFinal
))
},
errorCallback = { errorCode, errorMessage ->
sendAsrEvent(mapOf(
"type" to "error",
"errorCode" to errorCode,
"errorMessage" to errorMessage
))
}
)
result.success(true)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
} }
"startRecognition" -> { "setVoice" -> {
if (asrEventSink == null) { val voice = call.argument<String>("voice") ?: ""
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始识别", null) setVoice(voice, result)
return
}
scope.launch {
try {
asrInstance?.start()
sendAsrEvent(mapOf("type" to "sessionStarted"))
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("START_ERROR", e.message, null)
}
}
}
} }
"feedAudioData" -> { "speakOnce" -> {
val audioData = call.argument<ByteArray>("audioData") val text = call.argument<String>("text") ?: ""
if (audioData != null) { speakOnce(text, result)
scope.launch {
try {
asrInstance?.feedAudioData(audioData)
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("FEED_ERROR", e.message, null)
}
}
}
} else {
result.error("INVALID_ARGUMENT", "音频数据不能为空", null)
}
} }
"stopRecognition" -> { "speakStream" -> {
scope.launch { val text = call.argument<String>("text") ?: ""
try { speakStream(text, result)
asrInstance?.stop()
sendAsrEvent(mapOf("type" to "sessionStopped"))
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("STOP_ERROR", e.message, null)
}
}
}
} }
"dispose" -> { "flushStream" -> {
try { flushStream(result)
asrInstance?.release() }
asrInstance = null "stopTTS" -> {
result.success(true) stopTTS(result)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
}
} }
"disposeTTS" -> {
disposeTTS(result)
}
else -> { else -> {
result.notImplemented() result.notImplemented()
} }
@ -221,93 +180,23 @@ class BytedanceSpeechPlugin : FlutterPlugin {
} }
} }
// TTS方法处理器 /**
inner class TtsMethodHandler : MethodCallHandler { * ASR方法处理器
*/
inner class AsrMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) { when (call.method) {
"initialize" -> { "initializeASR" -> {
val appId = call.argument<String>("appId") ?: "" result.notImplemented()
val accessToken = call.argument<String>("accessToken") ?: ""
val speaker = call.argument<String>("speaker") ?: ""
val format = call.argument<String>("format") ?: "mp3"
val sampleRate = call.argument<Int>("sampleRate") ?: 24000
val enableTimestamp = call.argument<Boolean>("enableTimestamp") ?: true
try {
ttsInstance = BytedanceTTS(
context = context,
appId = appId,
accessToken = accessToken,
speaker = speaker,
format = format,
sampleRate = sampleRate,
enableTimestamp = enableTimestamp,
audioCallback = { audioData ->
sendTtsEvent(mapOf(
"type" to "audio",
"audioData" to audioData
))
},
completionCallback = {
sendTtsEvent(mapOf("type" to "completed"))
},
errorCallback = { errorCode, errorMessage ->
sendTtsEvent(mapOf(
"type" to "error",
"errorCode" to errorCode,
"errorMessage" to errorMessage
))
}
)
result.success(true)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
} }
"synthesize" -> { "startListening" -> {
val text = call.argument<String>("text") ?: "" result.notImplemented()
if (ttsEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始合成", null)
return
}
scope.launch {
try {
sendTtsEvent(mapOf("type" to "started"))
ttsInstance?.synthesize(text)
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("SYNTHESIS_ERROR", e.message, null)
}
}
}
} }
"stopSynthesis" -> { "stopListening" -> {
scope.launch { result.notImplemented()
try {
ttsInstance?.stop()
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("STOP_ERROR", e.message, null)
}
}
}
} }
"dispose" -> { "cancelASR" -> {
try { result.notImplemented()
ttsInstance?.release()
ttsInstance = null
result.success(true)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
}
} }
else -> { else -> {
result.notImplemented() result.notImplemented()
@ -317,15 +206,274 @@ class BytedanceSpeechPlugin : FlutterPlugin {
} }
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
asrChannel.setMethodCallHandler(null)
ttsChannel.setMethodCallHandler(null) ttsChannel.setMethodCallHandler(null)
asrEventChannel.setStreamHandler(null) asrChannel.setMethodCallHandler(null)
ttsEventChannel.setStreamHandler(null)
// 释放资源
bytedanceTTS?.release()
bytedanceTTS = null
bytedanceASR?.release()
bytedanceASR = null
job.cancel()
Log.d(TAG, "字节跳动语音插件已从Flutter引擎分离")
}
/**
* 初始化TTS服务
*/
private fun initializeTTS(ttsAppId: String, ttsAppToken: String, language: String, result: Result) {
scope.launch {
try {
if (ttsAppId.isEmpty() || ttsAppToken.isEmpty()) {
result.error(
"INVALID_PARAMS",
"AppID和AppToken不能为空",
null
)
return@launch
}
// 确保只有一个TTS实例
bytedanceTTS?.release()
// 创建新的TTS实例
bytedanceTTS = BytedanceTTS(context)
// 添加事件监听器
bytedanceTTS?.addListener(object : TtsEventListener {
override fun onEvent(event: TtsEvent) {
when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> {
sendTTSEvent("started", null)
}
TtsEventType.SYNTHESIS_COMPLETED -> {
sendTTSEvent("completed", null)
}
TtsEventType.SYNTHESIS_CANCELED -> {
sendTTSEvent("canceled", null)
}
TtsEventType.ERROR -> {
val params = HashMap<String, Any>()
params["code"] = event.params["errorCode"] ?: "UNKNOWN_ERROR"
params["message"] = event.params["errorMessage"] ?: "未知错误"
sendTTSEvent("error", params)
}
}
}
})
// 添加音频数据监听器
bytedanceTTS?.addAudioDataListener(object : AudioDataListener {
override fun onAudioData(data: ByteArray) {
sendTTSAudioData(data)
}
})
// 初始化TTS
val success = bytedanceTTS?.initialize(
ttsAppId = ttsAppId,
ttsAppToken = ttsAppToken,
ttsResource = "", // 火山TTS不使用resource
language = language
) ?: false
if (success) {
result.success(true)
} else {
result.error(
"INIT_FAILED",
"TTS初始化失败",
null
)
}
} catch (e: Exception) {
Log.e(TAG, "TTS初始化异常: ${e.message}", e)
result.error(
"INIT_EXCEPTION",
"TTS初始化异常: ${e.message}",
null
)
}
}
}
/**
* 设置TTS音色
*/
private fun setVoice(voice: String, result: Result) {
if (bytedanceTTS == null) {
result.error("NOT_INITIALIZED", "TTS未初始化", null)
return
}
try {
val success = bytedanceTTS?.setVoice(voice) ?: false
result.success(success)
} catch (e: Exception) {
Log.e(TAG, "设置音色异常: ${e.message}", e)
result.error(
"VOICE_EXCEPTION",
"设置音色异常: ${e.message}",
null
)
}
}
/**
* 合成并播放文本(单次)
*/
private fun speakOnce(text: String, result: Result) {
if (bytedanceTTS == null) {
result.error("NOT_INITIALIZED", "TTS未初始化", null)
return
}
scope.cancel() try {
asrInstance?.release() val success = bytedanceTTS?.speakOnce(text) ?: false
ttsInstance?.release() result.success(success)
asrInstance = null } catch (e: Exception) {
ttsInstance = null Log.e(TAG, "合成文本异常: ${e.message}", e)
result.error(
"SPEAK_EXCEPTION",
"合成文本异常: ${e.message}",
null
)
}
}
/**
* 流式合成文本
*/
private fun speakStream(text: String, result: Result) {
if (bytedanceTTS == null) {
result.error("NOT_INITIALIZED", "TTS未初始化", null)
return
}
try {
val success = bytedanceTTS?.speakStream(text) ?: false
result.success(success)
} catch (e: Exception) {
Log.e(TAG, "流式合成异常: ${e.message}", e)
result.error(
"STREAM_EXCEPTION",
"流式合成异常: ${e.message}",
null
)
}
}
/**
* 刷新流式合成缓冲区
*/
private fun flushStream(result: Result) {
if (bytedanceTTS == null) {
result.error("NOT_INITIALIZED", "TTS未初始化", null)
return
}
try {
val success = bytedanceTTS?.flushStream() ?: false
result.success(success)
} catch (e: Exception) {
Log.e(TAG, "刷新流异常: ${e.message}", e)
result.error(
"FLUSH_EXCEPTION",
"刷新流异常: ${e.message}",
null
)
}
}
/**
* 停止TTS
*/
private fun stopTTS(result: Result) {
if (bytedanceTTS == null) {
result.success(true)
return
}
try {
val success = bytedanceTTS?.stop() ?: true
result.success(success)
} catch (e: Exception) {
Log.e(TAG, "停止TTS异常: ${e.message}", e)
result.error(
"STOP_EXCEPTION",
"停止TTS异常: ${e.message}",
null
)
}
}
/**
* 释放TTS资源
*/
private fun disposeTTS(result: Result) {
try {
bytedanceTTS?.release()
bytedanceTTS = null
result.success(true)
} catch (e: Exception) {
Log.e(TAG, "释放TTS资源异常: ${e.message}", e)
result.error(
"DISPOSE_EXCEPTION",
"释放TTS资源异常: ${e.message}",
null
)
}
}
/**
* 发送TTS事件到Flutter端
*/
private fun sendTTSEvent(eventName: String, params: Map<String, Any>?) {
if (ttsEventSink == null) return
val event = HashMap<String, Any>()
event["event"] = eventName
if (params != null) {
event["params"] = params
}
mainHandler.post {
ttsEventSink?.success(event)
}
}
/**
* 发送TTS音频数据到Flutter端
*/
private fun sendTTSAudioData(data: ByteArray) {
if (ttsAudioSink == null) return
mainHandler.post {
ttsAudioSink?.success(data)
}
}
/**
* 发送ASR事件到Flutter端
*/
private fun sendASREvent(eventType: String, data: Any, params: Map<String, Any>? = null) {
if (asrEventSink == null) return
val event = HashMap<String, Any>()
event["type"] = eventType
event["data"] = data
if (params != null) {
event["params"] = params
}
mainHandler.post {
asrEventSink?.success(event)
}
} }
} }

757
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt

File diff suppressed because it is too large

5
local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt

@ -15,7 +15,6 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import com.yunqiinnovation.deepvoice_utils.FileLogger
/** ClassicBluetoothPlugin */ /** ClassicBluetoothPlugin */
class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler { class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler {
@ -119,7 +118,7 @@ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler {
sendEvent(eventMap) sendEvent(eventMap)
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "蓝牙状态获取失败", e) Log.e(TAG, "蓝牙状态获取失败", e)
} }
} }
@ -188,7 +187,7 @@ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler {
eventSink?.success(event) eventSink?.success(event)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "事件发送失败", e) Log.e(TAG, "事件发送失败", e)
} }
} }
} }

2
local_plugins/classic_bluetooth/pubspec.yaml

@ -9,8 +9,6 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
deepvoice_utils:
path: ../deepvoice_utils
flutter: flutter:
plugin: plugin:

46
local_plugins/deepvoice_utils/android/build.gradle.kts

@ -1,46 +0,0 @@
plugins {
// Android Library 插件
id("com.android.library")
// Kotlin Android 插件
id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.deepvoice_utils"
// 目标 SDK 版本
compileSdk = 35
defaultConfig {
// 最低 SDK 版本
minSdk = 21
targetSdk = 34
}
// Java 语言级别兼容配置
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// Kotlin 语言级别
kotlinOptions {
jvmTarget = "11"
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}

1
local_plugins/deepvoice_utils/android/settings.gradle.kts

@ -1 +0,0 @@
rootProject.name = "deepvoice_utils"

7
local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.deepvoice_utils">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

33
local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt

@ -1,33 +0,0 @@
package com.yunqiinnovation.deepvoice_utils
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
/** DeepvoiceUtilsPlugin */
class DeepvoiceUtilsPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "deepvoice_utils")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"getPlatformVersion" -> {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

237
local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt

@ -1,237 +0,0 @@
package com.yunqiinnovation.deepvoice_utils
import android.content.Context
import android.util.Log
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* 文件日志工具
* 将日志同时输出到Logcat和文件系统
*/
class FileLogger private constructor(context: Context) {
private val logDir: File
private var currentLogFile: File? = null
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
private val logExecutor: ExecutorService = Executors.newSingleThreadExecutor()
init {
// 初始化日志目录
logDir = File(context.getExternalFilesDir(null), "logs")
if (!logDir.exists()) {
logDir.mkdirs()
}
// 创建当天的日志文件
updateLogFile()
}
/**
* 更新日志文件为当天日期
*/
private fun updateLogFile() {
val today = dateFormat.format(Date())
val logFile = File(logDir, "log_$today.txt")
if (currentLogFile == null || currentLogFile?.absolutePath != logFile.absolutePath) {
currentLogFile = logFile
// 如果是新文件,添加文件头
if (!logFile.exists()) {
try {
logFile.createNewFile()
FileWriter(logFile, true).use { writer ->
writer.append("===== 日志开始: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())} =====\n")
}
} catch (e: IOException) {
Log.e(TAG, "创建日志文件失败: ${e.message}")
}
}
}
}
/**
* 调试级别日志
*/
fun d(tag: String, message: String) {
// 输出到Logcat
Log.d(tag, message)
// 异步写入文件
logToFile("D", tag, message)
}
/**
* 信息级别日志
*/
fun i(tag: String, message: String) {
// 输出到Logcat
Log.i(tag, message)
// 异步写入文件
logToFile("I", tag, message)
}
/**
* 警告级别日志
*/
fun w(tag: String, message: String) {
// 输出到Logcat
Log.w(tag, message)
// 异步写入文件
logToFile("W", tag, message)
}
/**
* 错误级别日志
*/
fun e(tag: String, message: String, throwable: Throwable? = null) {
// 输出到Logcat
if (throwable != null) {
Log.e(tag, message, throwable)
} else {
Log.e(tag, message)
}
// 异步写入文件
val errorMsg = if (throwable != null) {
"$message\n${throwable.message}\n${throwable.stackTraceToString()}"
} else {
message
}
logToFile("E", tag, errorMsg)
}
/**
* 将日志写入文件
*/
private fun logToFile(level: String, tag: String, message: String) {
logExecutor.execute {
try {
// 检查是否需要更新日志文件
updateLogFile()
// 格式化日志内容
val timestamp = timeFormat.format(Date())
val logLine = "$timestamp $level/$tag: $message\n"
// 写入文件
if (currentLogFile != null && currentLogFile!!.exists()) {
FileWriter(currentLogFile!!, true).use { writer ->
writer.append(logLine)
}
}
} catch (e: IOException) {
Log.e(TAG, "写入日志文件失败: ${e.message}")
}
}
}
/**
* 清理旧日志文件
* @param daysToKeep 保留的天数
*/
fun cleanOldLogs(daysToKeep: Int = 7) {
logExecutor.execute {
try {
val currentTime = System.currentTimeMillis()
val maxAge = daysToKeep * 24 * 60 * 60 * 1000L
logDir.listFiles()?.forEach { file ->
if (file.isFile && file.name.startsWith("log_") && file.name.endsWith(".txt")) {
val fileAge = currentTime - file.lastModified()
if (fileAge > maxAge) {
val deleted = file.delete()
if (deleted) {
Log.d(TAG, "已删除旧日志文件: ${file.name}")
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "清理旧日志失败: ${e.message}")
}
}
}
/**
* 关闭日志系统
*/
fun shutdown() {
logExecutor.shutdown()
}
companion object {
private const val TAG = "FileLogger"
private var INSTANCE: FileLogger? = null
/**
* 初始化日志系统
*/
@Synchronized
fun init(context: Context) {
if (INSTANCE == null) {
INSTANCE = FileLogger(context.applicationContext)
d(TAG, "文件日志系统已初始化")
}
}
/**
* 调试日志
*/
fun d(tag: String, message: String) {
INSTANCE?.d(tag, message) ?: Log.d(tag, message)
}
/**
* 信息日志
*/
fun i(tag: String, message: String) {
INSTANCE?.i(tag, message) ?: Log.i(tag, message)
}
/**
* 警告日志
*/
fun w(tag: String, message: String) {
INSTANCE?.w(tag, message) ?: Log.w(tag, message)
}
/**
* 错误日志
*/
fun e(tag: String, message: String, throwable: Throwable? = null) {
INSTANCE?.e(tag, message, throwable) ?: run {
if (throwable != null) {
Log.e(tag, message, throwable)
} else {
Log.e(tag, message)
}
}
}
/**
* 清理旧日志
*/
fun cleanOldLogs(daysToKeep: Int = 7) {
INSTANCE?.cleanOldLogs(daysToKeep)
}
/**
* 关闭日志系统
*/
fun shutdown() {
INSTANCE?.shutdown()
INSTANCE = null
}
}
}

4
local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h

@ -1,4 +0,0 @@
#import <Flutter/Flutter.h>
@interface DeepvoiceUtilsPlugin : NSObject<FlutterPlugin>
@end

12
local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m

@ -1,12 +0,0 @@
#import "DeepvoiceUtilsPlugin.h"
#if __has_include(<deepvoice_utils/deepvoice_utils-Swift.h>)
#import <deepvoice_utils/deepvoice_utils-Swift.h>
#else
#import "deepvoice_utils-Swift.h"
#endif
@implementation DeepvoiceUtilsPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[SwiftDeepvoiceUtilsPlugin registerWithRegistrar:registrar];
}
@end

30
local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift

@ -1,30 +0,0 @@
import Flutter
import UIKit
public class DeepvoiceUtilsPlugin: NSObject, FlutterPlugin {
private static let TAG = "DeepvoiceUtilsPlugin"
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "deepvoice_utils", binaryMessenger: registrar.messenger())
let instance = DeepvoiceUtilsPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
// 初始化文件日志
FileLogger.initialize()
FileLogger.d(TAG, "DeepvoiceUtilsPlugin 已初始化")
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS \(UIDevice.current.systemVersion)")
default:
result(FlutterMethodNotImplemented)
}
}
public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
// 关闭日志系统
FileLogger.shutdown()
}
}

286
local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift

@ -1,286 +0,0 @@
import Foundation
import os.log
/**
* 文件日志工具
* 将日志同时输出到控制台和文件系统
*/
@objc public class FileLogger: NSObject {
private static let TAG = "FileLogger"
private static var instance: FileLogger?
private let logDir: URL
private var currentLogFile: URL?
private let logQueue = DispatchQueue(label: "com.yunqiinnovation.deepvoice_utils.logger", qos: .utility)
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
private let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS"
return formatter
}()
/**
* 初始化日志系统
*/
private init(directory: URL) {
self.logDir = directory.appendingPathComponent("logs", isDirectory: true)
super.init()
// 创建日志目录
try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true)
// 创建当天日志文件
updateLogFile()
// 记录初始化完成
self.d(FileLogger.TAG, "文件日志系统已初始化")
}
/**
* 更新日志文件为当天日期
*/
private func updateLogFile() {
let today = dateFormatter.string(from: Date())
let logFile = logDir.appendingPathComponent("log_\(today).txt")
if currentLogFile == nil || currentLogFile?.path != logFile.path {
currentLogFile = logFile
// 如果是新文件,添加文件头
if !FileManager.default.fileExists(atPath: logFile.path) {
let headerDate = DateFormatter()
headerDate.dateFormat = "yyyy-MM-dd HH:mm:ss"
let headerLine = "===== 日志开始: \(headerDate.string(from: Date())) =====\n"
try? headerLine.write(to: logFile, atomically: true, encoding: .utf8)
}
}
}
/**
* 将日志写入文件
*/
private func logToFile(level: String, tag: String, message: String) {
logQueue.async { [weak self] in
guard let self = self else { return }
do {
// 检查是否需要更新日志文件
self.updateLogFile()
guard let logFile = self.currentLogFile,
FileManager.default.fileExists(atPath: logFile.path) else {
return
}
// 格式化日志内容
let timestamp = self.timeFormatter.string(from: Date())
let logLine = "\(timestamp) \(level)/\(tag): \(message)\n"
// 获取文件句柄并追加内容
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
if let data = logLine.data(using: .utf8) {
fileHandle.write(data)
}
fileHandle.closeFile()
} else {
// 如果无法获取文件句柄,尝试直接写入
let existingContent = try? String(contentsOf: logFile, encoding: .utf8)
let newContent = (existingContent ?? "") + logLine
try newContent.write(to: logFile, atomically: true, encoding: .utf8)
}
} catch {
print("写入日志文件失败: \(error.localizedDescription)")
}
}
}
/**
* 调试级别日志
*/
private func d(_ tag: String, _ message: String) {
// 输出到控制台
print("D/\(tag): \(message)")
// 写入文件
logToFile(level: "D", tag: tag, message: message)
}
/**
* 信息级别日志
*/
private func i(_ tag: String, _ message: String) {
// 输出到控制台
print("I/\(tag): \(message)")
// 写入文件
logToFile(level: "I", tag: tag, message: message)
}
/**
* 警告级别日志
*/
private func w(_ tag: String, _ message: String) {
// 输出到控制台
print("W/\(tag): \(message)")
// 写入文件
logToFile(level: "W", tag: tag, message: message)
}
/**
* 错误级别日志
*/
private func e(_ tag: String, _ message: String, _ error: Error? = nil) {
var fullMessage = message
// 如果有错误,添加错误信息
if let error = error {
fullMessage += "\n\(error.localizedDescription)"
}
// 输出到控制台
print("E/\(tag): \(fullMessage)")
// 写入文件
logToFile(level: "E", tag: tag, message: fullMessage)
}
/**
* 清理旧日志文件
*/
private func cleanOldLogs(daysToKeep: Int = 7) {
logQueue.async {
do {
let currentDate = Date()
let fileManager = FileManager.default
let logFiles = try fileManager.contentsOfDirectory(at: self.logDir, includingPropertiesForKeys: [.creationDateKey], options: [])
for fileURL in logFiles where fileURL.pathExtension == "txt" {
if let fileAttributes = try? fileManager.attributesOfItem(atPath: fileURL.path),
let creationDate = fileAttributes[.creationDate] as? Date {
let age = Calendar.current.dateComponents([.day], from: creationDate, to: currentDate)
if let days = age.day, days > daysToKeep {
try? fileManager.removeItem(at: fileURL)
print("D/\(FileLogger.TAG): 已删除旧日志文件: \(fileURL.lastPathComponent)")
}
}
}
} catch {
print("E/\(FileLogger.TAG): 清理旧日志失败: \(error.localizedDescription)")
}
}
}
// MARK: - 公共静态方法
/**
* 初始化日志系统
*/
@objc public static func initialize() {
if instance == nil {
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
instance = FileLogger(directory: documentsDirectory)
}
}
/**
* 初始化日志系统(使用自定义目录)
*/
@objc public static func initialize(withDirectory directory: URL) {
if instance == nil {
instance = FileLogger(directory: directory)
}
}
/**
* 调试日志
*/
@objc public static func d(_ tag: String, _ message: String) {
// 如果实例不存在,先使用默认目录初始化
if instance == nil {
initialize()
}
instance?.d(tag, message)
// 使用系统日志
if #available(iOS 10.0, *) {
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .debug, message)
}
}
/**
* 信息日志
*/
@objc public static func i(_ tag: String, _ message: String) {
if instance == nil {
initialize()
}
instance?.i(tag, message)
// 使用系统日志
if #available(iOS 10.0, *) {
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .info, message)
}
}
/**
* 警告日志
*/
@objc public static func w(_ tag: String, _ message: String) {
if instance == nil {
initialize()
}
instance?.w(tag, message)
// 使用系统日志
if #available(iOS 10.0, *) {
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .default, message)
}
}
/**
* 错误日志
*/
@objc public static func e(_ tag: String, _ message: String, _ error: Error? = nil) {
if instance == nil {
initialize()
}
instance?.e(tag, message, error)
// 使用系统日志
if #available(iOS 10.0, *) {
let errorMessage = error != nil ? "\(message): \(error!.localizedDescription)" : message
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .error, errorMessage)
}
}
/**
* 清理旧日志
*/
@objc public static func cleanOldLogs(daysToKeep: Int = 7) {
instance?.cleanOldLogs(daysToKeep: daysToKeep)
}
/**
* 关闭日志系统
*/
@objc public static func shutdown() {
instance = nil
}
}

11
local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift

@ -1,11 +0,0 @@
import Flutter
import UIKit
// 这个类是为了满足Flutter插件的命名约定
// 实际实现在DeepvoiceUtilsPlugin.swift中
public class SwiftDeepvoiceUtilsPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
// 调用真正的实现
DeepvoiceUtilsPlugin.register(with: registrar)
}
}

54
local_plugins/deepvoice_utils/ios/README.md

@ -1,54 +0,0 @@
# DeepVoice Utils iOS 实现
## 简介
本模块是 `deepvoice_utils` 插件的 iOS 实现部分,主要提供以下功能:
- 文件日志记录(FileLogger):将日志记录到文件,方便调试和问题排查
## 使用说明
### 日志系统
该插件自动初始化日志系统,开发者无需手动调用初始化方法。日志文件默认保存在应用的 Documents 目录下的 logs 文件夹中,按日期分文件存储。
## 配置要求
- iOS 11.0 或更高版本
- Swift 5.0
- 确保应用拥有文件写入权限
## 开发说明
### 文件结构
- `DeepvoiceUtilsPlugin.swift`:插件主类,负责与 Flutter 交互
- `FileLogger.swift`:日志系统实现,提供日志记录功能
- `SwiftDeepvoiceUtilsPlugin.swift`:命名约定桥接类
- `DeepvoiceUtilsPlugin.h/.m`:Objective-C 桥接文件
### 自定义日志目录
如需使用自定义日志目录,可以在应用启动时手动调用初始化方法:
```swift
import deepvoice_utils
// 在 AppDelegate 中
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 自定义日志目录
let customDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("custom_logs")
FileLogger.initialize(withDirectory: customDirectory)
return true
}
```
### 清理旧日志
默认情况下,日志系统不会自动清理旧日志。如果需要清理,可以手动调用:
```swift
// 清理7天前的日志
FileLogger.cleanOldLogs(daysToKeep: 7)
```

19
local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec

@ -1,19 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
#
Pod::Spec.new do |s|
s.name = 'deepvoice_utils'
s.version = '0.0.1'
s.summary = '深语工具库'
s.description = <<-DESC
提供文件日志记录等通用功能的Flutter插件。
DESC
s.homepage = 'http://yunqiinnovation.com'
s.license = { :file => '../LICENSE' }
s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.ios.deployment_target = '11.0'
s.swift_version = '5.0'
end

3
local_plugins/deepvoice_utils/lib/deepvoice_utils.dart

@ -1,3 +0,0 @@
library deepvoice_utils;
export 'src/file_logger.dart';

31
local_plugins/deepvoice_utils/lib/src/file_logger.dart

@ -1,31 +0,0 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
/// 文件日志记录器
///
/// 提供文件日志记录功能,支持简单的日志记录
class FileLogger {
/// 记录调试日志
static void d(String tag, String message) {
debugPrint('[$tag] DEBUG: $message');
}
/// 记录信息日志
static void i(String tag, String message) {
debugPrint('[$tag] INFO: $message');
}
/// 记录警告日志
static void w(String tag, String message) {
debugPrint('[$tag] WARNING: $message');
}
/// 记录错误日志
static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) {
final errorMsg = error != null ? '$message: $error' : message;
debugPrint('[$tag] ERROR: $errorMsg');
if (stackTrace != null) {
debugPrint(stackTrace.toString());
}
}
}

27
local_plugins/deepvoice_utils/pubspec.yaml

@ -1,27 +0,0 @@
name: deepvoice_utils
description: 通用工具库,包含日志、文件处理等功能
version: 0.0.1
environment:
sdk: ">=2.17.0 <4.0.0"
flutter: ">=2.5.0"
dependencies:
flutter:
sdk: flutter
logger: ^2.0.0
path_provider: ^2.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
plugin:
platforms:
android:
package: com.yunqiinnovation.deepvoice_utils
pluginClass: DeepvoiceUtilsPlugin
ios:
pluginClass: DeepvoiceUtilsPlugin

144
local_plugins/jieli/README.md

@ -1,144 +0,0 @@
# Jieli 插件
一个用于连接杰理蓝牙耳机的Flutter插件,支持Android 12+的BLE通信。
## 功能
- 扫描支持特定BLE服务的耳机设备
- 连接设备并发现服务
- 接收耳机的唤醒通知
- 向耳机发送控制命令启动录音
- 接收音频数据流
## 技术规范
此插件实现了以下BLE特性:
- **服务UUID**: 0000A100-0000-1000-8000-00805F9B34FB
- **音频数据特征UUID**: 0000A101-0000-1000-8000-00805F9B34FB
- **控制特征UUID**: 0000A102-0000-1000-8000-00805F9B34FB
## 使用方法
### 初始化插件
```dart
final jieli = Jieli();
```
### 检查蓝牙状态
```dart
bool isEnabled = await jieli.isBluetoothEnabled();
if (!isEnabled) {
await jieli.requestBluetoothEnable();
}
```
### 扫描设备
```dart
// 监听扫描结果
jieli.scanResults.listen((devices) {
// 处理扫描到的设备列表
for (var device in devices) {
print('发现设备: ${device.name} (${device.id}), RSSI: ${device.rssi}');
}
});
// 开始扫描,10秒后自动停止
await jieli.startScan(scanTimeout: 10000);
// 手动停止扫描
await jieli.stopScan();
```
### 连接设备
```dart
// 监听连接状态
jieli.connectionState.listen((state) {
switch (state) {
case ConnectionState.connected:
print('设备已连接');
break;
case ConnectionState.disconnected:
print('设备已断开');
break;
case ConnectionState.connecting:
print('正在连接...');
break;
case ConnectionState.disconnecting:
print('正在断开...');
break;
case ConnectionState.error:
print('连接错误');
break;
}
});
// 连接到设备
await jieli.connect(deviceId);
// 断开连接
await jieli.disconnect();
```
### 接收唤醒信号
```dart
// 监听唤醒信号
jieli.wakeupStream.listen((wakeup) {
print('收到唤醒信号');
// 通常在收到唤醒后自动发送开始录音命令
});
```
### 发送录音命令
```dart
// 发送开始录音命令
await jieli.sendStartRecordingCommand();
// 发送停止录音命令
await jieli.sendStopRecordingCommand();
```
### 接收音频数据
```dart
// 监听音频数据流
jieli.audioDataStream.listen((data) {
print('收到音频数据: ${data.length} 字节');
// 处理音频数据
});
```
### 资源清理
```dart
// 在不再需要插件时释放资源
await jieli.dispose();
```
## 权限
使用此插件需要以下Android权限:
- Android 12+ (API 31+): `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`
- Android 11及以下: `BLUETOOTH`, `BLUETOOTH_ADMIN`
插件会在需要时自动请求权限,但建议在应用清单文件中声明这些权限:
```xml
<!-- 针对 Android 11 及以下的传统蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- Android 12+ 新的 BLE 扫描和连接权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```

1
local_plugins/jieli/android/settings.gradle.kts

@ -1 +0,0 @@
rootProject.name = "jieli"

22
local_plugins/jieli/android/src/main/AndroidManifest.xml

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.jieli">
<!-- 声明需要蓝牙功能 -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<!-- 针对 Android 11 及以下的传统蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- Android 12+ 新的 BLE 扫描和连接权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
</manifest>

665
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt

@ -1,665 +0,0 @@
package com.yunqiinnovation.jieli
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.ParcelUuid
import android.util.Log
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.*
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.jl_opus.JlOpusPlugin
/**
* BLE服务类,提供核心蓝牙功能
*/
class BleService(private val context: Context) {
companion object {
private const val TAG = "JieliBleService"
// 服务和特征UUID - 根据协议规范重新定义
// private const val AUDIO_SERVICE_UUID = "0000faa0-0000-1000-8000-00805f9b34fb" // 主服务UUID
private const val AUDIO_SERVICE_UUID = "0000abc0-0000-1111-2222-123456789abc" // 主服务UUID
private const val NOTIFY_CHAR_UUID =
"0000abc2-0000-1111-2222-123456789abc" // 用于接收数据的特征(notify)
private const val WRITE_CHAR_UUID =
"0000abc1-0000-1111-2222-123456789abc" // 用于发送命令的特征(write without response)
private const val CLIENT_CHAR_CONFIG_UUID =
"00002902-0000-1000-8000-00805f9b34fb" // 标准的客户端特征配置描述符
// 连接状态常量
const val STATE_DISCONNECTED = 0
const val STATE_CONNECTING = 1
const val STATE_CONNECTED = 2
const val STATE_DISCONNECTING = 3
const val STATE_ERROR = 4
// 扫描结果更新间隔(毫秒)
private const val SCAN_RESULT_UPDATE_INTERVAL = 500L
// 命令类型常量
const val COMMAND_VOLUME = 1
}
/**
* 回调接口
*/
interface Callback {
fun onScanResult(devices: List<Map<String, Any>>)
fun onConnectionStateChanged(state: Int)
fun onAudioDataReceived(data: ByteArray)
fun onWakeupSignalReceived()
fun onCommandResult(commandType: Int, success: Boolean)
}
// region 文件相关
private var outputFile: File? = null
private var fos: FileOutputStream? = null
// Azure服务
private var azureAsrHelper: AzureAsrHelper? = null
private var azureTtsHelper: AzureTtsHelper? = null
// 获取插件实例
private val opusPlugin = JlOpusPlugin()
// 蓝牙管理器和适配器
private val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
private val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
// GATT连接和特征
private var bluetoothGatt: BluetoothGatt? = null
private var notifyChar: BluetoothGattCharacteristic? = null
private var writeChar: BluetoothGattCharacteristic? = null
// 扫描相关
private val scanHandler = Handler(Looper.getMainLooper())
private val scanResults = ArrayList<ScanResult>()
private var isScanning = false
private var isPendingScanResultUpdate = false
private val scanResultUpdateRunnable = Runnable {
notifyScanResults()
isPendingScanResultUpdate = false
}
// 连接状态LiveData
val connectionState = MutableLiveData<Int>(STATE_DISCONNECTED)
// 统一回调接口
var callback: Callback? = null
/**
* 开始扫描BLE设备
*/
fun startScan(scanTimeout: Long = 5000): Boolean {
if (bluetoothLeScanner == null || isScanning) {
return false
}
Log.i(TAG, "开始扫描BLE设备...")
scanResults.clear()
val serviceUuid = UUID.fromString(AUDIO_SERVICE_UUID)
val scanFilter = ScanFilter.Builder()
// .setServiceUuid(ParcelUuid(serviceUuid))
.build()
val scanFilters = listOf(scanFilter)
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback)
isScanning = true
// 设置超时停止扫描
scanHandler.postDelayed({
Log.d(
TAG,
"定时器触发,准备停止扫描 [主线程:${Looper.myLooper() == Looper.getMainLooper()}]"
)
stopScan()
}, scanTimeout)
return true
}
/**
* 停止扫描
*/
fun stopScan(): Boolean {
Log.i(TAG, "停止扫描BLE设备")
if (bluetoothLeScanner == null || !isScanning) {
return false
}
bluetoothLeScanner.stopScan(scanCallback)
scanHandler.removeCallbacksAndMessages(null)
isScanning = false
// 最后一次发送扫描结果
notifyScanResults()
return true
}
/**
* 扫描回调
*/
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
val deviceName = device.name ?: "未知设备"
val deviceAddress = device.address
val rssi = result.rssi
val scanRecord = result.scanRecord
if (!deviceName.contains("zxd")) {
return
}
Log.d(TAG, "============发现设备: $deviceName [$deviceAddress], RSSI: $rssi")
// 打印所有发现的UUID以便调试
val serviceUuids = scanRecord?.serviceUuids
if (serviceUuids != null) {
for (uuid in serviceUuids) {
Log.d(TAG, "设备UUID: ${uuid.uuid}")
}
}
// 检查制造商数据
for (i in 0 until 65535) {
val manufacturerData = scanRecord?.getManufacturerSpecificData(i)
if (manufacturerData != null) {
// Log.d(TAG, "发现厂商ID: $i, 数据长度: ${manufacturerData.size}")
// 这里可以根据已知的厂商ID和格式解析数据
}
}
// 避免重复添加
val existingIndex = scanResults.indexOfFirst { it.device.address == deviceAddress }
if (existingIndex >= 0) {
scanResults[existingIndex] = result
} else {
Log.d(TAG, "扫描回调A, scanResults[existingIndex]ADD")
scanResults.add(result)
}
// 如果没有待处理的更新,安排一个
if (!isPendingScanResultUpdate) {
isPendingScanResultUpdate = true
scanHandler.postDelayed(scanResultUpdateRunnable, SCAN_RESULT_UPDATE_INTERVAL)
}
}
override fun onScanFailed(errorCode: Int) {
Log.e(TAG, "扫描失败,错误码: $errorCode")
isScanning = false
}
}
/**
* 将扫描结果通知回调
*/
private fun notifyScanResults() {
val devicesJson = scanResults.map { result ->
mapOf(
"id" to result.device.address,
"name" to (result.device.name ?: "未知设备"),
"rssi" to result.rssi
)
}
callback?.onScanResult(devicesJson)
}
/**
* 连接到指定设备
*/
fun connect(deviceAddress: String) {
if (bluetoothAdapter == null) {
Log.e(TAG, "蓝牙适配器不可用")
updateConnectionState(STATE_ERROR)
return
}
// 先断开现有连接
bluetoothGatt?.close()
bluetoothGatt = null
try {
val device = bluetoothAdapter.getRemoteDevice(deviceAddress)
updateConnectionState(STATE_CONNECTING)
// 连接GATT服务器
Log.i(TAG, "正在连接设备: ${device.name ?: "未知设备"} [$deviceAddress]")
bluetoothGatt =
device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
} catch (e: Exception) {
Log.e(TAG, "连接设备时发生错误: ${e.message}")
updateConnectionState(STATE_ERROR)
}
}
/**
* 断开连接
*/
fun disconnect() {
bluetoothGatt?.disconnect()
updateConnectionState(STATE_DISCONNECTING)
}
/**
* 更新连接状态并通知回调
*/
private fun updateConnectionState(state: Int) {
connectionState.postValue(state)
callback?.onConnectionStateChanged(state)
}
/**
* GATT回调
*/
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "设备已连接,开始发现服务...")
updateConnectionState(STATE_CONNECTED)
// 开始搜索服务
gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.i(TAG, "设备已断开连接")
updateConnectionState(STATE_DISCONNECTED)
// 清理资源
gatt.close()
bluetoothGatt = null
notifyChar = null
writeChar = null
} else if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "连接失败,status=$status")
updateConnectionState(STATE_ERROR)
gatt.close()
bluetoothGatt = null
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "服务发现成功")
// 查找目标服务
val audioService = gatt.getService(UUID.fromString(AUDIO_SERVICE_UUID))
if (audioService != null) {
Log.i(TAG, "找到音频服务,开始遍历特征...")
audioService.characteristics.forEach { char ->
Log.i(TAG, "特征UUID: ${char.uuid}")
}
Log.i(TAG, "找到音频服务$")
// 获取特征
notifyChar = audioService.getCharacteristic(UUID.fromString(NOTIFY_CHAR_UUID))
writeChar = audioService.getCharacteristic(UUID.fromString(WRITE_CHAR_UUID))
if (notifyChar != null && writeChar != null) {
Log.i(TAG, "找到所需特征")
// 设置特征写入类型为无响应模式
writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
// 注册特征通知
setupNotifications(gatt)
opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object :
JlOpusPlugin.CallbackOpus {
override fun onAudioDataReceivedOpus(pcmData: ByteArray) {
// Log.i(TAG, "收到解码控制通知: ${pcmData.contentToString()}")
callback?.onAudioDataReceived(pcmData)
// 处理解码后的 PCM 数据(如播放或写入文件)
}
})
} else {
Log.e(TAG, "未找到所需特征")
updateConnectionState(STATE_ERROR)
}
} else {
Log.e(TAG, "未找到音频服务")
updateConnectionState(STATE_ERROR)
}
} else {
Log.e(TAG, "服务发现失败,status=$status")
updateConnectionState(STATE_ERROR)
}
}
// 添加扩展函数(在文件顶部声明)
fun List<Byte>.toByteArray(): ByteArray {
return ByteArray(this.size) { this[it] }
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
val characteristicUuid = characteristic.uuid.toString()
// Log.d(TAG, "收到特征变化通知: $characteristicUuid")
when (characteristicUuid) {
// NOTIFY_CHAR_UUID -> {
// // 收到音频数据
// val audioData = characteristic.value
// Log.d(TAG, "收到音频数据帧,大小: ${audioData.size} 字节")
// callback?.onAudioDataReceived(audioData)
// }
NOTIFY_CHAR_UUID -> {
// 处理控制消息
val value = characteristic.value
// Log.i(TAG, "收到控制通知: ${value.size}")
// 判断是否收到音频数据
if (value.isNotEmpty() && (value[0].toInt() and 0xFF == 0x4b || value[0].toInt() and 0xFF == 0x48)) {
// 处理设备返回的协议数据
fos?.write(value)
opusPlugin?.handleDecodeRequest(value)
}
// 判断是否是协议格式响应
if (value.isNotEmpty() && value[0].toInt() and 0xFF == 0xA5) {
// 处理设备返回的协议数据
processResponseData(value)
}
// 假设0x01表示唤醒信号
else if (value.isNotEmpty() && value[0].toInt() == 0x01) {
Log.i(TAG, "收到唤醒信号")
callback?.onWakeupSignalReceived()
// 收到唤醒后自动发送开始录音命令
sendStartRecordingCommand()
}
}
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (characteristic.uuid.toString() == WRITE_CHAR_UUID) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "命令发送成功: ${characteristic.value.contentToString()}")
} else {
Log.e(TAG, "命令发送失败,status=$status")
}
}
}
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "描述符写入成功: ${descriptor.uuid}")
Log.i(TAG, "特征通知设置成功,设备就绪")
} else {
Log.e(TAG, "描述符写入失败,status=$status")
updateConnectionState(STATE_ERROR)
}
}
}
/**
* 设置特征通知
*/
private fun setupNotifications(gatt: BluetoothGatt) {
// 启用 notify 特征通知
if (notifyChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
gatt.setCharacteristicNotification(notifyChar, true)
val descriptor = notifyChar?.getDescriptor(UUID.fromString(CLIENT_CHAR_CONFIG_UUID))
descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (descriptor != null) {
gatt.writeDescriptor(descriptor)
Log.i(TAG, "设置数据通知特征通知")
}
} else {
Log.e(TAG, "数据通知特征不支持通知")
}
}
/**
* 处理设备返回的数据
* 协议格式:
* 协议头(0xA5) + 字段类型(1byte) + 数据长度(1byte) + 数据域(N byte) + 校验和(1byte)
*/
private fun processResponseData(data: ByteArray) {
if (data.size < 4) {
Log.e(TAG, "返回数据格式错误,长度不足")
return
}
// 判断协议头
if (data[0].toInt() and 0xFF != 0xA5) {
Log.e(TAG, "返回数据协议头错误: ${data[0].toInt() and 0xFF}")
return
}
val fieldType = data[1].toInt() and 0xFF
val dataLen = data[2].toInt() and 0xFF
// 验证长度
if (data.size != dataLen + 4) { // 协议头+字段类型+数据长度+数据域+校验和
Log.e(TAG, "返回数据长度不匹配: 应为${dataLen + 4},实际为${data.size}")
return
}
// 计算校验和
var checksum = 0
for (i in 0 until data.size - 1) {
checksum += data[i].toInt() and 0xFF
}
checksum = checksum and 0xFF
val receivedChecksum = data[data.size - 1].toInt() and 0xFF
if (checksum != receivedChecksum) {
Log.e(TAG, "校验和错误: 计算值=$checksum, 接收值=$receivedChecksum")
return
}
// 解析字段类型
when (fieldType) {
0x01 -> {
// 音量调整命令的响应
val result = data[3].toInt() and 0xFF
val success = result == 0
Log.i(TAG, "音量调整命令响应: ${if (success) "成功" else "失败"}")
callback?.onCommandResult(COMMAND_VOLUME, success)
}
// 其他命令类型可在此处添加
else -> {
Log.d(TAG, "未知命令类型: ${String.format("0x%02X", fieldType)}")
}
}
}
/**
* 发送开始录音命令
*/
fun sendStartRecordingCommand(): Boolean {
if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) {
Log.e(TAG, "无法发送开始录音命令,设备未连接")
return false
}
try {
val commandData = byteArrayOf(0x01)
writeChar?.value = commandData
// 获取外部存储目录路径
// 生成带时间戳的文件名(格式:yyyyMMdd_HHmmss_audio.opus)
val timeStamp =
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val fileName = "${timeStamp}_audio.opus"
val externalDir = context.getExternalFilesDir(null)
outputFile = File(externalDir, fileName)
fos = FileOutputStream(outputFile).also {
Log.i(TAG, "音频文件创建成功:${outputFile?.absolutePath}")
}
// val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
// val region = call.argument<String>("region") ?: ""
// val supportedLanguages = call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN")
// val success = azureAsrHelper.initialize(subscriptionKey, region, supportedLanguages.toTypedArray())
// result.success(success)
val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false
if (success) {
Log.i(TAG, "开始录音命令已发送")
} else {
Log.e(TAG, "开始录音命令发送失败")
}
return success
} catch (e: Exception) {
Log.e(TAG, "发送开始录音命令时发生异常: ${e.message}")
return false
}
}
/**
* 发送停止录音命令
*/
fun sendStopRecordingCommand(): Boolean {
if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) {
Log.e(TAG, "无法发送停止录音命令,设备未连接")
return false
}
try {
val commandData = byteArrayOf(0x02)
writeChar?.value = commandData
val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false
fos?.close()
if (success) {
Log.i(TAG, "停止录音命令已发送")
} else {
Log.e(TAG, "停止录音命令发送失败")
}
return success
} catch (e: Exception) {
Log.e(TAG, "发送停止录音命令时发生异常: ${e.message}")
return false
}
}
/**
* 发送音量调整命令
* 协议格式:
* 协议头(0xAA) + 字段类型(0x01) + 数据长度(0x01) + 音量值(0~16) + 校验和
*/
fun adjustVolume(volume: Int): Boolean {
Log.e(
TAG,
"adjustVolume,bluetoothGatt=${bluetoothGatt},writeChar=${writeChar},connectionState=${connectionState.value}"
)
if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) {
Log.e(TAG, "无法发送音量调整命令,设备未连接")
return false
}
// 限制音量范围为0-16
val safeVolume = volume.coerceIn(0, 16)
try {
// 构建命令数据
val header: Byte = 0xAA.toByte()
val fieldType: Byte = 0x01
val dataLen: Byte = 0x01
val volumeData: Byte = safeVolume.toByte()
// 计算校验和
val checksum = ((header.toInt() and 0xFF) +
(fieldType.toInt() and 0xFF) +
(dataLen.toInt() and 0xFF) +
(volumeData.toInt() and 0xFF)) and 0xFF
// 创建完整命令
val commandData =
byteArrayOf(header, fieldType, dataLen, volumeData, checksum.toByte())
// 写入特征
writeChar?.value = commandData
val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false
if (success) {
Log.i(TAG, "音量调整命令已发送: $safeVolume")
} else {
Log.e(TAG, "音量调整命令发送失败")
}
return success
} catch (e: Exception) {
Log.e(TAG, "发送音量调整命令时发生异常: ${e.message}")
return false
}
}
/**
* 增加音量
*/
fun increaseVolume(): Boolean {
// 假设当前音量保存在设备中,我们只需发送增加命令
return adjustVolume(16) // 发送最大音量命令
}
/**
* 减小音量
*/
fun decreaseVolume(): Boolean {
// 假设当前音量保存在设备中,我们只需发送减小命令
return adjustVolume(0) // 发送最小音量命令
}
/**
* 清理资源
*/
fun cleanup() {
stopScan()
scanHandler.removeCallbacks(scanResultUpdateRunnable)
bluetoothGatt?.close()
bluetoothGatt = null
notifyChar = null
writeChar = null
}
}

294
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt

@ -1,294 +0,0 @@
package com.yunqiinnovation.jieli
import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.*
/** JieliPlugin */
class JieliPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel.StreamHandler {
/// 方法通道
private lateinit var methodChannel: MethodChannel
/// 统一事件通道
private lateinit var eventChannel: EventChannel
private lateinit var context: Context
private var activity: Activity? = null
/// BLE服务实例
private lateinit var bleService: BleService
// 协程作用域
private val pluginScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 事件接收器
private var eventSink: EventChannel.EventSink? = null
// 事件类型常量
companion object {
private const val EVENT_TYPE_AUDIO_DATA = "audio_data"
private const val EVENT_TYPE_CONNECTION_STATE = "connection_state"
private const val EVENT_TYPE_SCAN_RESULT = "scan_result"
private const val EVENT_TYPE_WAKEUP_SIGNAL = "wakeup_signal"
private const val EVENT_TYPE_COMMAND_RESULT = "command_result"
}
// 蓝牙相关请求码
private val REQUEST_ENABLE_BT = 1
private val REQUEST_PERMISSION_BLE = 2
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
val binaryMessenger = flutterPluginBinding.binaryMessenger
// 设置方法通道
methodChannel = MethodChannel(binaryMessenger, "com.yunqiinnovation.jieli")
methodChannel.setMethodCallHandler(this)
// 设置统一事件通道
eventChannel = EventChannel(binaryMessenger, "com.yunqiinnovation.jieli/events")
eventChannel.setStreamHandler(this)
context = flutterPluginBinding.applicationContext
// 初始化BLE服务
bleService = BleService(context)
// 设置回调
setupBleCallbacks()
}
/**
* 设置BLE服务回调
*/
private fun setupBleCallbacks() {
// 设置统一回调接口
bleService.callback = object : BleService.Callback {
override fun onScanResult(devices: List<Map<String, Any>>) {
pluginScope.launch(Dispatchers.Main) {
sendEvent(EVENT_TYPE_SCAN_RESULT, devices)
}
}
override fun onConnectionStateChanged(state: Int) {
pluginScope.launch(Dispatchers.Main) {
sendEvent(EVENT_TYPE_CONNECTION_STATE, state)
}
}
override fun onAudioDataReceived(data: ByteArray) {
pluginScope.launch(Dispatchers.Main) {
// Log.d("","数据流onAudioDataReceived${data}")
sendEvent(EVENT_TYPE_AUDIO_DATA, data)
}
}
override fun onWakeupSignalReceived() {
pluginScope.launch(Dispatchers.Main) {
sendEvent(EVENT_TYPE_WAKEUP_SIGNAL, true)
}
}
override fun onCommandResult(commandType: Int, success: Boolean) {
pluginScope.launch(Dispatchers.Main) {
sendEvent(EVENT_TYPE_COMMAND_RESULT, mapOf(
"commandType" to commandType,
"success" to success
))
}
}
}
}
/**
* 发送事件到Flutter端
*/
private fun sendEvent(eventType: String, data: Any?) {
eventSink?.success(mapOf(
"type" to eventType,
"data" to data
))
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"startScan" -> {
val scanTimeout = call.argument<Int>("scanTimeout") ?: 10000
if (!hasRequiredPermissions()) {
requestBlePermissions()
result.error("PERMISSION_DENIED", "需要蓝牙权限才能扫描设备", null)
return
}
val success = bleService.startScan(scanTimeout.toLong())
result.success(success)
}
"stopScan" -> {
val success = bleService.stopScan()
result.success(success)
}
"connect" -> {
val deviceId = call.argument<String>("deviceId")
if (deviceId == null) {
result.error("INVALID_ARGUMENT", "设备ID不能为空", null)
return
}
if (!hasRequiredPermissions()) {
requestBlePermissions()
result.error("PERMISSION_DENIED", "需要蓝牙权限才能连接设备", null)
return
}
bleService.connect(deviceId)
result.success(true)
}
"disconnect" -> {
bleService.disconnect()
result.success(true)
}
"sendStartRecordingCommand" -> {
val success = bleService.sendStartRecordingCommand()
result.success(success)
}
"sendStopRecordingCommand" -> {
val success = bleService.sendStopRecordingCommand()
result.success(success)
}
"isBluetoothEnabled" -> {
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter
result.success(bluetoothAdapter?.isEnabled == true)
}
"requestBluetoothEnable" -> {
if (activity == null) {
result.error("ACTIVITY_NOT_AVAILABLE", "Activity不可用,无法请求打开蓝牙", null)
return
}
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter
if (bluetoothAdapter == null) {
result.error("BLUETOOTH_UNAVAILABLE", "设备不支持蓝牙", null)
return
}
if (bluetoothAdapter.isEnabled) {
result.success(true)
return
}
// 请求打开蓝牙
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
activity?.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
result.success(true)
}
"adjustVolume" -> {
val volume = call.argument<Int>("volume") ?: 0
val success = bleService.adjustVolume(volume)
result.success(success)
}
"increaseVolume" -> {
val success = bleService.increaseVolume()
result.success(success)
}
"decreaseVolume" -> {
val success = bleService.decreaseVolume()
result.success(success)
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
bleService.cleanup()
pluginScope.cancel()
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
activity = null
}
/// EventChannel.StreamHandler接口实现
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
/// 检查是否拥有所需权限
private fun hasRequiredPermissions(): Boolean {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
hasPermission(android.Manifest.permission.BLUETOOTH_SCAN) &&
hasPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
} else {
hasPermission(android.Manifest.permission.BLUETOOTH) &&
hasPermission(android.Manifest.permission.BLUETOOTH_ADMIN)
}
}
/// 检查单个权限
private fun hasPermission(permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
/// 请求BLE权限
private fun requestBlePermissions() {
activity?.let {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
ActivityCompat.requestPermissions(
it,
arrayOf(
android.Manifest.permission.BLUETOOTH_SCAN,
android.Manifest.permission.BLUETOOTH_CONNECT
),
REQUEST_PERMISSION_BLE
)
} else {
ActivityCompat.requestPermissions(
it,
arrayOf(
android.Manifest.permission.BLUETOOTH,
android.Manifest.permission.BLUETOOTH_ADMIN
),
REQUEST_PERMISSION_BLE
)
}
}
}
}

283
local_plugins/jieli/lib/jieli.dart

@ -1,283 +0,0 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
/// 设备类,代表一个BLE设备
class BleDevice {
final String id;
final String name;
final int rssi;
BleDevice({required this.id, required this.name, required this.rssi});
factory BleDevice.fromJson(Map<String, dynamic> json) {
return BleDevice(
id: json['id'] as String,
name: json['name'] as String? ?? 'Unknown Device',
rssi: json['rssi'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'rssi': rssi,
};
}
@override
String toString() => 'BleDevice(id: $id, name: $name, rssi: $rssi)';
}
/// 连接状态
enum ConnectionState {
disconnected,
connecting,
connected,
disconnecting,
error
}
/// 事件类型
class EventType {
static const String audioData = 'audio_data';
static const String connectionState = 'connection_state';
static const String scanResult = 'scan_result';
static const String wakeupSignal = 'wakeup_signal';
static const String commandResult = 'command_result';
}
/// 杰理BLE插件主类
class Jieli {
static const MethodChannel _methodChannel =
MethodChannel('com.yunqiinnovation.jieli');
static const EventChannel _eventChannel =
EventChannel('com.yunqiinnovation.jieli/events');
/// 扫描结果流
static final StreamController<List<BleDevice>> _scanController =
StreamController<List<BleDevice>>.broadcast();
Stream<List<BleDevice>> get scanResults => _scanController.stream;
/// 连接状态变化流
static final StreamController<ConnectionState> _connectionController =
StreamController<ConnectionState>.broadcast();
Stream<ConnectionState> get connectionState => _connectionController.stream;
/// 音频数据流
static final StreamController<Uint8List> _audioController =
StreamController<Uint8List>.broadcast();
Stream<Uint8List> get audioDataStream => _audioController.stream;
/// 唤醒信号流
static final StreamController<bool> _wakeupController =
StreamController<bool>.broadcast();
Stream<bool> get wakeupStream => _wakeupController.stream;
/// 命令结果流
static final StreamController<CommandResult> _commandResultController =
StreamController<CommandResult>.broadcast();
Stream<CommandResult> get commandResultStream =>
_commandResultController.stream;
/// 原始事件流
Stream<Map<String, dynamic>>? _eventStream;
/// 事件流订阅
StreamSubscription? _eventSubscription;
/// 命令类型常量
static const int commandTypeVolume = 1;
/// 构造函数,初始化平台通道和事件监听
Jieli() {
_setupEventListener();
}
/// 设置事件监听器
void _setupEventListener() {
_eventSubscription =
_eventChannel.receiveBroadcastStream().listen(_handleEvent);
}
/// 处理来自原生平台的事件
void _handleEvent(dynamic event) {
if (event is! Map<dynamic, dynamic>) return;
final eventData = Map<String, dynamic>.from(event);
final String eventType = eventData['type'] as String;
final dynamic data = eventData['data'];
switch (eventType) {
case EventType.scanResult:
if (data is List<dynamic>) {
final deviceList = data
.map(
(item) => BleDevice.fromJson(Map<String, dynamic>.from(item)))
.toList();
_scanController.add(deviceList);
}
break;
case EventType.connectionState:
if (data is int && data >= 0 && data < ConnectionState.values.length) {
_connectionController.add(ConnectionState.values[data]);
}
break;
case EventType.audioData:
if (data is Uint8List) {
//print('收到ble通道消息: $data');
_audioController.add(data);
}
break;
case EventType.wakeupSignal:
if (data is bool) {
_wakeupController.add(data);
}
break;
case EventType.commandResult:
if (data is Map<dynamic, dynamic>) {
final commandType = data['commandType'] as int;
final success = data['success'] as bool;
_commandResultController.add(CommandResult(
commandType: commandType,
success: success,
));
}
break;
}
}
/// 开始扫描BLE设备
Future<bool> startScan({int scanTimeout = 10000}) async {
final result = await _methodChannel.invokeMethod<bool>(
'startScan',
{'scanTimeout': scanTimeout},
);
return result ?? false;
}
/// 停止扫描BLE设备
Future<bool> stopScan() async {
final result = await _methodChannel.invokeMethod<bool>('stopScan');
return result ?? false;
}
/// 连接到指定设备
Future<bool> connect(String deviceId) async {
final result = await _methodChannel.invokeMethod<bool>(
'connect',
{'deviceId': deviceId},
);
return result ?? false;
}
/// 断开连接
Future<bool> disconnect() async {
final result = await _methodChannel.invokeMethod<bool>('disconnect');
return result ?? false;
}
/// 发送开始录音命令
Future<bool> sendStartRecordingCommand() async {
final result =
await _methodChannel.invokeMethod<bool>('sendStartRecordingCommand');
return result ?? false;
}
/// 发送停止录音命令
Future<bool> sendStopRecordingCommand() async {
final result =
await _methodChannel.invokeMethod<bool>('sendStopRecordingCommand');
return result ?? false;
}
/// 关闭插件,释放资源
Future<void> dispose() async {
await stopScan();
// await disconnect();
// _eventSubscription?.cancel();
// _scanController.close();
// _connectionController.close();
// _audioController.close();
// _wakeupController.close();
// _commandResultController.close();
}
/// 检查蓝牙是否已启用
Future<bool> isBluetoothEnabled() async {
final result =
await _methodChannel.invokeMethod<bool>('isBluetoothEnabled');
return result ?? false;
}
/// 请求打开蓝牙
Future<bool> requestBluetoothEnable() async {
final result =
await _methodChannel.invokeMethod<bool>('requestBluetoothEnable');
return result ?? false;
}
/// 调整音量
/// [volume] 音量值,范围0-16
Future<bool> adjustVolume(int volume) async {
try {
final result = await _methodChannel.invokeMethod<bool>(
'adjustVolume',
{'volume': volume},
);
return result ?? false;
} catch (e) {
print('调整音量失败: $e');
return false;
}
}
/// 增加音量
Future<bool> increaseVolume() async {
try {
final result = await _methodChannel.invokeMethod<bool>('increaseVolume');
return result ?? false;
} catch (e) {
print('增加音量失败: $e');
return false;
}
}
/// 减小音量
Future<bool> decreaseVolume() async {
try {
final result = await _methodChannel.invokeMethod<bool>('decreaseVolume');
return result ?? false;
} catch (e) {
print('减小音量失败: $e');
return false;
}
}
/// 命令结果回调
Stream<CommandResult> get onCommandResult => commandResultStream
.where((event) => event.commandType == commandTypeVolume)
.map((event) => event);
}
/// 蓝牙设备模型
class BluetoothDevice {
// ... existing code ...
}
/// 命令结果
class CommandResult {
final int commandType;
final bool success;
CommandResult({
required this.commandType,
required this.success,
});
@override
String toString() =>
'CommandResult{commandType: $commandType, success: $success}';
}

23
local_plugins/jieli/pubspec.yaml

@ -1,23 +0,0 @@
name: jieli
description: A Flutter plugin for connecting with Jieli BLE earphones
version: 0.0.1
homepage:
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
plugin:
platforms:
android:
package: com.yunqiinnovation.jieli
pluginClass: JieliPlugin

9
local_plugins/jl_ota/android/build.gradle

@ -18,9 +18,6 @@ allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
flatDir {
dirs 'libs'
}
} }
} }
@ -51,8 +48,8 @@ android {
} }
dependencies { dependencies {
implementation(name: 'jl_bt_ota_V1.9.0-release', ext: 'aar') // implementation project(':ble_service')
implementation(project(":ble_service"))
testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0") testImplementation("org.mockito:mockito-core:5.0.0")
} }
@ -68,4 +65,4 @@ android {
} }
} }
} }
} }

BIN
local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.0-release.aar

Binary file not shown.

2
local_plugins/jl_ota/android/settings.gradle

@ -1 +1 @@
rootProject.name = 'jl_ota' rootProject.name = 'jl_ota'

255
local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt

@ -14,32 +14,18 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.annotation.NonNull import androidx.annotation.NonNull
//import com.yunqiinnovation.ble_service.BleService
import com.jieli.jl_bt_ota.constant.ErrorCode;
import com.jieli.jl_bt_ota.constant.StateCode;
import com.jieli.jl_bt_ota.impl.BluetoothOTAManager;
import com.jieli.jl_bt_ota.interfaces.BtEventCallback;
import com.jieli.jl_bt_ota.interfaces.IActionCallback;
import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback;
import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure;
import com.jieli.jl_bt_ota.model.base.BaseError;
import com.jieli.jl_bt_ota.model.response.TargetInfoResponse;
import com.yunqiinnovation.ble_service.BleService
/** JlOtaPlugin */ /** JlOtaPlugin */
class JlOtaPlugin(): FlutterPlugin, MethodCallHandler { class JlOtaPlugin() : FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android /// The MethodChannel that will the communication between Flutter and native Android
/// ///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it /// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity /// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel private lateinit var channel: MethodChannel
private lateinit var context: Context private lateinit var context: Context
private lateinit var eventChannel: EventChannel private lateinit var eventChannel: EventChannel
private var streamSink: EventChannel.EventSink? = null private var streamSink: EventChannel.EventSink? = null
/// BLE服务实例
private var otaManager: OTAManager? = null
companion object { companion object {
private const val TAG = "JlOtaPlugin" private const val TAG = "JlOtaPlugin"
@ -48,10 +34,11 @@ class JlOtaPlugin(): FlutterPlugin, MethodCallHandler {
} }
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "jl_ota") context = flutterPluginBinding.applicationContext
channel.setMethodCallHandler(this) channel = MethodChannel(flutterPluginBinding.binaryMessenger, "jl_ota")
channel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME) eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME)
@ -65,215 +52,25 @@ class JlOtaPlugin(): FlutterPlugin, MethodCallHandler {
} }
}) })
otaManager = OTAManager(context)
// otaManager.getBluetoothOption().setFirmwareFilePath(filePath);
// otaManager.startOTA(new IUpgradeCallback() {
// }
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
val bluetoothOption = BluetoothOTAConfigure()
//选择通讯方式
bluetoothOption.priority = BluetoothOTAConfigure.PREFER_BLE
//是否需要自定义回连方式(默认不需要,如需要自定义回连方式,需要客户自行实现)
bluetoothOption.isUseReconnect = true
//是否启用设备认证流程(与固件工程师确认)
bluetoothOption.isUseAuthDevice = true
//设置BLE的MTU
bluetoothOption.mtu = 500
//是否需要改变BLE的MTU
bluetoothOption.isNeedChangeMtu = false
//是否启用杰理服务器(暂时不支持)
bluetoothOption.isUseJLServer = false
Log.d(TAG, "bluetoothOTAManager初始化成功")
//配置OTA参数
otaManager?.configure(bluetoothOption)
var filePath= "/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files/downloads/ota_upgrades/1.ufw"
otaManager?.bluetoothOption?.firmwareFilePath = filePath
Log.d(TAG, "OpusManager路径成功${filePath}")
// Add ?. after bluetoothOption ^
otaManager?.startOTA(
CustomUpdateCallback(
context, null, this
)
)
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
} }
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onMethodCall(call: MethodCall, result: Result) {
channel.setMethodCallHandler(null)
}
}
class OTAManager(context: Context) : BluetoothOTAManager(context) {
override fun getConnectedDevice(): BluetoothDevice? {
// 这里可以添加空实现的注释说明
return null
}
override fun getConnectedBluetoothGatt(): BluetoothGatt? {
// 这里可以添加空实现的注释说明
return null
}
override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) {
// 这里可以添加空实现的注释说明
} if (call.method == "getPlatformVersion") {
override fun disconnectBluetoothDevice
(bluetoothDevice: BluetoothDevice?) {
// 这里可以添加空实现的注释说明
} // BleService.otaManager?.configureOTA()
/** 数据发送方法(协议无关) */
override fun sendDataToDevice(
bluetoothDevice: BluetoothDevice?,
bytes: ByteArray?
): Boolean {return true}
}
private class CustomUpdateCallback( // BleService.otaManager?.startOTA()
val context: Context, result.success("Android ${android.os.Build.VERSION.RELEASE}")
val device: BluetoothDevice?, } else {
val viewModel: JlOtaPlugin result.notImplemented()
) : IUpgradeCallback{
companion object {
private const val TAG = "JlOtaPlugin"
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota"
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events"
}
override fun onStartOTA() {
Log.d(TAG, "onStartOTA开始升级")
// viewModel.otaStateMLD.value = OTAStart(device)
}
override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) {
Log.d(TAG, "onNeedReconnect需要重新链接")
// viewModel.otaStateMLD.value = OTAReconnect(device, addr, isNewReconnectWay)
// if (viewModel.isUseReconnectWay()) {
// viewModel.reconnectDev(addr, isNewReconnectWay)
// }
}
override fun onProgress(type: Int, progress: Float) {
Log.d(TAG, "onProgress正在进展")
//viewModel.otaStateMLD.value = OTAWorking(device, type, progress)
}
override fun onStopOTA() {
Log.d(TAG, "onStartOTA停止升级")
// viewModel.otaStateMLD.value =
// OTAEnd(device, ErrorCode.ERR_NONE, context.getString(R.string.ota_complete))
// val handler = Handler(Looper.getMainLooper())
// handler.postDelayed({
// // Log.d("ZHM", "onStopOTA: disconnectDevice connected : "+viewModel.bluetoothHelper.isConnected()+ " getConnectedDevice: "+viewModel.bluetoothHelper.getConnectedDevice())
// //变地址导致无法判断device是否一致
// if (viewModel.bluetoothHelper.isConnected()/* && BluetoothUtil.deviceEquals(
// device,
// viewModel.bluetoothHelper.getConnectedDevice()
// )*/
// ) {
// Log.d("ZHM", "onStopOTA: disconnectDevice")
// viewModel.bluetoothHelper.disconnectDevice(viewModel.otaManager.connectedDevice)
// }
// }, 500)
}
override fun onCancelOTA() {
Log.d(TAG, "onCancelOTA取消升级")
// viewModel.otaStateMLD.value =
// OTAEnd(
// device,
// ErrorCode.ERR_UNKNOWN,
// context.getString(R.string.ota_upgrade_cancel)
// )
}
override fun onError(error: BaseError?) {
Log.d(TAG, "onError升级错误")
// error?.let {
// viewModel.otaStateMLD.value = OTAEnd(device, it.subCode, it.message)
// }
// viewModel.bluetoothHelper.disconnectDevice(viewModel.otaManager.connectedDevice)//升级失败,断开设备,方便自动化继续升级
} }
} }
// class OtaDemo {
// private val otaManager = OTAManager(MainApplication.getInstance())
// fun initOTA(context: Context, firmwarePath: String) {
// val otaManager = JlOtaPlugin()
// val bluetoothOption = BluetoothOTAConfigure.createDefault().apply {
// priority = BluetoothOTAConfigure.PREFER_BLE
// useAuthDevice = true
// bleIntervalMs = 500
// timeoutMs = 3000
// mtu = 500
// needChangeMtu = false
// useReconnect = false
// setFirmwareFilePath(firmwarePath)
// }
// otaManager.configure(bluetoothOption)
// }
// fun otaFirmware(filePath: String) { // 方法名改为小写开头 override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
// val otaManager = JlOtaPlugin() channel.setMethodCallHandler(null)
}
// otaManager.registerBluetoothCallback(object : BtEventCallback {
// override fun onConnection(device: BluetoothDevice, status: Int) {
// if (status == StateCode.CONNECTION_OK) {
// if (otaManager.isOTA) return
// otaManager.queryMandatoryUpdate(object : IActionCallback<TargetInfoResponse> {
// override fun onSuccess(deviceInfo: TargetInfoResponse) {
// // 处理设备信息
// deviceInfo.versionCode
// deviceInfo.versionName
// deviceInfo.projectCode
// deviceInfo.uid
// deviceInfo.pid
// }
// override fun onError(baseError: BaseError) {
// if (baseError.code == ErrorCode.ERR_NONE &&
// baseError.subCode == ErrorCode.ERR_NONE) {
// val deviceInfo = otaManager.deviceInfo
// deviceInfo?.let {
// it.versionCode
// it.versionName
// it.projectCode
// it.uid
// it.pid
// }
// }
// }
// })
// }
// }
// })
// otaManager.bluetoothOption?.setFirmwareFilePath(filePath) }
// otaManager.startOTA(object : IUpgradeCallback {
// override fun onStartOTA() = Unit
// override fun onNeedReconnect(addr: String, isNewReconnectWay: Boolean) {
// if (otaManager.bluetoothOption?.useReconnect == true) {
// // 处理自定义回连
// }
// }
// override fun onProgress(type: Int, progress: Float) = Unit
// override fun onStopOTA() = Unit
// override fun onCancelOTA() = Unit
// override fun onError(error: BaseError) = Unit
// })
// otaManager.release()
// }
//}

1
local_plugins/music_service/android/build.gradle

@ -66,6 +66,5 @@ android {
} }
dependencies { dependencies {
implementation project(':deepvoice_utils')
implementation("androidx.media:media:1.6.0") implementation("androidx.media:media:1.6.0")
} }

25
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -17,7 +17,7 @@ import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media.MediaBrowserServiceCompat import androidx.media.MediaBrowserServiceCompat
import androidx.media.session.MediaButtonReceiver import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -46,16 +46,15 @@ class MusicService : MediaBrowserServiceCompat() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FileLogger.init(this)
isRunning.set(true) isRunning.set(true)
setupMediaSession() setupMediaSession()
startForeground(NOTIFICATION_ID, createNotification("等待播放","DeepSound","")) startForeground(NOTIFICATION_ID, createNotification("等待播放","DeepSound",""))
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
FileLogger.i(TAG, "MediaService 启动完成") Log.i(TAG, "MediaService 启动完成")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
FileLogger.i(TAG, "onStartCommand: $intent") Log.i(TAG, "onStartCommand: $intent")
intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let { intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
// 分发给 MediaSession 处理媒体按钮 // 分发给 MediaSession 处理媒体按钮
MediaButtonReceiver.handleIntent(mediaSession, intent) MediaButtonReceiver.handleIntent(mediaSession, intent)
@ -107,10 +106,10 @@ class MusicService : MediaBrowserServiceCompat() {
) )
// 回调中处理具体操作 // 回调中处理具体操作
setCallback(object : MediaSessionCompat.Callback() { setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() { FileLogger.i(TAG, "onPlay 通知!"); resume()} override fun onPlay() { Log.i(TAG, "onPlay 通知!"); resume()}
override fun onPause() { FileLogger.i(TAG, "onPause 通知!"); pause()} override fun onPause() { Log.i(TAG, "onPause 通知!"); pause()}
override fun onSkipToNext(){ FileLogger.i(TAG, "onSkipToNext 通知!"); playNext()} override fun onSkipToNext(){ Log.i(TAG, "onSkipToNext 通知!"); playNext()}
override fun onSkipToPrevious() { FileLogger.i(TAG, "onSkipToNext 通知!"); playPrevious()} override fun onSkipToPrevious() { Log.i(TAG, "onSkipToNext 通知!"); playPrevious()}
}) })
// 初始播放状态 // 初始播放状态
setPlaybackState( setPlaybackState(
@ -221,7 +220,7 @@ class MusicService : MediaBrowserServiceCompat() {
private fun play(song: Map<String,String>) { private fun play(song: Map<String,String>) {
val url = song["url"] ?: return val url = song["url"] ?: return
FileLogger.i(TAG, "开始播放: $url") Log.i(TAG, "开始播放: $url")
val title = song["title"] ?: "未知标题" val title = song["title"] ?: "未知标题"
val artist = song["artist"] ?: "未知歌手" val artist = song["artist"] ?: "未知歌手"
val coverUrl = song["coverUrl"] val coverUrl = song["coverUrl"]
@ -234,11 +233,11 @@ class MusicService : MediaBrowserServiceCompat() {
setOnPreparedListener { setOnPreparedListener {
start() start()
updateNotification(title,artist,coverUrl) updateNotification(title,artist,coverUrl)
FileLogger.i(TAG, "开始播放: $url") Log.i(TAG, "开始播放: $url")
} }
setOnCompletionListener { playNextOrStop() } setOnCompletionListener { playNextOrStop() }
setOnErrorListener { _, what, extra -> setOnErrorListener { _, what, extra ->
FileLogger.e(TAG, "播放错误 what=$what extra=$extra") Log.e(TAG, "播放错误 what=$what extra=$extra")
true true
} }
prepareAsync() prepareAsync()
@ -261,7 +260,7 @@ class MusicService : MediaBrowserServiceCompat() {
val coverUrl = song["coverUrl"] val coverUrl = song["coverUrl"]
updateNotification(title,artist,coverUrl) updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PLAYING); setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
FileLogger.i(TAG, "已暂停") Log.i(TAG, "已暂停")
} }
} }
@ -281,7 +280,7 @@ class MusicService : MediaBrowserServiceCompat() {
val coverUrl = song["coverUrl"] val coverUrl = song["coverUrl"]
updateNotification(title,artist,coverUrl) updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED); setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
// FileLogger.i(TAG, "继续播放") // Log.i(TAG, "继续播放")
} }
} }

4
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

@ -3,7 +3,7 @@ package com.yunqiinnovation.music_service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.music_service.MusicServiceStarter
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -24,7 +24,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
FileLogger.d(TAG, "Method call: ${call.method}") Log.d(TAG, "Method call: ${call.method}")
when (call.method) { when (call.method) {
"play" -> { "play" -> {
val song = call.argument<Map<String, String>>("song") val song = call.argument<Map<String, String>>("song")

49
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt

@ -586,11 +586,17 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
return return
} }
try { // 验证JSON是否完整(至少检查大括号匹配)
handleStreamJson(JSONObject(data), toolCalls, callback, messages) if (isValidJson(data)) {
} catch (e: Exception) { try {
// 忽略解析错误,但记录日志 handleStreamJson(JSONObject(data), toolCalls, callback, messages)
Log.e(TAG, "解析JSON出错: ${e.message}", e) } catch (e: Exception) {
// 忽略解析错误,但记录日志
Log.e(TAG, "解析JSON出错: ${e.message}", e)
}
} else {
// 记录不完整的JSON,但不尝试解析它
Log.d(TAG, "收到不完整的JSON数据,跳过处理, data=$data")
} }
} }
} }
@ -879,6 +885,39 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
fun onFunctionCall(functionCall: JSONObject) fun onFunctionCall(functionCall: JSONObject)
fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject)
} }
/**
* 检查字符串是否为有效的JSON
* 使用简单的括号匹配和引号检查来确定JSON是否完整
*/
private fun isValidJson(jsonStr: String): Boolean {
if (jsonStr.isEmpty()) return false
if (!jsonStr.startsWith("{") || !jsonStr.endsWith("}")) return false
var braceCount = 0
var insideQuotes = false
var escapeNext = false
for (char in jsonStr) {
if (escapeNext) {
escapeNext = false
continue
}
when (char) {
'\\' -> escapeNext = true
'"' -> if (!escapeNext) insideQuotes = !insideQuotes
'{' -> if (!insideQuotes) braceCount++
'}' -> if (!insideQuotes) braceCount--
}
// 如果括号不匹配(闭合太多),提前返回
if (braceCount < 0) return false
}
// 所有引号都应该闭合,所有括号也应该匹配
return braceCount == 0 && !insideQuotes
}
} }
/** /**

173
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt

@ -18,6 +18,10 @@ import org.json.JSONObject
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume import kotlin.coroutines.resume
import android.view.KeyEvent import android.view.KeyEvent
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
/** /**
* 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能 * 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能
*/ */
@ -878,10 +882,10 @@ class SystemFunctionHandler(private val context: Context) {
} }
/** /**
* 获取当前位置 * 获取当前位置(简化版,只获取最后已知位置)
* @return 操作结果 * @return 操作结果
*/ */
suspend fun getCurrentLocation(): Result = suspendCancellableCoroutine { continuation -> suspend fun getCurrentLocation(): Result = withContext(Dispatchers.Main) {
Log.d(TAG, "开始获取当前位置") Log.d(TAG, "开始获取当前位置")
try { try {
@ -901,13 +905,10 @@ class SystemFunctionHandler(private val context: Context) {
if (!hasFineLocationPermission && !hasCoarseLocationPermission) { if (!hasFineLocationPermission && !hasCoarseLocationPermission) {
Log.d(TAG, "没有位置权限") Log.d(TAG, "没有位置权限")
return@withContext Result(
// 如果没有权限,返回需要授权的结果
continuation.resume(Result(
success = false, success = false,
message = "需要位置权限才能获取当前位置" message = "需要位置权限才能获取当前位置"
)) )
return@suspendCancellableCoroutine
} }
// 检查位置服务是否开启 // 检查位置服务是否开启
@ -915,153 +916,49 @@ class SystemFunctionHandler(private val context: Context) {
!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { !locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
Log.d(TAG, "位置服务未启用") Log.d(TAG, "位置服务未启用")
return@withContext Result(
// 提醒用户打开位置服务
try {
val settingsIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(settingsIntent)
} catch (e: Exception) {
Log.e(TAG, "无法打开位置设置: ${e.message}")
}
continuation.resume(Result(
success = false, success = false,
message = "位置服务未启用,请打开GPS或网络定位" message = "位置服务未启用,请打开GPS或网络定位"
)) )
return@suspendCancellableCoroutine
} }
// 尝试先获取最后已知位置 // 获取最后已知位置
var bestLocation: Location? = null var bestLocation: Location? = null
try { // 尝试获取网络位置
// 检查网络提供者最后已知位置
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
val networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
if (networkLocation != null) {
bestLocation = networkLocation
Log.d(TAG, "获取到网络位置: ${networkLocation.latitude}, ${networkLocation.longitude}")
}
}
// 检查GPS提供者最后已知位置
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
val gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (gpsLocation != null && (bestLocation == null || gpsLocation.accuracy < bestLocation.accuracy)) {
bestLocation = gpsLocation
Log.d(TAG, "获取到GPS位置: ${gpsLocation.latitude}, ${gpsLocation.longitude}")
}
}
} catch (e: Exception) {
Log.e(TAG, "获取最后已知位置失败: ${e.message}")
}
// 如果有最后已知位置,直接返回结果
if (bestLocation != null) {
val message = "当前位置: 经度 ${bestLocation.longitude}, 纬度 ${bestLocation.latitude}"
Log.d(TAG, message)
continuation.resume(Result(
success = true,
message = message
))
return@suspendCancellableCoroutine
}
// 如果没有最后已知位置,尝试请求更新位置(设置10秒超时)
val locationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
// 收到位置更新
val message = "当前位置: 经度 ${location.longitude}, 纬度 ${location.latitude}"
Log.d(TAG, "位置更新: $message")
locationManager.removeUpdates(this)
if (continuation.isActive) {
continuation.resume(Result(
success = true,
message = message
))
}
}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
// 不需要处理
}
override fun onProviderEnabled(provider: String) {
// 不需要处理
}
override fun onProviderDisabled(provider: String) {
// 如果提供者被禁用,检查是否还有其他可用提供者
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) &&
!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.removeUpdates(this)
if (continuation.isActive) {
continuation.resume(Result(
success = false,
message = "位置服务已被禁用"
))
}
}
}
}
// 注册位置更新请求
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.requestLocationUpdates( val networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
LocationManager.NETWORK_PROVIDER, if (networkLocation != null) {
0L, bestLocation = networkLocation
0f, Log.d(TAG, "获取到网络位置: ${networkLocation.latitude}, ${networkLocation.longitude}")
locationListener }
)
} }
// 尝试获取GPS位置(如果更精确)
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locationManager.requestLocationUpdates( val gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
LocationManager.GPS_PROVIDER, if (gpsLocation != null && (bestLocation == null || gpsLocation.accuracy < bestLocation.accuracy)) {
0L, bestLocation = gpsLocation
0f, Log.d(TAG, "获取到GPS位置: ${gpsLocation.latitude}, ${gpsLocation.longitude}")
locationListener
)
}
// 设置超时取消
continuation.invokeOnCancellation {
try {
locationManager.removeUpdates(locationListener)
} catch (e: Exception) {
Log.e(TAG, "取消位置更新失败: ${e.message}")
} }
} }
// 启动超时处理 // 返回结果
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ return@withContext if (bestLocation != null) {
if (continuation.isActive) { val message = "当前位置: 经度 ${bestLocation.longitude}, 纬度 ${bestLocation.latitude}"
try { Log.d(TAG, message)
locationManager.removeUpdates(locationListener) Result(success = true, message = message)
continuation.resume(Result( } else {
success = false, Log.d(TAG, "无法获取位置信息")
message = "获取位置超时,请稍后重试" Result(success = false, message = "无法获取位置信息,请稍后重试")
)) }
} catch (e: Exception) {
Log.e(TAG, "超时处理失败: ${e.message}")
}
}
}, 10000L) // 10秒超时
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "获取位置过程中发生异常: ${e.message}") Log.e(TAG, "获取位置过程中发生异常: ${e.message}")
if (continuation.isActive) { return@withContext Result(
continuation.resume(Result( success = false,
success = false, message = "获取位置失败: ${e.message}"
message = "获取位置失败: ${e.message}" )
))
}
} }
} }

BIN
local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock

Binary file not shown.

BIN
local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

2
local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

@ -1,2 +1,2 @@
#Sun May 11 13:39:53 IST 2025 #Fri May 16 09:26:08 IST 2025
gradle.version=8.10 gradle.version=8.10

27
local_plugins/jieli/android/build.gradle.kts → local_plugins/speech/android/build.gradle.kts

@ -4,11 +4,12 @@ plugins {
// Kotlin Android 插件 // Kotlin Android 插件
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1" id("com.github.johnrengelman.shadow") version "8.1.1"
kotlin("plugin.serialization") version "1.9.24"
} }
android { android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致) // 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.jieli" namespace = "com.deep_voice.speech"
// 目标 SDK 版本 // 目标 SDK 版本
compileSdk = 35 compileSdk = 35
@ -46,12 +47,18 @@ android {
} }
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("androidx.core:core-ktx:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0") implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("androidx.lifecycle:lifecycle-service:2.4.0") implementation("com.squareup.okhttp3:logging-interceptor:4.9.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") implementation("com.anthropic:anthropic-java:0.8.0")
implementation(project(":azure_speech")) implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
implementation(project(":jl_opus")) implementation("io.ktor:ktor-client-core:3.1.2")
} implementation("io.ktor:ktor-client-cio:3.1.2")
implementation("io.ktor:ktor-client-okhttp:3.1.2")
implementation("io.ktor:ktor-client-logging:3.1.2")
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("androidx.media:media:1.6.0")
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save