Browse Source

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

weicu
lxm 12 months ago
parent
commit
9e450e9f22
  1. 2
      android/app/build.gradle.kts
  2. 2
      android/gradle/wrapper/gradle-wrapper.properties
  3. 4
      android/settings.gradle.kts
  4. 1
      lib/core/translations/language/en_us.dart
  5. 2
      lib/core/translations/language/zh_cn.dart
  6. 2
      lib/data/models/appconfig_model.dart
  7. 8
      lib/data/models/appconfig_model.g.dart
  8. 33
      lib/data/services/ble_manager.dart
  9. 63
      lib/modules/agent/controllers/agent_controller.dart
  10. 67
      lib/modules/agent/views/agent_view.dart
  11. 20
      lib/modules/agent/views/message_bubble.dart
  12. 2
      lib/modules/opus_test/controllers/opus_test_controller.dart
  13. 40
      lib/modules/pairing/controllers/pairing_controller.dart
  14. 73
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  15. 12
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  16. 1
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  17. 400
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  18. 199
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  19. 12
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  20. 11
      local_plugins/agent_service/lib/agent_service.dart
  21. 1
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  22. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  23. 1
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  24. 6
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  25. BIN
      local_plugins/ble_service/android/libs/jl_audio_decode_V2.0.0_20005bat.aar
  26. BIN
      local_plugins/ble_service/android/libs/jl_audio_decode_V2.1.0_20013-release.aar
  27. 17
      local_plugins/ble_service/android/src/main/AndroidManifest.xml
  28. 700
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt
  29. 761
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  30. 72
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt
  31. 1589
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  32. 57
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  33. 56
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt
  34. 11
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt
  35. 506
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt
  36. 160
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/RecordingFile.kt
  37. 20
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
  38. 2
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift
  39. 2
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift
  40. 2
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  41. 2
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
  42. 2
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
  43. 70
      local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift
  44. 2
      pubspec.yaml

2
android/app/build.gradle.kts

@ -24,7 +24,7 @@ val keystoreProperties =Properties().apply {
android {
namespace = "com.saitong.voitrans"
compileSdk = 35
compileSdk = 36
ndkVersion = "28.0.13004108"
compileOptions {

2
android/gradle/wrapper/gradle-wrapper.properties

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

4
android/settings.gradle.kts

@ -25,8 +25,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader")
id("com.android.application") version "8.8.0" apply false
id("com.android.library") version "8.8.0" apply false
id("com.android.application") version "8.9.1" apply false
id("com.android.library") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
}

1
lib/core/translations/language/en_us.dart

@ -540,6 +540,7 @@ const Map<String, String> enUS = {
"loadMoreMessages": "Load more messages...",
"loadFailedClickRetry": "Load failed, click to retry",
"swipeUpForMore": "Swipe up for more",
"callingTool": "I'm helping you, please wait!", // 正在帮您,请稍等!
// 预设提示词
"presetMusic": "Play some music", // Play some music

2
lib/core/translations/language/zh_cn.dart

@ -511,7 +511,7 @@ const Map<String, String> zhCN = {
"loadMoreMessages": "加载更多消息...",
"loadFailedClickRetry": "加载失败,点击重试",
"swipeUpForMore": "向上滑动查看更多",
"callingTool": "正在调用工具!",
"callingTool": "正在帮您,请稍等!",
// 预设提示词
"presetMusic": "来点音乐", // 来点音乐

2
lib/data/models/appconfig_model.dart

@ -79,6 +79,7 @@ class DBProduct {
final String? productimage;
final String scanuuid;
final String? version;
final bool? iscontinuouschat;
final String? updatedescription;
final String? updatepackageaddress;
@ -90,6 +91,7 @@ class DBProduct {
required this.productimage,
required this.scanuuid,
required this.version,
required this.iscontinuouschat,
required this.updatedescription,
required this.updatepackageaddress,
});

8
lib/data/models/appconfig_model.g.dart

@ -79,6 +79,7 @@ DBProduct _$DBProductFromJson(Map<String, dynamic> json) => DBProduct(
productimage: json['productimage'] as String?,
scanuuid: json['scanuuid'] as String,
version: json['version'] as String?,
iscontinuouschat: json['iscontinuouschat'] as bool?,
updatedescription: json['updatedescription'] as String?,
updatepackageaddress: json['updatepackageaddress'] as String?,
);
@ -86,5 +87,12 @@ DBProduct _$DBProductFromJson(Map<String, dynamic> json) => DBProduct(
Map<String, dynamic> _$DBProductToJson(DBProduct instance) => <String, dynamic>{
'id': instance.id,
'factoryid': instance.factoryid,
'devicetype': instance.devicetype,
'productimage': instance.productimage,
'scanuuid': instance.scanuuid,
'version': instance.version,
'iscontinuouschat': instance.iscontinuouschat,
'devicename': instance.devicename,
'updatedescription': instance.updatedescription,
'updatepackageaddress': instance.updatepackageaddress,
};

33
lib/data/services/ble_manager.dart

@ -6,6 +6,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:get/get.dart';
import 'package:ble_service/ble_service.dart';
import 'package:agent_service/agent_service.dart';
import 'package:voitrans/data/models/appconfig_model.dart';
import '../../data/models/appconfig.dart';
import '../../core/utils/logger.dart';
@ -639,6 +640,35 @@ class BleManager extends GetxService {
}
}
String getDeviceName() {
if (deviceInfo.value != null) {
return deviceInfo.value!.name;
}
return "";
}
// 加载TTS偏好设置
Future<bool> _loadTtsInterrupe() async {
try {
if (!_storage.hasData("agent_tts_interrupe")) {
// 获取设备类型以确定检查策略
String deviceName = getDeviceName();
DBProduct? product = AppConfig.getproduct(deviceName);
if (deviceName != "" && product != null) {
// Logger.d('AgentController',
// 'liwe----------product $deviceName ${product.toJson()}');
return !product.iscontinuouschat!;
} else {
return false;
}
} else {
return _storage.read('agent_tts_interrupe') ?? false;
}
} catch (e) {
return false;
}
}
/// 启动BLE服务
Future<bool> startAgentService() async {
try {
@ -668,6 +698,8 @@ class BleManager extends GetxService {
// 从Flutter资源包中加载.mcp.json
final jsonData = AppConfig.mcpConfig();
Logger.info('启动BLE服务,musicServiceType: ${musicServiceType}');
final isTtsInterruptEnabled = await _loadTtsInterrupe();
// 根据APP语言动态配置语音识别语言
List<String> supportedLanguages = [];
try {
@ -737,6 +769,7 @@ class BleManager extends GetxService {
'supportedLanguages': supportedLanguages,
'musicServiceType': musicServiceType,
"navigationMode": navigationMode,
"isttsinterrupt": isTtsInterruptEnabled ? "true" : "false",
"callingTool": "callingTool".tr
};

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

@ -6,6 +6,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:uuid/uuid.dart';
import 'package:voitrans/data/models/appconfig.dart';
import 'package:voitrans/data/models/appconfig_model.dart';
import 'package:voitrans/data/models/song_model.dart';
import 'package:voitrans/data/services/ble_manager.dart';
import 'package:voitrans/data/services/bluetooth_manager.dart';
@ -40,6 +41,7 @@ enum VoiceInputMode {
class AgentController extends GetxController with WidgetsBindingObserver {
static AgentController get to => Get.find(); //单例访问模式
final GetStorage _storage = GetStorage();
// 聊天消息列表
final messages = <Message>[].obs;
final currsessionid = ''.obs;
@ -77,7 +79,10 @@ class AgentController extends GetxController with WidgetsBindingObserver {
final isProcessing = false.obs;
// TTS启用状态
final isTtsEnabled = true.obs;
// TTS播报打断启用状态
final isTtsInterruptEnabled = false.obs;
// 是否显示工具调用
final isShowToolInterruptEnabled = false.obs;
// 图片处理状态
final isImageProcessing = false.obs;
@ -162,6 +167,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
super.onInit();
_initAppDir();
_loadTtsPreference(); //加载TTS偏好设置
_loadTtsInterrupe(); //加载TTS播报打断偏好设置
_setupScrollListener(); //设置滚动监听器
_loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件
@ -257,6 +263,50 @@ class AgentController extends GetxController with WidgetsBindingObserver {
}
}
// 加载TTS偏好设置
Future<void> _loadTtsInterrupe() async {
try {
if (!_storage.hasData("agent_tts_interrupe")) {
// 获取设备类型以确定检查策略
String deviceName = getDeviceName();
DBProduct? product = AppConfig.getproduct(deviceName);
if (deviceName != "" && product != null) {
// Logger.d('AgentController',
// 'liwe----------product $deviceName ${product.toJson()}');
isTtsInterruptEnabled.value = !product.iscontinuouschat!;
} else {
isTtsInterruptEnabled.value = false;
}
} else {
isTtsInterruptEnabled.value =
_storage.read('agent_tts_interrupe') ?? false;
}
await AgentService.isInterrupt(isTtsInterruptEnabled.value);
} catch (e) {
// 如果读取失败,使用默认值
Logger.e(TAG, '加载TTS偏好设置失败: $e');
isTtsInterruptEnabled.value = false;
}
}
/// 切换TTS播报打断状态
/// 当启用播报打断时,用户说话会自动停止当前的TTS播放
Future<void> toggleTtsInterrupt() async {
isTtsInterruptEnabled.toggle();
await _storage.write('agent_tts_interrupe', isTtsInterruptEnabled.value);
// 保存偏好设置
try {
await AgentService.isInterrupt(isTtsInterruptEnabled.value);
} catch (e) {
Logger.e(TAG, '保存TTS播报打断偏好设置失败: $e');
}
}
/// 当启用显示工具调用效果
Future<void> toggleShowAIToolInterrupt() async {
isShowToolInterruptEnabled.toggle();
}
// 设置文本输入监听器
void _setupTextInputListener() {
textController.addListener(() {
@ -530,7 +580,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
final sessionid = event.data['sessionid'] ?? '';
final text = event.data['text'] ?? '';
currentText.value = text; // 保留当前文本,以便其他地方使用
Logger.i(TAG, '识别中间结果: $text');
Logger.i(TAG, '[$sessionid]识别中间结果: $text');
if (text.isNotEmpty) {
// 查找是否有正在识别中的消息
final index = messages.lastIndexWhere(
@ -562,7 +612,7 @@ class AgentController extends GetxController with WidgetsBindingObserver {
case AgentServiceEventType.recognitionResult:
final sessionid = event.data['sessionid'] ?? '';
final text = event.data['text'] ?? '';
Logger.i(TAG, '识别最终结果: $text');
Logger.i(TAG, '[$sessionid]识别最终结果: $text');
// 查找是否有正在识别中的消息
final index = messages.lastIndexWhere(
(msg) => msg.sessionid == sessionid && msg.isUser);
@ -2765,4 +2815,11 @@ class AgentController extends GetxController with WidgetsBindingObserver {
message.isToolCallsExpanded = isExpanded;
messages.refresh();
}
String getDeviceName() {
if (_bluetoothManager.currentDevice != null) {
return _bluetoothManager.currentDevice!.name;
}
return "";
}
}

67
lib/modules/agent/views/agent_view.dart

@ -172,6 +172,73 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
),
color: isDarkMode ? Colors.grey[800] : Colors.white,
itemBuilder: (context) => [
// 播报打断开关选项
PopupMenuItem(
child: Obx(() => Row(
children: [
Icon(
controller.isShowToolInterruptEnabled.value
? Icons.build_circle
: Icons.build_circle_outlined,
color: controller.isShowToolInterruptEnabled.value
? primaryColor
: (isDarkMode ? Colors.grey[400] : Colors.grey),
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
//'ttsInterruptToggle'.tr,
"显示工具",
style: TextStyle(fontSize: 14.sp),
),
const Spacer(),
Switch(
value: controller.isShowToolInterruptEnabled.value,
onChanged: (value) =>
controller.toggleShowAIToolInterrupt(),
activeColor: primaryColor,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
],
)),
onTap: () => controller.toggleTtsInterrupt(),
),
// 播报打断开关选项
PopupMenuItem(
child: Obx(() => Row(
children: [
Icon(
controller.isTtsInterruptEnabled.value
? Icons.voice_over_off
: Icons.voice_over_off_outlined,
color: controller.isTtsInterruptEnabled.value
? primaryColor
: (isDarkMode ? Colors.grey[400] : Colors.grey),
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
//'ttsInterruptToggle'.tr,
"播报打断",
style: TextStyle(fontSize: 14.sp),
),
const Spacer(),
Switch(
value: controller.isTtsInterruptEnabled.value,
onChanged: (value) =>
controller.toggleTtsInterrupt(),
activeColor: primaryColor,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
],
)),
onTap: () => controller.toggleTtsInterrupt(),
),
// 清除聊天记录选项
PopupMenuItem(
child: Row(
children: [

20
lib/modules/agent/views/message_bubble.dart

@ -84,7 +84,8 @@ class MessageBubble extends GetView<AgentController> {
],
),
child: Padding(
padding: (!isUser &&
padding: (controller.isShowToolInterruptEnabled.value &&
!isUser &&
functionCalls != null &&
functionCalls!.isNotEmpty)
? const EdgeInsets.only(top: 30)
@ -93,7 +94,8 @@ class MessageBubble extends GetView<AgentController> {
),
),
//工具调用流程显示(仅在AI消息且有functionCalls时显示)
if (!isUser &&
if (controller.isShowToolInterruptEnabled.value &&
!isUser &&
functionCalls != null &&
functionCalls!.isNotEmpty)
_buildFunctionCallsWidget(context),
@ -112,7 +114,7 @@ class MessageBubble extends GetView<AgentController> {
Text(
_formatTimestamp(timestamp),
style: TextStyle(
fontSize: 14,
fontSize: 8,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: Colors.grey[500],
@ -183,12 +185,20 @@ class MessageBubble extends GetView<AgentController> {
);
}
if (!isUser && message.isEmpty && functionCalls == null && meta == null) {
if (!isUser &&
message.isEmpty &&
(functionCalls == null ||
!controller.isShowToolInterruptEnabled.value) &&
meta == null) {
return _buildTypingIndicator();
}
// 如果只有工具调用而没有文本内容,显示一个占位符确保气泡有足够宽度
if (!isUser && message.isEmpty && functionCalls != null && meta == null) {
if (!isUser &&
message.isEmpty &&
functionCalls != null &&
controller.isShowToolInterruptEnabled.value &&
meta == null) {
return Container(
constraints: const BoxConstraints(
minWidth: 200, // 确保最小宽度

2
lib/modules/opus_test/controllers/opus_test_controller.dart

@ -140,7 +140,7 @@ class OpusTestController extends GetxController {
for (var entity in files) {
if (entity is File) {
final path = entity.path.toLowerCase();
if (path.endsWith('.opus')) {
if (path.endsWith('.opus') || path.endsWith('.txt')) {
final fileSize = await entity.length();
externalFiles.add(PlatformFile(
name: entity.path.split('/').last,

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

@ -672,9 +672,10 @@ class PairingController extends GetxController with WidgetsBindingObserver {
_verifiedDeviceAddress?.toUpperCase()) {
// 已有关联,检查BLE连接状态
Logger.i(_tag, '设备已关联,检查BLE连接状态');
print('fdp--connectble--pairedMacAddress: $pairedMacAddress');
final currentBleState = await _bleManager.getConnectionState();
Logger.i(_tag, '验证时BLE状态: $currentBleState');
print('fdp--connectble--currentBleState: $currentBleState');
if (currentBleState == 2) {
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,直接完成配对');
@ -700,21 +701,24 @@ class PairingController extends GetxController with WidgetsBindingObserver {
return;
}
Logger.i(_tag, '开始BLE设备关联: $_verifiedDeviceName:$_verifiedDeviceAddress');
// 调用BLE管理器的关联方法
final success = await _bleManager.startDeviceAssociation(
deviceNamePattern: _verifiedDeviceName!,
macAddress: _verifiedDeviceAddress!,
);
// Logger.i(_tag, '开始BLE设备关联: $_verifiedDeviceName:$_verifiedDeviceAddress');
if (Platform.isIOS) {
// 调用BLE管理器的关联方法
final success = await _bleManager.startDeviceAssociation(
deviceNamePattern: _verifiedDeviceName!,
macAddress: _verifiedDeviceAddress!,
);
if (success) {
Logger.i(_tag, 'BLE设备关联成功');
// 关联成功后,转到等待BLE连接状态
_transition(PairingState.waitingBle);
if (success) {
Logger.i(_tag, 'BLE设备关联成功');
//关联成功后,转到等待BLE连接状态
_transition(PairingState.waitingBle);
} else {
_setError(ErrorType.associationFailed,
'bleDeviceAssociationFailed'.tr); //BLE设备关联失败
}
} else {
_setError(ErrorType.associationFailed,
'bleDeviceAssociationFailed'.tr); //BLE设备关联失败
_transition(PairingState.waitingBle);
}
} catch (e) {
Logger.e(_tag, 'BLE设备关联异常: $e');
@ -803,13 +807,12 @@ class PairingController extends GetxController with WidgetsBindingObserver {
}
} else {
//未绑定设备
_bleManager.clearPairedDevices();
_bleManager.clearPairedDevices();
Logger.i(_tag, '验证时BLE状态: $currentBleState');
if (currentBleState == 2) {
//已连接 清理
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,断开并清理缓存');
_bleManager.disconnect();
}
}
@ -940,6 +943,11 @@ class PairingController extends GetxController with WidgetsBindingObserver {
}
}
}
final currentBleState = await _bleManager.getConnectionState();
if (currentBleState == 2) {
_bleManager.clearPairedDevices();
_bleManager.disconnect();
}
await Future.delayed(Duration(seconds: 1));
}
_bleManager.stopScan();

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

@ -2,6 +2,7 @@ package com.yunqiinnovation.agent_service
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import org.json.JSONArray
@ -62,6 +63,12 @@ interface AgentServiceListener {
*/
object AgentService : CoroutineScope {
private val TAG = "AgentService"
// SharedPreferences 相关常量
private const val PREF_NAME = "agent_service_prefs"
private const val KEY_IS_INTERRUPT = "is_interrupt"
private var _isInterrupt = AtomicBoolean(false)
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
@ -196,8 +203,16 @@ object AgentService : CoroutineScope {
serverurl = config["serverurl"]?.toString() ?: "";
usertoken = config["usertoken"]?.toString() ?: "";
callingTool = config["callingTool"]?.toString() ?: "";
// 初始化上下文和服务
this.context = appContext.applicationContext
// 从 SharedPreferences 读取 _isInterrupt 状态
loadIsInterruptFromCache()
if (config["isttsinterrupt"] != null){
_isInterrupt.set(config["isttsinterrupt"]?.toString() == "true")
}
this.chatApiService = ChatApiService(context)
this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context)
@ -367,6 +382,9 @@ object AgentService : CoroutineScope {
sendEvent("playback_started", mapOf("status" to "playback_started"))
// 确保气泡音已停止,避免与TTS播放重叠
audioPlayer?.stopAudio()
if(_isInterrupt.get()){
BleService.closeCodec()
}
// 发送AI说话开始广播
val intent =
Intent("com.yunqiinnovation.music_service.AI_SAY_START").apply {
@ -390,6 +408,9 @@ object AgentService : CoroutineScope {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
if(currentRecognitionMode == "ble_wakeup"&&_isInterrupt.get()){
BleService.openB1Encoder()
}
if(isplaymusic){
isplaymusic = false
Log.e(TAG, "QQ 音乐恢复播放")
@ -521,7 +542,21 @@ object AgentService : CoroutineScope {
startIdleCheckForMode(currentRecognitionMode)
}
}
/**
* 切换中断状态
* 如果当前为false,则设置为true;如果当前为true,则设置为false
* @return 切换后的状态值
*/
fun isInterrupt(isInterrupt: Boolean): Boolean {
// val currentValue = _isInterrupt.get()
// val newValue = !currentValue
_isInterrupt.set(isInterrupt)
// 保存到 SharedPreferences
saveIsInterruptToCache(isInterrupt)
return isInterrupt
}
/**
* 开始语音识别
* @param isExternalActive 是否使用外部音频源(蓝牙)
@ -617,6 +652,7 @@ object AgentService : CoroutineScope {
sendEvent(
"recognizing", mapOf(
"sessionid" to sessionid,
"text" to recognizing,
"language" to detectedLanguage
)
@ -1013,6 +1049,7 @@ object AgentService : CoroutineScope {
override fun onComplete(sessionid: String) {
try {
if (sessionid == currsessionId) {
audioPlayer?.stopAudio()
}
// 视情况决定是否朗读回复
@ -1090,10 +1127,11 @@ object AgentService : CoroutineScope {
if (name == "exit_interaction") {
BleService.closeCodec()
stopRecognition()
}else if (name == "qqmusic_pause" || name == "music_pause"){
isplaymusic = false
}
Log.d(TAG, "liwei--------------- AI Call onFunctionCall iscallingTool:$iscallingTool callingTool:$callingTool")
if (iscallingTool) {
ttsService?.speakStream(callingTool)
iscallingTool = false
}
@ -2544,4 +2582,35 @@ object AgentService : CoroutineScope {
}
}
}
/**
* 从 SharedPreferences 加载 _isInterrupt 状态
*/
private fun loadIsInterruptFromCache() {
try {
if (::context.isInitialized) {
val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
val cachedValue = prefs.getBoolean(KEY_IS_INTERRUPT, false) // 默认值为 false
_isInterrupt.set(cachedValue)
Log.d(TAG, "从缓存加载 _isInterrupt 状态: $cachedValue")
}
} catch (e: Exception) {
Log.e(TAG, "加载 _isInterrupt 缓存状态时发生异常: ${e.message}", e)
}
}
/**
* 保存 _isInterrupt 状态到 SharedPreferences
*/
private fun saveIsInterruptToCache(isInterrupt: Boolean) {
try {
if (::context.isInitialized) {
val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
prefs.edit().putBoolean(KEY_IS_INTERRUPT, isInterrupt).apply()
Log.d(TAG, "保存 _isInterrupt 状态到缓存: $isInterrupt")
}
} catch (e: Exception) {
Log.e(TAG, "保存 _isInterrupt 缓存状态时发生异常: ${e.message}", e)
}
}
}

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

@ -77,6 +77,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
config["usertoken"]?.toString()?.let { intent.putExtra("usertoken", it) }
config["musicServiceType"]?.toString()?.let { intent.putExtra("musicServiceType", it) }
config["navigationMode"]?.toString()?.let { intent.putExtra("navigationMode", it) }
config["isttsinterrupt"]?.toString()?.let { intent.putExtra("isttsinterrupt", it) }
config["callingTool"]?.toString()?.let { intent.putExtra("callingTool", it) }
@ -110,6 +111,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
"stopAgentService" -> {
try {
Log.d(TAG, "停止AgentService成功")
val intent = Intent(context, BleMediaService::class.java)
context.stopService(intent)
result.success(true)
@ -129,6 +131,16 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.error("START_ERROR", "启动对话失败: ${e.message}", null)
}
}
"isInterrupt" -> {
try {
val isinterrupt = call.argument<Boolean>("isinterrupt")
val _isInrrupt = isinterrupt?.let { AgentService.isInterrupt(isinterrupt) }
result.success(_isInrrupt)
} catch (e: Exception) {
Log.e(TAG, "检查是否打断失败", e)
result.error("IS_INTERRUPT_ERROR", "检查是否打断失败: ${e.message}", null)
}
}
"recognizeCallback" -> {
val isStarted = AgentService.recognizeCallback()
result.success(isStarted)

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

@ -66,6 +66,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
return false
}
Log.d(TAG, "手动启动语音识别: ")
AgentService.interruptCurrentResponse()
return AgentService.startRecognition(true, "ble_wakeup")
}

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

@ -21,6 +21,15 @@ import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
import com.yunqiinnovation.ble_service.BleConst
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleCompanionManager
// 添加蓝牙相关导入
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
/**
* BLE代理服务
@ -28,6 +37,7 @@ import com.yunqiinnovation.ble_service.BleService
* 1. 语音代理初始化和管理
* 2. BLE设备连接管理
* 3. 后台扫描结果处理
* 4. 蓝牙设备连接监听
*/
class BleMediaService : Service(), CoroutineScope {
@ -37,6 +47,9 @@ class BleMediaService : Service(), CoroutineScope {
private const val NOTIFICATION_ID = 1002
private const val PREF_NAME = "deep_voice_config"
// 目标蓝牙设备名称
private const val TARGET_DEVICE_NAME = "DeapSound"
// ChatAgent配置参数键
private const val KEY_AZURE_SPEECH_KEY = "azureSpeechKey"
private const val KEY_AZURE_SPEECH_REGION = "azureSpeechRegion"
@ -76,6 +89,309 @@ class BleMediaService : Service(), CoroutineScope {
// 当前启动Intent
private var currentIntent: Intent? = null
// 蓝牙相关组件
private var bluetoothAdapter: BluetoothAdapter? = null
private var bluetoothReceiver: BroadcastReceiver? = null
// 添加标志位跟踪广播接收器注册状态
private var isBluetoothReceiverRegistered = false
// 添加定期检查蓝牙状态的协程作业
private var bluetoothCheckJob: kotlinx.coroutines.Job? = null
/**
* BLE伴侣设备管理器
* 用于管理设备配对和后台扫描
*/
private var companionManager: BleCompanionManager? = null
/**
* 蓝牙设备连接状态广播接收器
* 监听蓝牙设备的连接和断开事件
*/
private fun createBluetoothReceiver(): BroadcastReceiver {
return object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
BluetoothDevice.ACTION_ACL_CONNECTED -> {
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
device?.let { handleDeviceConnected(it) }
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
device?.let { handleDeviceDisconnected(it) }
}
BluetoothAdapter.ACTION_STATE_CHANGED -> {
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
handleBluetoothStateChanged(state)
}
}
}
}
}
/**
* 处理蓝牙设备连接事件
* @param device 连接的蓝牙设备
*/
private fun handleDeviceConnected(device: BluetoothDevice) {
val deviceName = device.name ?: "Unknown"
val deviceAddress = device.address ?: "Unknown"
Log.i(TAG, "蓝牙设备已连接: $deviceName ($deviceAddress)")
// 检查是否是目标设备
if (deviceName.equals(TARGET_DEVICE_NAME, ignoreCase = true)) {
Log.i(TAG, "检测到目标设备 '$TARGET_DEVICE_NAME' 连接!")
Log.i(TAG, "设备信息:")
Log.i(TAG, " - 设备名称: $deviceName")
Log.i(TAG, " - 设备地址: $deviceAddress")
Log.i(TAG, " - 设备类型: ${getDeviceTypeString(device.type)}")
Log.i(TAG, " - 绑定状态: ${getBondStateString(device.bondState)}")
// 可以在这里添加更多的处理逻辑,比如自动连接BLE服务等
onTargetDeviceConnected(device)
}
}
/**
* 处理蓝牙设备断开事件
* @param device 断开的蓝牙设备
*/
private fun handleDeviceDisconnected(device: BluetoothDevice) {
val deviceName = device.name ?: "Unknown"
val deviceAddress = device.address ?: "Unknown"
Log.i(TAG, "蓝牙设备已断开: $deviceName ($deviceAddress)")
if (deviceName.equals(TARGET_DEVICE_NAME, ignoreCase = true)) {
Log.i(TAG, "目标设备 '$TARGET_DEVICE_NAME' 已断开连接")
onTargetDeviceDisconnected(device)
}
}
/**
* 处理蓝牙适配器状态变化
* @param state 蓝牙适配器状态
*/
private fun handleBluetoothStateChanged(state: Int) {
when (state) {
BluetoothAdapter.STATE_OFF -> {
Log.w(TAG, "蓝牙已关闭")
//companionManager?.unregisterBackgroundScan()
// 蓝牙关闭时,注销设备连接监听器(保留状态监听器)
unregisterDeviceConnectionReceiver()
}
BluetoothAdapter.STATE_ON -> {
Log.i(TAG, "蓝牙已开启")
// 蓝牙开启后,重新注册设备连接监听器
registerDeviceConnectionReceiver()
// 检查已连接的设备
checkAlreadyConnectedDevices()
}
BluetoothAdapter.STATE_TURNING_OFF -> {
Log.i(TAG, "蓝牙正在关闭...")
}
BluetoothAdapter.STATE_TURNING_ON -> {
Log.i(TAG, "蓝牙正在开启...")
}
}
}
/**
* 目标设备连接时的处理逻辑
* @param device 连接的目标设备
*/
private fun onTargetDeviceConnected(device: BluetoothDevice) {
// 在这里可以添加目标设备连接后的特殊处理逻辑
// 比如自动启动某些功能、发送通知等
Log.i(TAG, "目标设备 '${device.name}' 连接成功,执行相关处理逻辑")
}
/**
* 目标设备断开时的处理逻辑
* @param device 断开的目标设备
*/
private fun onTargetDeviceDisconnected(device: BluetoothDevice) {
// 在这里可以添加目标设备断开后的处理逻辑
Log.i(TAG, "目标设备 '${device.name}' 断开连接,执行清理逻辑")
}
/**
* 获取设备类型字符串
* @param type 设备类型
* @return 设备类型描述
*/
private fun getDeviceTypeString(type: Int): String {
return when (type) {
BluetoothDevice.DEVICE_TYPE_CLASSIC -> "经典蓝牙"
BluetoothDevice.DEVICE_TYPE_LE -> "低功耗蓝牙(BLE)"
BluetoothDevice.DEVICE_TYPE_DUAL -> "双模蓝牙"
else -> "未知类型"
}
}
/**
* 获取绑定状态字符串
* @param bondState 绑定状态
* @return 绑定状态描述
*/
private fun getBondStateString(bondState: Int): String {
return when (bondState) {
BluetoothDevice.BOND_NONE -> "未绑定"
BluetoothDevice.BOND_BONDING -> "正在绑定"
BluetoothDevice.BOND_BONDED -> "已绑定"
else -> "未知状态"
}
}
/**
* 检查已连接的蓝牙设备
* 在服务启动或蓝牙开启时调用,检查是否有目标设备已经连接
*/
private fun checkAlreadyConnectedDevices() {
try {
bluetoothAdapter?.let { adapter ->
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_CONNECT)
== PackageManager.PERMISSION_GRANTED) {
val bondedDevices = adapter.bondedDevices
Log.i(TAG, "检查已绑定的设备数量: ${bondedDevices?.size ?: 0}")
bondedDevices?.forEach { device ->
val deviceName = device.name ?: "Unknown"
Log.d(TAG, "已绑定设备: $deviceName (${device.address})")
if (deviceName.equals(TARGET_DEVICE_NAME, ignoreCase = true)) {
Log.i(TAG, "发现已绑定的目标设备: $deviceName")
val pairedUuid = companionManager?.getPairedUuid()
val pairedMac = companionManager?.getPairedMacAddress()
companionManager?.registerBackgroundScan(pairedMac, pairedUuid)
// 注意:这里只能检查绑定状态,无法直接判断连接状态
// 实际连接状态需要通过广播接收器监听
}
}
} else {
Log.w(TAG, "缺少蓝牙连接权限,无法检查已连接设备")
}
}
} catch (e: Exception) {
Log.e(TAG, "检查已连接设备时发生异常", e)
}
}
/**
* 初始化蓝牙监听器
* 设置蓝牙适配器和注册广播接收器
*/
private fun initializeBluetoothListener() {
try {
// 获取蓝牙管理器和适配器
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
bluetoothAdapter = bluetoothManager?.adapter
if (bluetoothAdapter == null) {
Log.w(TAG, "设备不支持蓝牙功能")
return
}
// 创建蓝牙广播接收器
if (bluetoothReceiver == null) {
bluetoothReceiver = createBluetoothReceiver()
}
// 首先注册蓝牙状态变化监听器(始终保持注册)
registerBluetoothStateReceiver()
// 如果蓝牙已开启,注册设备连接监听器
if (bluetoothAdapter!!.isEnabled) {
Log.i(TAG, "蓝牙已开启,注册设备连接监听器")
registerDeviceConnectionReceiver()
// 检查已连接的设备
checkAlreadyConnectedDevices()
} else {
Log.w(TAG, "蓝牙未开启,仅注册状态监听器")
}
} catch (e: Exception) {
Log.e(TAG, "初始化蓝牙监听器时发生异常", e)
}
}
/**
* 注册蓝牙状态变化监听器
* 这个监听器始终保持注册状态,用于监听蓝牙开关状态
*/
private fun registerBluetoothStateReceiver() {
try {
if (!isBluetoothReceiverRegistered && bluetoothReceiver != null) {
val stateFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
registerReceiver(bluetoothReceiver, stateFilter)
isBluetoothReceiverRegistered = true
Log.i(TAG, "蓝牙状态监听器注册成功")
}
} catch (e: Exception) {
Log.e(TAG, "注册蓝牙状态监听器时发生异常", e)
}
}
/**
* 注册设备连接监听器
* 仅在蓝牙开启时注册,用于监听设备连接和断开事件
*/
private fun registerDeviceConnectionReceiver() {
try {
if (isBluetoothReceiverRegistered && bluetoothReceiver != null) {
// 先注销现有的接收器
unregisterReceiver(bluetoothReceiver!!)
// 重新注册包含所有事件的接收器
val fullFilter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
}
registerReceiver(bluetoothReceiver, fullFilter)
Log.i(TAG, "设备连接监听器注册成功")
}
} catch (e: Exception) {
Log.e(TAG, "注册设备连接监听器时发生异常", e)
}
}
/**
* 注销设备连接监听器
* 蓝牙关闭时调用,但保留状态监听器
*/
private fun unregisterDeviceConnectionReceiver() {
try {
if (isBluetoothReceiverRegistered && bluetoothReceiver != null) {
// 注销当前接收器
unregisterReceiver(bluetoothReceiver!!)
// 重新注册仅包含状态变化的接收器
val stateFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
registerReceiver(bluetoothReceiver, stateFilter)
Log.i(TAG, "设备连接监听器已注销,保留状态监听器")
}
} catch (e: Exception) {
Log.e(TAG, "注销设备连接监听器时发生异常", e)
}
}
/**
* 完全注销蓝牙广播接收器
* 仅在服务销毁时调用
*/
private fun unregisterBluetoothReceiver() {
try {
if (isBluetoothReceiverRegistered && bluetoothReceiver != null) {
unregisterReceiver(bluetoothReceiver!!)
isBluetoothReceiverRegistered = false
Log.i(TAG, "蓝牙广播接收器已完全注销")
}
} catch (e: Exception) {
Log.e(TAG, "注销蓝牙广播接收器时发生异常", e)
}
}
override fun onCreate() {
super.onCreate()
@ -84,10 +400,80 @@ class BleMediaService : Service(), CoroutineScope {
// 设置服务状态
isRunning.set(true)
startForeground(NOTIFICATION_ID, createNotification())
// 初始化BLE伴侣设备管理器 - 使用单例模式获取实例
companionManager = BleCompanionManager.getInstance(this)
// 初始化蓝牙监听器
initializeBluetoothListener()
// 启动定期蓝牙状态检查
startBluetoothStatusCheck()
Log.i(TAG, "BLE代理服务已启动")
}
/**
* 启动定期蓝牙状态检查
* 每30秒检查一次蓝牙状态,确保监听器正常工作
*/
private fun startBluetoothStatusCheck() {
bluetoothCheckJob = launch {
while (isRunning.get()) {
try {
kotlinx.coroutines.delay(30000) // 30秒检查一次
checkAndRestoreBluetoothListener()
} catch (e: Exception) {
Log.e(TAG, "蓝牙状态检查时发生异常", e)
}
}
}
Log.i(TAG, "蓝牙状态定期检查已启动")
}
/**
* 检查并恢复蓝牙监听器
* 确保在蓝牙开启时监听器正常工作
*/
private fun checkAndRestoreBluetoothListener() {
try {
bluetoothAdapter?.let { adapter ->
val isBluetoothEnabled = adapter.isEnabled
Log.d(TAG, "蓝牙状态检查: 蓝牙${if (isBluetoothEnabled) "已开启" else "已关闭"}, 接收器${if (isBluetoothReceiverRegistered) "已注册" else "未注册"}")
if (isBluetoothEnabled && isBluetoothReceiverRegistered) {
// 蓝牙开启且接收器已注册,检查是否需要重新注册设备连接监听器
// 这里可以通过尝试获取当前注册的IntentFilter来判断
// 但Android没有直接的API,所以我们采用重新注册的方式确保监听器有效
Log.d(TAG, "重新确认设备连接监听器注册状态")
registerDeviceConnectionReceiver()
} else if (isBluetoothEnabled && !isBluetoothReceiverRegistered) {
// 蓝牙开启但接收器未注册,重新初始化
Log.w(TAG, "检测到蓝牙已开启但监听器未注册,重新初始化")
initializeBluetoothListener()
} else if (!isBluetoothEnabled && isBluetoothReceiverRegistered) {
// 蓝牙关闭但接收器仍注册,保持状态监听器
Log.d(TAG, "蓝牙已关闭,确保仅保留状态监听器")
unregisterDeviceConnectionReceiver()
}
} ?: run {
// 蓝牙适配器为空,重新初始化
Log.w(TAG, "蓝牙适配器为空,重新初始化监听器")
initializeBluetoothListener()
}
} catch (e: Exception) {
Log.e(TAG, "检查和恢复蓝牙监听器时发生异常", e)
}
}
/**
* 停止蓝牙状态检查
*/
private fun stopBluetoothStatusCheck() {
bluetoothCheckJob?.cancel()
bluetoothCheckJob = null
Log.i(TAG, "蓝牙状态定期检查已停止")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "BleAgentService.onStartCommand: ${intent?.action}")
@ -185,6 +571,8 @@ class BleMediaService : Service(), CoroutineScope {
it.getStringExtra(KEY_MUSICSERVICE_TYPE)?.let { value -> config[KEY_MUSICSERVICE_TYPE] = value }
it.getStringExtra(APP_NAVIGATION_MODE)?.let { value -> config[APP_NAVIGATION_MODE] = value }
it.getStringExtra(APP_CALLING_TOOL)?.let { value -> config[APP_CALLING_TOOL] = value }
it.getStringExtra("isttsinterrupt")?.let { value -> config["isttsinterrupt"] = value }
// 提取 supportedLanguages 参数
it.getStringArrayListExtra(KEY_SUPPORTED_LANGUAGES)?.let { languages ->
if (languages.isNotEmpty()) {
@ -272,6 +660,14 @@ class BleMediaService : Service(), CoroutineScope {
override fun onDestroy() {
Log.i(TAG, "BLE代理服务正在销毁...")
// 停止蓝牙状态检查
stopBluetoothStatusCheck()
// 完全注销蓝牙广播接收器
unregisterBluetoothReceiver()
bluetoothReceiver = null
bluetoothAdapter = null
// 取消所有协程
cancel()
@ -338,4 +734,4 @@ class BleMediaService : Service(), CoroutineScope {
return builder.build()
}
}
}

199
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -27,6 +27,10 @@ class AgentServiceImpl: NSObject {
static let shared = AgentServiceImpl()
// UserDefaults 相关常量
private let PREF_NAME = "agent_service_prefs"
private let KEY_IS_INTERRUPT = "is_interrupt"
private var listeners = [AgentServiceListener]()
private let listenersLock = NSLock()
@ -76,11 +80,15 @@ class AgentServiceImpl: NSObject {
//是否保留最终结果
private var isKeepResult = true;
// 新增:是否在识别过程中被中断
public var isInterrupt: Bool = true
var currsessionId: String = ""
var language: String = ""
private var serverurl = ""
private var usertoken = ""
private let timerQueue = DispatchQueue(label: "com.example.idleTimer.queue")
private var idleTimer: DispatchSourceTimer?
private let maxIdleSeconds: TimeInterval = 10
var micCapture: MicrophoneCapture!
@ -174,6 +182,14 @@ class AgentServiceImpl: NSObject {
self.callingTool = callingTool
}
//加载 isInterrupt 状态
loadIsInterruptFromCache()
if let isttsinterrupt = config["isttsinterrupt"] as? String {
if (isttsinterrupt == "true"){
self.isInterrupt = true
}
}
if let serverurl = config["serverurl"] as? String {
self.serverurl = serverurl
}
@ -248,6 +264,7 @@ class AgentServiceImpl: NSObject {
if isInitialized {
loadChatHistory()
}
LocationService.shared.getCurrentLocation() { location in
// self.locationInfo = location
@ -357,41 +374,62 @@ class AgentServiceImpl: NSObject {
}
private func startIdleCheck(idleSeconds: TimeInterval = 10) {
stopIdleCheck()
guard isRecognizing else { return }
os_log("启动空闲检测,超时时间: %.0f秒,模式: %{public}@", log: logger, type: .info, idleSeconds, currentRecognitionMode)
// 使用全局队列而不是主队列,避免后台挂起问题
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility))
idleTimer = timer
timer.schedule(deadline: .now() + idleSeconds)
timer.setEventHandler { [weak self] in
// 使用更安全的方式检查self是否存在
DispatchQueue.main.async { [weak self] in
// 2. 所有对idleTimer的操作都通过串行队列执行,避免竞态条件
timerQueue.async {
// 先停止旧的定时器(在串行队列中执行,确保线程安全)
self.stopIdleCheck()
guard self.isRecognizing else { return }
os_log("启动空闲检测,超时时间: %.0f秒,模式: %{public}@",
log: self.logger,
type: .info,
idleSeconds,
self.currentRecognitionMode)
// 使用全局队列作为定时器回调队列(避免后台挂起)
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility))
timer.schedule(deadline: .now() + idleSeconds)
timer.setEventHandler { [weak self] in
guard let self = self else { return }
if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming {
os_log("空闲检测触发自动停止,模式: %{public}@", log: self.logger, type: .info, self.currentRecognitionMode)
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
self.stopRecognition()
self.sendEvent(name: "auto_stop", data: [
"reason": "idle_timeout",
"seconds": idleSeconds,
"mode": self.currentRecognitionMode
])
// 回调在全局队列,涉及UI或实例变量操作时切换到主线程
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// 检查状态(确保在主线程访问这些属性,避免UI相关竞态)
if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming {
os_log("空闲检测触发自动停止,模式: %{public}@",
log: self.logger,
type: .info,
self.currentRecognitionMode)
// 关闭编码器(设备侧操作,若BleService线程安全可直接调用,否则需加锁)
BleService.shared.closeCodec()
self.stopRecognition()
self.sendEvent(name: "auto_stop", data: [
"reason": "idle_timeout",
"seconds": idleSeconds,
"mode": self.currentRecognitionMode
])
}
}
}
timer.resume()
// 3. 在串行队列中安全赋值
self.idleTimer = timer
}
timer.resume()
}
private func stopIdleCheck() {
idleTimer?.cancel()
idleTimer = nil
timerQueue.async {
self.idleTimer?.cancel()
self.idleTimer = nil // 清空引用
}
}
private func restartIdleCheck() {
@ -520,11 +558,11 @@ class AgentServiceImpl: NSObject {
if !isRecognizing || !isInitialized {
return false
}
guard let audioStream = azureAsrHelper?.audioStream else {
os_log("AI音频流未初始化", log: logger,type: .error)
return false
}
audioStream.saveAudioDataTo(data: audioData)
guard let audioStream = azureAsrHelper?.audioStream else {
// os_log("AI音频流未初始化", log: logger,type: .error)
return false
}
audioStream.saveAudioDataTo(data: audioData)
return true
}
@ -654,6 +692,15 @@ audioStream.saveAudioDataTo(data: audioData)
messages.append(userMessage)
os_log("添加用户消息", log: logger, type: .info)
// 保存用户消息
self.chatStorageHelper.saveMessage(
agentId: self.agentId,
sessionId: sessionid,
message: displayText,
sender: "user",
metadata:""
)
addToHistoryMessages(chatApiService.createUserMessage(content: displayText))
currsessionId = sessionid
let callback = ChatApiStreamCallback(
@ -778,16 +825,9 @@ audioStream.saveAudioDataTo(data: audioData)
*/
internal func saveChatMessage(sessionId:String,userMessage: String, assistantMessage: String, metadata: String = "",userMetadata: String) {
DispatchQueue.global(qos: .utility).async {
// 保存用户消息
let userMessageId = self.chatStorageHelper.saveMessage(
agentId: self.agentId,
sessionId: sessionId,
message: userMessage,
sender: "user",
metadata: userMetadata
)
if userMessageId != -1 {
// if userMessageId != -1 {
if !assistantMessage.isEmpty || !metadata.isEmpty {
// 保存AI回复
let assistantMessageId = self.chatStorageHelper.saveMessage(
@ -801,9 +841,9 @@ audioStream.saveAudioDataTo(data: audioData)
os_log("保存助手消息失败", log: self.logger, type: .error)
}
}
} else {
os_log("保存用户消息失败", log: self.logger, type: .error)
}
// } else {
// os_log("保存用户消息失败", log: self.logger, type: .error)
// }
}
}
@ -814,6 +854,7 @@ internal func autoHandleFunctionCallResult(toolName: String, functionCallResult:
guard let contextValue = functionCallResult["context"] else {
return [
"meta": "",
"iscard":false,
"broadcast": true,
"notreply": false,
]
@ -881,9 +922,9 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -
metadata["isinternal"] = isinternal
} else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" {
processQQMusicPlayList(cardQQMusiclist)
} else if toolName == "maps_weather" {
} else if toolName == "map_weather" {
iscard = true
}else if toolName == "map_weather" {
}else if toolName == "maps_weather" {
iscard = true
}
metadata["iscard"] = iscard
@ -897,6 +938,7 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -
return [
"meta": metaStr,
"iscard":iscard,
"broadcast": broadcast,
"notreply": notreply
]
@ -1670,7 +1712,10 @@ class ChatApiStreamCallback: StreamCallback {
// 停止等待音效
agentService.audioPlayer?.stopAwaitSound()
}
os_log("onComplete agentService.isInterrupt:%{public}@", log: agentService.logger, type: .info, String(agentService.isInterrupt))
if(agentService.isInterrupt){
BleService.shared.closeCodec()
}
if speakResponse && reply && broadcast && sessionid == agentService.currsessionId{
agentService.azureTtsHelper?.flushStream(sessionid:sessionid)
@ -1749,10 +1794,10 @@ class ChatApiStreamCallback: StreamCallback {
print("onFunctionCall=functionCall\(functionCall)")
if (name == "exit_interaction") {
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
agentService.stopRecognition()
}
// 关闭编码器 (设备侧)
BleService.shared.closeCodec()
agentService.stopRecognition()
}
try agentService.sendEvent(name: "function_call", data: [
"sessionid":sessionid,
@ -1782,11 +1827,15 @@ class ChatApiStreamCallback: StreamCallback {
// agentService.audioPlayer?.stopCallingSound()
let resultmap = agentService.autoHandleFunctionCallResult(toolName:name,functionCallResult:functionCallResult)
metadata = resultmap["meta"] as? String ?? ""
broadcast = resultmap["broadcast"] as? Bool ?? true
reply = !(resultmap["notreply"] as? Bool ?? true)
iscard = !(resultmap["iscard"] as? Bool ?? false)
iscard = resultmap["iscard"] as? Bool ?? false
os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{iscard}@",
log: agentService.logger,
type: .info,
"\(broadcast)", "\(reply)", "\(iscard)")
if (iscard){
metadata = resultmap["meta"] as? String ?? ""
agentService.sendEvent(name: "function_call_result", data: [
"sessionid":sessionid,
"function_name": name,
@ -1794,10 +1843,13 @@ class ChatApiStreamCallback: StreamCallback {
"result": functionCallResult,
"meta":metadata,
])
os_log("收到函数调用结果! broadcast: %{public}@ reply: %{public}@ %{metadata}@",
log: agentService.logger,
type: .info,
"\(broadcast)", "\(reply)", "\(metadata)")
}else{
agentService.sendEvent(name: "function_call_result", data: [
"sessionid":sessionid,
"function_name": name,
"function_call": functionCall,
"result": functionCallResult,
])
}
}catch{
@ -1825,7 +1877,7 @@ extension AgentServiceImpl: TtsEventListener {
restartIdleCheck()
sendEvent(name: "tts_started", data: ["status": "started"])
audioPlayer?.stopAwaitSound()
case .synthesisCompleted:
isSpeaking = false
restartIdleCheck()
@ -1835,8 +1887,14 @@ extension AgentServiceImpl: TtsEventListener {
restartIdleCheck()
sendEvent(name: "playback_started", data: ["status": "playback_started"])
MusicService.shared.reduceVolume()
if(isInterrupt){ //播报开始。打断唤醒输入
BleService.shared.closeCodec()
}
case .playbackCompleted:
restartIdleCheck()
if(isInterrupt){//播报结束。打开解码器
BleService.shared.openB1Encoder()
}
sendEvent(name: "playback_completed", data: ["status": "playback_completed"])
MusicService.shared.restoreVolume()
case .synthesisCanceled:
@ -2147,7 +2205,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
isStartingRecognition = false
// 使用模式感知的空闲检测启动方法
startIdleCheckForMode(mode: currentRecognitionMode)
if (currentRecognitionMode == "ble_wakeup") {
if (currentRecognitionMode == "ble_wakeup") {
audioPlayer?.playStartSound()
}
@ -2196,4 +2254,25 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
stopRequestedDuringStart = false
stopIdleCheck()
}
// MARK: - isInterrupt 缓存方法
/**
* 从 UserDefaults 加载 isInterrupt 状态
*/
private func loadIsInterruptFromCache() {
let userDefaults = UserDefaults.standard
let cachedValue = userDefaults.bool(forKey: KEY_IS_INTERRUPT) // 默认值为 false
isInterrupt = cachedValue
os_log("从缓存加载 isInterrupt 状态: %{public}@", log: logger, type: .info, String(isInterrupt))
}
/**
* 保存 isInterrupt 状态到 UserDefaults
*/
func saveIsInterruptToCache(_ isInterrupt: Bool) {
let userDefaults = UserDefaults.standard
userDefaults.set(isInterrupt, forKey: KEY_IS_INTERRUPT)
os_log("保存 isInterrupt 状态到缓存: %{public}@", log: logger, type: .info, String(isInterrupt))
}
}

12
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -78,7 +78,17 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
let mode = (call.arguments as? [String: Any])?["mode"] as? String ?? "normal"
impl.startRecognition(mode: mode)
result(true)
case "isInterrupt":
if let args = call.arguments as? [String: Any],
let isInterrupt = args["isinterrupt"] as? Bool {
impl.isInterrupt = isInterrupt
// 保存到 UserDefaults
impl.saveIsInterruptToCache(isInterrupt)
result(impl.isInterrupt)
} else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "isinterrupt 参数缺失或类型错误", details: nil))
}
case "recognizeCallback":
impl.recognizeCallback()
result(true)

11
local_plugins/agent_service/lib/agent_service.dart

@ -266,6 +266,17 @@ class AgentService {
}
}
static Future<bool> isInterrupt(bool isInterrupt) async {
try {
final bool result = await _channel.invokeMethod('isInterrupt', {
'isinterrupt': isInterrupt,
});
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '检查是否打断失败', e.details);
}
}
/// 设备识别器
///
/// 启动语音识别,开始监听用户语音输入

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

@ -347,6 +347,7 @@ class AzureAsrHelper(private val context: Context) {
currentLanguage
}
callback.onResult(currsessionid,event.result.text, detectedLanguage)
currsessionid = UUID.randomUUID().toString()
}
}
)

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

@ -671,9 +671,9 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
try {
synthesizer?.StopSpeakingAsync()
synthesizer?.close()
streamBuffer.clear()
isSpeaking = false
recreateSynthesizer()
return true
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败: ${e.message}")

1
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -801,6 +801,7 @@ public class AzureAsrHelper: NSObject {
// 在主线程回调结果
DispatchQueue.main.async {
callback.onResult(sessionid: self.currsessionId,result.text ?? "", detectedLanguage)
self.currsessionId = UUID().uuidString
}
}

6
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -376,7 +376,7 @@ private func sendAstEvent(_ event: [String: Any]) {
// 安全解包版本
guard let audioStream = azureAsrHelper.audioStream else {
os_log("音频流未初始化", type: .error)
// os_log("音频流未初始化", type: .error)
return
}
print("liwei----------外部音频推送:\(audioBytes.data.count)")
@ -986,7 +986,7 @@ extension AzureSpeechPlugin: BleService.Callback {
}
// 安全解包版本
guard let audioStream = azureAsrHelper.audioStream else {
os_log("音频流未初始化", type: .error)
// os_log("音频流未初始化", type: .error)
return
}
azureAsrHelper.audioStream?.saveAudioDataTo(data: data)
@ -1030,7 +1030,7 @@ extension AzureSpeechPlugin: BleService.Callback {
// 安全解包版本
guard let audioStream = azureAsrHelper.audioStream else {
os_log("音频流未初始化", type: .error)
// os_log("音频流未初始化", type: .error)
return
}
azureAsrHelper.audioStream?.saveAudioDataTo(data: rightBuffer)

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

Binary file not shown.

BIN
local_plugins/ble_service/android/libs/jl_audio_decode_V2.1.0_20013-release.aar

Binary file not shown.

17
local_plugins/ble_service/android/src/main/AndroidManifest.xml

@ -12,6 +12,9 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- 系统启动权限 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Companion 特权:后台运行 & 启动前台服务 -->
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND"/>
<uses-permission android:name="android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND"/>
@ -47,6 +50,18 @@
<action android:name="com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" />
</intent-filter>
</receiver>
<!-- 系统启动接收器 -->
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
</application>
</manifest>
</manifest>

700
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt

@ -0,0 +1,700 @@
package com.yunqiinnovation.ble_service
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.annotation.RequiresPermission
import java.util.*
/**
* BLE指令发送器类:负责处理蓝牙设备的指令发送、接收和队列管理
* 功能包括:
* 1. 指令队列管理
* 2. 指令封装和CRC校验
* 3. 指令发送和重试机制
* 4. 响应超时处理
* 5. 设备响应和通知处理
*/
class BleCommandSender {
companion object {
private const val TAG = "BleCommandSender"
private const val REPLY_TIMEOUT_MS = 1000L // 响应超时时间1秒
}
// ======================================================================================================
// 成员变量
// ======================================================================================================
/** 指令队列 */
private val commandQueue: Queue<ByteArray> = LinkedList()
/** 是否正在等待回复 */
var isWaitingReply = true
private set
/** 回复的指令类型 */
var replyCommandType = 0xff
private set
/** 响应超时处理器 */
private val replyTimeoutHandler = Handler(Looper.getMainLooper())
/** 响应超时回调 */
private val replyTimeoutRunnable = Runnable {
if (!isWaitingReply) {
Log.w(TAG, "命令响应超时,自动重置 isWaitingReply 为 true")
isWaitingReply = true
processNextCommand()
}
}
/** 指令发送状态回调接口 */
interface CommandSenderCallback {
/**
* 检查连接状态
* @return 是否已连接
*/
fun isConnected(): Boolean
/**
* 获取写入特征
* @return 写入特征对象
*/
fun getWriteCharacteristic(): BluetoothGattCharacteristic?
/**
* 获取GATT连接
* @return GATT连接对象
*/
fun getBluetoothGatt(): BluetoothGatt?
/**
* 通知设备信息接收
* @param infoType 信息类型
* @param infoData 信息数据
*/
fun notifyDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>)
/**
* 通知唤醒信号接收
*/
fun notifyWakeupSignalReceived()
/**
* 获取录音文件对象
* @return 录音文件对象
*/
fun getRecordFile(): RecordingFile?
/**
* 获取重新压缩录音文件对象
* @return 重新压缩录音文件对象
*/
fun getRecordFile1(): RecordingFile?
}
private var callback: CommandSenderCallback? = null
// ======================================================================================================
// 公共方法
// ======================================================================================================
/**
* 设置回调接口
* @param callback 回调接口实现
*/
fun setCallback(callback: CommandSenderCallback) {
this.callback = callback
}
/**
* 发送命令到设备 - 通用方法
* @param cmdType 命令类型
* @param cmdData 命令数据
* @return 是否成功发送
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean {
val callback = this.callback ?: run {
Log.e(TAG, "发送命令失败: 回调接口未设置")
return false
}
if (!callback.isConnected()) {
Log.e(TAG, "发送命令失败: 设备未连接")
return false
}
try {
// 创建命令包
val cmdPacket = createCommandPacket(cmdType, cmdData)
// 记录命令详情
logCommandDetails(cmdType, cmdData, cmdPacket)
// 获取写入特征
val writeChar = callback.getWriteCharacteristic() ?: run {
Log.e(TAG, "发送命令失败: 写入特征未找到")
return false
}
// 设置数据并发送
writeChar.value = cmdPacket
return if (isWaitingReply) {
// 直接发送
val gatt = callback.getBluetoothGatt()
val isSuccess = gatt?.writeCharacteristic(writeChar) == true
if (isSuccess) {
isWaitingReply = false
startReplyTimeout()
true
} else {
Log.e(TAG, "发送命令失败: 写入特征失败")
false
}
} else {
// 加入队列等待发送
commandQueue.offer(cmdPacket)
Log.d(TAG, "命令已加入队列,当前队列长度: ${commandQueue.size}")
true
}
} catch (e: Exception) {
Log.e(TAG, "发送命令异常: ${e.message}", e)
return false
}
}
/**
* 处理命令响应,重置等待状态并处理下一个命令
* @param responseCommandType 响应的命令类型
*/
fun onCommandResponse(responseCommandType: Int) {
isWaitingReply = true
replyCommandType = responseCommandType
replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable)
processNextCommand()
}
/**
* 处理设备响应数据 - 从BleService移动过来
* @param data 响应数据
*/
@SuppressLint("StaticFieldLeak")
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun processDeviceResponse(data: ByteArray) {
Log.d(
TAG,
"收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}"
)
if (data.size < 3) {
Log.e(TAG, "设备响应数据格式错误:数据长度过短")
return
}
val header = data[0] // 0xBB
val cmdType = data[1].toInt() and 0xFF
// 判断命令失败响应(长度为0)
if (data.size == 4 && data[2].toInt() and 0xFF == 0) {
Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败")
// 创建失败结果Map
val resultMap = mapOf(
"success" to false,
"command" to cmdType,
"rawData" to byteArrayOf()
)
callback?.notifyDeviceInfoReceived(cmdType, resultMap)
// 通知指令发送器处理响应
onCommandResponse(cmdType)
return
}
// 正常响应处理
val length = data[2].toInt() and 0xFF
// 检查数据长度
if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC
Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return
}
// 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节
val payload = data.sliceArray(0 until data.size - 1)
val calculatedCrc = calculateCrc8Maxim(payload, payload.size)
val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
Log.e(
TAG,
"设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
)
return
}
// 提取响应数据
val responseData = if (length > 0) ByteArray(length) else ByteArray(0)
if (length > 0) {
System.arraycopy(data, 3, responseData, 0, length)
}
Log.d(
TAG,
"收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${
responseData.joinToString(", ") {
"0x${
(it.toInt() and 0xFF).toString(16)
}"
}
}"
)
// 通知指令发送器处理响应
onCommandResponse(cmdType)
// 根据命令类型处理不同的响应
when (cmdType) {
BleConst.CMD_GET_VERSION -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to "versionInfo",
"rawData" to responseData
)
if (responseData.isNotEmpty()) {
// 转换为字符串前,确认是否是有效的ASCII字符
val version =
responseData
.map { it.toInt() and 0xFF } // 转为无符号整数 (0-255)
.dropWhile { it == 0xFF } // 仅丢弃开头的连续 255
.joinToString(".") { it.toString() }
resultMap["version"] = version
Log.i(TAG, "接收到目标版本号: $version")
}
callback?.notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_GET_PRODUCT_ID -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to "productId",
"rawData" to responseData
)
if (responseData.isNotEmpty()) {
// 转换为字符串前,确认是否是有效的ASCII字符
val productId =
responseData.joinToString(".") { (it.toInt() and 0xFF).toString() }
resultMap["productId"] = productId
Log.i(TAG, "接收到产品ID: $productId")
}
callback?.notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_GET_COLOR_ID -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to "colorInfo",
"rawData" to responseData
)
if (responseData.isNotEmpty()) {
val colorId = (responseData[0].toInt() and 0xFF).toString()
resultMap["colorId"] = colorId
Log.i(TAG, "接收到耳机颜色ID: $colorId")
}
callback?.notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_GET_BATTERY_INFO -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to "batteryInfo",
"rawData" to responseData
)
if (responseData.isNotEmpty() && responseData.size >= 3) {
// 处理电池信息 - 左耳、右耳、充电仓
val leftEarBattery = responseData[0].toInt() and 0x7F
val leftEarCharging = (responseData[0].toInt() and 0x80) != 0
val rightEarBattery =
if (responseData.size > 1) responseData[1].toInt() and 0x7F else 0
val rightEarCharging =
if (responseData.size > 1) (responseData[1].toInt() and 0x80) != 0 else false
val caseBattery =
if (responseData.size > 2) responseData[2].toInt() and 0x7F else 0
val caseCharging =
if (responseData.size > 2) (responseData[2].toInt() and 0x80) != 0 else false
// 添加到结果Map
resultMap["leftEarBattery"] = leftEarBattery
resultMap["leftEarCharging"] = leftEarCharging
resultMap["rightEarBattery"] = rightEarBattery
resultMap["rightEarCharging"] = rightEarCharging
resultMap["caseBattery"] = caseBattery
resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
Log.i(
TAG, "接收到电量信息: 左耳=${leftEarBattery}% " +
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
"(${if (caseCharging) "充电中" else "未充电"})"
)
}
callback?.notifyDeviceInfoReceived(cmdType, resultMap)
}
BleConst.CMD_CONTROL_CODEC -> {
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to cmdType,
"rawData" to responseData
)
if (responseData.isNotEmpty() && responseData.size >= 2) {
val codecStatus = responseData[0].toInt() and 0xFF
val channelMode = responseData[1].toInt() and 0xFF
// 解码状态描述
val statusDesc = when (codecStatus) {
BleConst.CODEC_CONTROL_CLOSE -> "已关闭编解码"
BleConst.CODEC_CONTROL_DECODE_ON -> "已打开解码"
BleConst.CODEC_CONTROL_A2DP_PLAY -> "A2DP播放模式"
BleConst.CODEC_CONTROL_CALL_RECORD_PLAY -> "通话记录播放模式"
BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码"
else -> "未知状态($codecStatus)"
}
// 处理录音文件
if (codecStatus == BleConst.CODEC_CONTROL_DECODE_ON ||
codecStatus == BleConst.CODEC_CONTROL_A2DP_PLAY ||
codecStatus == BleConst.CODEC_CONTROL_CALL_RECORD_PLAY ||
codecStatus == BleConst.CODEC_CONTROL_ENCODE_ON
) {
callback?.getRecordFile1()?.closeFile()
callback?.getRecordFile1()?.creatingFiles()
callback?.getRecordFile()?.closeFile()
callback?.getRecordFile()?.creatingFiles()
} else if (codecStatus == BleConst.CODEC_CONTROL_CLOSE) {
callback?.getRecordFile()?.closeFile()
callback?.getRecordFile1()?.closeFile()
}
Log.d(TAG, "收到设备响应,channelMode: $channelMode")
// 声道模式描述
val channelDesc = when (channelMode) {
BleConst.AUDIO_CHANNEL_LEFT -> "左声道"
BleConst.AUDIO_CHANNEL_RIGHT -> "右声道"
BleConst.AUDIO_CHANNEL_STEREO -> "立体声"
else -> "未知声道($channelMode)"
}
resultMap["codecStatus"] = codecStatus
resultMap["channelMode"] = channelMode
resultMap["statusDesc"] = statusDesc
resultMap["channelDesc"] = channelDesc
Log.i(TAG, "编解码控制响应: $statusDesc, $channelDesc")
} else if (responseData.isNotEmpty() && responseData.size == 1) {
val codecStatus = responseData[0].toInt() and 0xFF
if (codecStatus == 0x00) {
Log.d(TAG, "收到设备响应:失败,codecStatus: $codecStatus")
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to "Error",
"rawData" to responseData
)
}
}
callback?.notifyDeviceInfoReceived(cmdType, resultMap)
}
else -> {
// 对于未知命令,返回原始数据
val resultMap = mapOf(
"success" to true,
"command" to cmdType,
"rawData" to responseData,
"isUnknown" to true
)
Log.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}")
callback?.notifyDeviceInfoReceived(cmdType, resultMap)
}
}
}
/**
* 处理设备主动上报数据 - 帧头为0xCC
* 将可见性修改为internal,允许同包内的BleService类访问
*/
fun processDeviceNotification(data: ByteArray) {
Log.d(
TAG,
"收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}"
)
if (data.size < 3) {
Log.e(TAG, "设备主动上报数据格式错误:数据长度过短")
return
}
val header = data[0] // 0xCC
val infoType = data[1].toInt() and 0xFF
val length = data[2].toInt() and 0xFF
// 检查数据长度
if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC
Log.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return
}
// 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节
val payload = data.sliceArray(0 until data.size - 1)
val calculatedCrc = calculateCrc8Maxim(payload, payload.size)
val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
Log.e(
TAG,
"设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
)
return
}
// 提取上报数据
val notifyData = if (length > 0) ByteArray(length) else ByteArray(0)
if (length > 0) {
System.arraycopy(data, 3, notifyData, 0, length)
}
Log.d(
TAG,
"收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${
notifyData.joinToString(", ") {
"0x${
(it.toInt() and 0xFF).toString(16)
}"
}
}"
)
// 根据上报类型处理
when (infoType) {
BleConst.CMD_GET_BATTERY_INFO -> { // 电量信息
val resultMap = mutableMapOf<String, Any>(
"success" to true,
"command" to infoType,
"rawData" to notifyData,
"isNotification" to true // 标记为主动上报
)
if (notifyData.isNotEmpty() && notifyData.size >= 3) {
// 处理电池信息 - 左耳、右耳、充电仓
val leftEarBattery = notifyData[0].toInt() and 0x7F
val leftEarCharging = (notifyData[0].toInt() and 0x80) != 0
val rightEarBattery =
if (notifyData.size > 1) notifyData[1].toInt() and 0x7F else 0
val rightEarCharging =
if (notifyData.size > 1) (notifyData[1].toInt() and 0x80) != 0 else false
val caseBattery = if (notifyData.size > 2) notifyData[2].toInt() and 0x7F else 0
val caseCharging =
if (notifyData.size > 2) (notifyData[2].toInt() and 0x80) != 0 else false
// 添加到结果Map
resultMap["leftEarBattery"] = leftEarBattery
resultMap["leftEarCharging"] = leftEarCharging
resultMap["rightEarBattery"] = rightEarBattery
resultMap["rightEarCharging"] = rightEarCharging
resultMap["caseBattery"] = caseBattery
resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
Log.i(
TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " +
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
"(${if (caseCharging) "充电中" else "未充电"})"
)
}
callback?.notifyDeviceInfoReceived(infoType, resultMap)
}
BleConst.CMD_WAKE_UP -> {
Log.i(TAG, "收到唤醒指令")
// 接收到唤醒信号,打开编码器 (设备侧)
callback?.notifyWakeupSignalReceived()
}
// 可以在这里添加其他主动上报类型的处理
else -> {
// 对于未知上报类型,返回原始数据
val resultMap = mapOf(
"success" to true,
"command" to infoType,
"rawData" to notifyData,
"isUnknown" to true,
"isNotification" to true // 标记为主动上报
)
Log.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}")
callback?.notifyDeviceInfoReceived(infoType, resultMap)
}
}
}
/**
* 清空命令队列
*/
fun clearCommandQueue() {
commandQueue.clear()
Log.d(TAG, "命令队列已清空")
}
/**
* 获取当前队列长度
* @return 队列中待发送的命令数量
*/
fun getQueueSize(): Int = commandQueue.size
/**
* 重置发送状态
*/
fun reset() {
isWaitingReply = true
replyCommandType = 0xff
replyTimeoutHandler.removeCallbacks(replyTimeoutRunnable)
clearCommandQueue()
Log.d(TAG, "指令发送器已重置")
}
// ======================================================================================================
// 私有方法
// ======================================================================================================
/**
* 创建命令包
* @param cmdType 命令类型
* @param cmdData 命令数据
* @return 完整的命令包(包含帧头、命令、长度、数据、CRC)
*/
private fun createCommandPacket(cmdType: Byte, cmdData: ByteArray): ByteArray {
val cmdLength = cmdData.size
val cmdPacket = ByteArray(4 + cmdLength) // 帧头 + 命令 + 长度 + 数据 + CRC
// 帧头
cmdPacket[0] = BleConst.APP_REQUEST_HEADER
// 命令类型
cmdPacket[1] = cmdType
// 命令长度
cmdPacket[2] = cmdLength.toByte()
// 填充命令数据(如果有)
if (cmdLength > 0) {
System.arraycopy(cmdData, 0, cmdPacket, 3, cmdLength)
}
// 计算CRC - 对帧头到数据的所有字节进行校验 (不包括CRC自身)
val crcPayload = cmdPacket.sliceArray(0 until cmdPacket.size - 1)
val crc = calculateCrc8Maxim(crcPayload, crcPayload.size)
// 设置CRC
cmdPacket[3 + cmdLength] = crc
return cmdPacket
}
/**
* 记录命令详情
* @param cmdType 命令类型
* @param cmdData 命令数据
* @param cmdPacket 完整命令包
*/
private fun logCommandDetails(cmdType: Byte, cmdData: ByteArray, cmdPacket: ByteArray) {
val crc = cmdPacket[cmdPacket.size - 1]
Log.d(
TAG,
"发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=${cmdData.size}, " +
"数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " +
"CRC=0x${(crc.toInt() and 0xFF).toString(16)}"
)
}
/**
* 处理队列中的下一个命令
*/
private fun processNextCommand() {
if (commandQueue.isEmpty()) {
return
}
val callback = this.callback ?: return
val nextCmd = commandQueue.poll() ?: return
val writeChar = callback.getWriteCharacteristic() ?: run {
Log.e(TAG, "处理下一个命令失败: 写入特征未找到")
return
}
val gatt = callback.getBluetoothGatt() ?: run {
Log.e(TAG, "处理下一个命令失败: GATT连接未找到")
return
}
writeChar.value = nextCmd
val isSuccess = gatt.writeCharacteristic(writeChar)
if (isSuccess) {
isWaitingReply = false
startReplyTimeout()
Log.d(TAG, "发送队列中的下一个命令,剩余队列长度: ${commandQueue.size}")
} else {
Log.e(TAG, "发送队列中的命令失败")
}
}
/**
* 启动响应超时计时器
*/
private fun startReplyTimeout() {
replyTimeoutHandler.postDelayed(replyTimeoutRunnable, REPLY_TIMEOUT_MS)
}
/**
* 计算CRC8/MAXIM校验值
* @param data 要校验的数据
* @param length 数据长度
* @return CRC8校验值
*/
private fun calculateCrc8Maxim(data: ByteArray, length: Int): Byte {
var crc = 0
val polynomial = 0x31
for (i in 0 until length) {
crc = crc xor (data[i].toInt() and 0xFF)
for (j in 0 until 8) {
if (crc and 0x80 != 0) {
crc = ((crc shl 1) xor polynomial) and 0xFF
} else {
crc = (crc shl 1) and 0xFF
}
}
}
return crc.toByte()
}
}

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

@ -22,10 +22,12 @@ import java.util.UUID
import java.util.regex.Pattern
/**
* BleCompanionManager
* BleCompanionManager - 单例模式
* 用于管理BLE设备的配对和关联
*
* 使用单例模式确保全局只有一个实例,避免重复初始化和资源浪费
*/
class BleCompanionManager(private val context: Context) {
class BleCompanionManager private constructor(private val context: Context) {
private val TAG = "BleCompanionManager"
// 常量定义
@ -34,6 +36,44 @@ class BleCompanionManager(private val context: Context) {
private const val PREF_NAME = "ble_companion_prefs"
private const val KEY_ASSOCIATION_IDS = "association_ids"
private const val KEY_PAIRED_MAC = "paired_mac"
private const val KEY_PAIRED_UUID = "paired_uuid"
// 单例实例
@Volatile
private var INSTANCE: BleCompanionManager? = null
/**
* 获取BleCompanionManager单例实例
* 使用双重检查锁定模式确保线程安全
*
* @param context 应用上下文,建议使用ApplicationContext
* @return BleCompanionManager实例
*/
fun getInstance(context: Context): BleCompanionManager {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: BleCompanionManager(context.applicationContext).also { INSTANCE = it }
}
}
/**
* 获取已初始化的实例(如果存在)
*
* @return BleCompanionManager实例,如果未初始化则返回null
*/
fun getInstanceIfInitialized(): BleCompanionManager? {
return INSTANCE
}
/**
* 销毁单例实例
* 主要用于测试或特殊情况下的清理
*/
fun destroyInstance() {
synchronized(this) {
INSTANCE?.cleanup()
INSTANCE = null
}
}
}
// 会话保存
@ -44,6 +84,36 @@ class BleCompanionManager(private val context: Context) {
// 用于验证的临时MAC地址
private var pendingMacAddress: String? = null
// 创建PendingIntent
private val pendingIntent by lazy {
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(context, BleWakeReceiver::class.java)
PendingIntent.getBroadcast(
context,
0,
scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
}
/**
* 清理资源
* 在销毁实例时调用
*/
private fun cleanup() {
try {
unregisterBackgroundScan()
pendingMacAddress = null
} catch (e: Exception) {
Log.e(TAG, "清理资源时发生错误: ${e.message}", e)
}
}
/**
* 获取CompanionDeviceManager服务(使用Activity上下文)
*/
@ -63,313 +133,344 @@ class BleCompanionManager(private val context: Context) {
}
/**
* 获取已关联的设备MAC地址
* 获取已连接的设备MAC地址
*/
fun getPairedMacAddress(): String? {
return sharedPreferences.getString(KEY_PAIRED_MAC, null)
}
/**
* 保存已关联的设备MAC地址
/**
* 保存连接的设备UUID
*/
fun savePairedMacAddress(macAddress: String) {
sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply()
Log.i(TAG, "已保存配对设备MAC: $macAddress")
}
/**
* 清除已关联的设备MAC地址
*/
fun clearPairedMacAddress() {
sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply()
Log.i(TAG, "已清除配对设备MAC")
}
/**
* 检查是否有已关联的设备
* 保存连接的设备UUID列表
* 将UUID列表转换为JSON字符串进行存储
*/
@RequiresApi(Build.VERSION_CODES.O)
fun hasAssociations(): Boolean {
val associationIds = getAssociationIds()
return associationIds.isNotEmpty()
}
/**
* 获取所有关联ID
*/
@RequiresApi(Build.VERSION_CODES.O)
fun getAssociationIds(): List<String> {
// 使用默认上下文获取CompanionDeviceManager
val companionDeviceManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager
} else {
null
fun savePairedUuid(uuids: List<String>) {
if (uuids.isNotEmpty()) {
// 将列表转换为JSON字符串格式进行存储
val uuidsJson = uuids.joinToString(",") { "\"$it\"" }
val jsonString = "[$uuidsJson]"
sharedPreferences.edit().putString(KEY_PAIRED_UUID, jsonString).apply()
Log.i(TAG, "已保存连接设备UUID列表: $uuids")
}
return getAssociationIds(companionDeviceManager)
}
/**
* 获取所有关联ID(使用指定的CompanionDeviceManager)
* 获取已连接的设备UUID列表
* 从JSON字符串解析为UUID列表
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun getAssociationIds(companionDeviceManager: CompanionDeviceManager?): List<String> {
if (companionDeviceManager == null) {
return emptyList()
}
// 注意: allAssociations 仅在API 33 (Android 13)及以上版本可用
// 在早期版本中,使用associations (返回的是String数组)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
fun getPairedUuid(): List<String> {
val jsonString = sharedPreferences.getString(KEY_PAIRED_UUID, null)
return if (!jsonString.isNullOrEmpty()) {
try {
// 由于AssociationInfo类在API 30+引入,使用反射获取更安全
val method = CompanionDeviceManager::class.java.getMethod("getAllAssociations")
@Suppress("UNCHECKED_CAST")
val associations = method.invoke(companionDeviceManager) as? List<Any>
associations?.map { association ->
// 尝试获取AssociationInfo.id
val idField = association.javaClass.getField("id")
idField.get(association).toString()
} ?: emptyList()
// 简单的JSON解析,移除方括号和引号,然后分割
jsonString.trim('[', ']')
.split(",")
.map { it.trim().trim('"') }
.filter { it.isNotEmpty() }
} catch (e: Exception) {
Log.e(TAG, "获取关联ID失败: ${e.message}", e)
Log.e(TAG, "解析UUID列表失败: ${e.message}")
emptyList()
}
} else {
// API 26-32 使用 associations 方法
companionDeviceManager.associations?.toList() ?: emptyList()
emptyList()
}
}
/**
* 保存连接的设备MAC地址
*/
fun savePairedMacAddress(macAddress: String) {
sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply()
Log.i(TAG, "已保存连接设备MAC: $macAddress")
}
/**
* 开始设备关联流程
*
* @param activity 当前Activity,用于启动系统关联对话框
* @param deviceNamePattern 设备名称正则表达式,例如"MyHeadset.*"
* @param serviceUuid 可选的服务UUID
* @param macAddress 可选的MAC地址
* 清除已连接的设备MAC地址
*/
@RequiresApi(Build.VERSION_CODES.O)
fun startAssociation(
activity: Activity,
deviceNamePattern: String? = null,
serviceUuid: String? = null,
macAddress: String? = null
): Boolean {
// 使用Activity上下文获取CompanionDeviceManager
val companionDeviceManager = getCompanionDeviceManager(activity)
if (companionDeviceManager == null) {
Log.e(TAG, "设备不支持CompanionDeviceManager")
return false
}
fun clearPairedMacAddress() {
sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply()
Log.i(TAG, "已清除连接设备MAC")
}
// /**
// * 检查是否有已关联的设备
// */
// @RequiresApi(Build.VERSION_CODES.O)
// fun hasAssociations(): Boolean {
// val associationIds = getAssociationIds()
// return associationIds.isNotEmpty()
// }
// /**
// * 获取所有关联ID
// */
// @RequiresApi(Build.VERSION_CODES.O)
// fun getAssociationIds(): List<String> {
// // 使用默认上下文获取CompanionDeviceManager
// val companionDeviceManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager
// } else {
// null
// }
// return getAssociationIds(companionDeviceManager)
// }
// /**
// * 获取所有关联ID(使用指定的CompanionDeviceManager)
// */
// @RequiresApi(Build.VERSION_CODES.O)
// private fun getAssociationIds(companionDeviceManager: CompanionDeviceManager?): List<String> {
// if (companionDeviceManager == null) {
// return emptyList()
// }
try {
// 构建设备过滤器
val deviceFilterBuilder = BluetoothDeviceFilter.Builder()
// // 注意: allAssociations 仅在API 33 (Android 13)及以上版本可用
// // 在早期版本中,使用associations (返回的是String数组)
// return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// try {
// // 由于AssociationInfo类在API 30+引入,使用反射获取更安全
// val method = CompanionDeviceManager::class.java.getMethod("getAllAssociations")
// @Suppress("UNCHECKED_CAST")
// val associations = method.invoke(companionDeviceManager) as? List<Any>
// associations?.map { association ->
// // 尝试获取AssociationInfo.id
// val idField = association.javaClass.getField("id")
// idField.get(association).toString()
// } ?: emptyList()
// } catch (e: Exception) {
// Log.e(TAG, "获取关联ID失败: ${e.message}", e)
// emptyList()
// }
// } else {
// // API 26-32 使用 associations 方法
// companionDeviceManager.associations?.toList() ?: emptyList()
// }
// }
// /**
// * 开始设备关联流程
// *
// * @param activity 当前Activity,用于启动系统关联对话框
// * @param deviceNamePattern 设备名称正则表达式,例如"MyHeadset.*"
// * @param serviceUuid 可选的服务UUID
// * @param macAddress 可选的MAC地址
// */
// @RequiresApi(Build.VERSION_CODES.O)
// fun startAssociation(
// activity: Activity,
// deviceNamePattern: String? = null,
// serviceUuid: String? = null,
// macAddress: String? = null
// ): Boolean {
// // 使用Activity上下文获取CompanionDeviceManager
// val companionDeviceManager = getCompanionDeviceManager(activity)
// if (companionDeviceManager == null) {
// Log.e(TAG, "设备不支持CompanionDeviceManager")
// return false
// }
// try {
// // 构建设备过滤器
// val deviceFilterBuilder = BluetoothDeviceFilter.Builder()
// 添加设备名称 - 严格匹配模式
deviceNamePattern?.let {
// 使用"^设备名称$"格式创建精确匹配的正则表达式
val exactNamePattern = Pattern.compile("^$it$")
deviceFilterBuilder.setNamePattern(exactNamePattern)
Log.i(TAG, "设置严格名称匹配: $it")
}
// // 添加设备名称 - 严格匹配模式
// deviceNamePattern?.let {
// // 使用"^设备名称$"格式创建精确匹配的正则表达式
// val exactNamePattern = Pattern.compile("^$it$")
// deviceFilterBuilder.setNamePattern(exactNamePattern)
// Log.i(TAG, "设置严格名称匹配: $it")
// }
// 添加服务UUID
serviceUuid?.let {
try {
val uuid = ParcelUuid(java.util.UUID.fromString(it))
deviceFilterBuilder.addServiceUuid(uuid, null)
Log.i(TAG, "设置服务UUID匹配: $it")
} catch (e: Exception) {
Log.e(TAG, "设置服务UUID失败: ${e.message}", e)
}
}
// // 添加服务UUID
// serviceUuid?.let {
// try {
// val uuid = ParcelUuid(java.util.UUID.fromString(it))
// deviceFilterBuilder.addServiceUuid(uuid, null)
// Log.i(TAG, "设置服务UUID匹配: $it")
// } catch (e: Exception) {
// Log.e(TAG, "设置服务UUID失败: ${e.message}", e)
// }
// }
// 添加MAC地址 - 严格匹配
macAddress?.let {
try {
// 检查是否是有效的MAC地址格式
if (!isValidMacAddress(it)) {
Log.e(TAG, "无效的MAC地址格式: $it")
return false
}
// // 添加MAC地址 - 严格匹配
// macAddress?.let {
// try {
// // 检查是否是有效的MAC地址格式
// if (!isValidMacAddress(it)) {
// Log.e(TAG, "无效的MAC地址格式: $it")
// return false
// }
// 存储MAC地址到临时变量,成功匹配后再保存
pendingMacAddress = it
// // 存储MAC地址到临时变量,成功匹配后再保存
// pendingMacAddress = it
// 设置MAC地址严格匹配
deviceFilterBuilder.setAddress(it)
Log.i(TAG, "设置MAC地址严格匹配: $it")
} catch (e: Exception) {
Log.e(TAG, "设置MAC地址失败: ${e.message}", e)
return false
}
} ?: run {
// 如果没有提供MAC地址也没有提供设备名称,则返回错误
if (deviceNamePattern == null && serviceUuid == null) {
Log.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一")
return false
}
}
// // 设置MAC地址严格匹配
// deviceFilterBuilder.setAddress(it)
// Log.i(TAG, "设置MAC地址严格匹配: $it")
// } catch (e: Exception) {
// Log.e(TAG, "设置MAC地址失败: ${e.message}", e)
// return false
// }
// } ?: run {
// // 如果没有提供MAC地址也没有提供设备名称,则返回错误
// if (deviceNamePattern == null && serviceUuid == null) {
// Log.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一")
// return false
// }
// }
// 创建请求
val associationRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilterBuilder.build())
.setSingleDevice(true) // 找到一个即停止
.build()
// // 创建请求
// val associationRequest = AssociationRequest.Builder()
// .addDeviceFilter(deviceFilterBuilder.build())
// .setSingleDevice(true) // 找到一个即停止
// .build()
// 发起关联请求
companionDeviceManager.associate(
associationRequest,
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(intentSender: IntentSender) {
try {
activity.startIntentSenderForResult(
intentSender,
REQUEST_CODE_ASSOCIATE,
null,
0,
0,
0
)
Log.i(TAG, "已启动设备选择对话框")
} catch (e: Exception) {
Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
}
}
// // 发起关联请求
// companionDeviceManager.associate(
// associationRequest,
// object : CompanionDeviceManager.Callback() {
// override fun onDeviceFound(intentSender: IntentSender) {
// try {
// activity.startIntentSenderForResult(
// intentSender,
// REQUEST_CODE_ASSOCIATE,
// null,
// 0,
// 0,
// 0
// )
// Log.i(TAG, "已启动设备选择对话框")
// } catch (e: Exception) {
// Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
// }
// }
// API 26 (Android 8) 使用 onDeviceFound
// API 28+ (Android 9+) 使用 onAssociationPending
@RequiresApi(Build.VERSION_CODES.P)
override fun onAssociationPending(intentSender: IntentSender) {
try {
activity.startIntentSenderForResult(
intentSender,
REQUEST_CODE_ASSOCIATE,
null,
0,
0,
0
)
Log.i(TAG, "已启动设备选择对话框")
} catch (e: Exception) {
Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
}
}
// // API 26 (Android 8) 使用 onDeviceFound
// // API 28+ (Android 9+) 使用 onAssociationPending
// @RequiresApi(Build.VERSION_CODES.P)
// override fun onAssociationPending(intentSender: IntentSender) {
// try {
// activity.startIntentSenderForResult(
// intentSender,
// REQUEST_CODE_ASSOCIATE,
// null,
// 0,
// 0,
// 0
// )
// Log.i(TAG, "已启动设备选择对话框")
// } catch (e: Exception) {
// Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
// }
// }
// API 33+ (Android 13+) 使用 onAssociationCreated
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onAssociationCreated(associationInfo: AssociationInfo) {
Log.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}")
// 保存关联ID
try {
val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress")
val mac = macAddressMethod.invoke(associationInfo) as? String
mac?.let { savePairedMacAddress(it) }
} catch (e: Exception) {
Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
}
}
// // API 33+ (Android 13+) 使用 onAssociationCreated
// @RequiresApi(Build.VERSION_CODES.TIRAMISU)
// override fun onAssociationCreated(associationInfo: AssociationInfo) {
// Log.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}")
// // 保存关联ID
// try {
// val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress")
// val mac = macAddressMethod.invoke(associationInfo) as? String
// mac?.let { savePairedMacAddress(it) }
// } catch (e: Exception) {
// Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
// }
// }
override fun onFailure(error: CharSequence?) {
Log.e(TAG, "设备关联失败: $error")
}
},
null
)
// override fun onFailure(error: CharSequence?) {
// Log.e(TAG, "设备关联失败: $error")
// }
// },
// null
// )
return true
} catch (e: Exception) {
Log.e(TAG, "启动设备关联失败: ${e.message}", e)
return false
}
}
// return true
// } catch (e: Exception) {
// Log.e(TAG, "启动设备关联失败: ${e.message}", e)
// return false
// }
// }
/**
* 处理关联结果
*
* @param requestCode 请求码
* @param resultCode 结果码
* @param data Intent数据
* @return 是否处理了关联结果
*/
@RequiresApi(Build.VERSION_CODES.O)
fun handleAssociationResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode != REQUEST_CODE_ASSOCIATE) {
return false
}
// /**
// * 处理关联结果
// *
// * @param requestCode 请求码
// * @param resultCode 结果码
// * @param data Intent数据
// * @return 是否处理了关联结果
// */
// @RequiresApi(Build.VERSION_CODES.O)
// fun handleAssociationResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
// if (requestCode != REQUEST_CODE_ASSOCIATE) {
// return false
// }
Log.i(TAG, "收到关联结果: resultCode=$resultCode")
// Log.i(TAG, "收到关联结果: resultCode=$resultCode")
if (resultCode == Activity.RESULT_OK && data != null) {
// 获取选中的蓝牙设备
val bluetoothDevice: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE, BluetoothDevice::class.java)
} else {
@Suppress("DEPRECATION")
data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
}
// if (resultCode == Activity.RESULT_OK && data != null) {
// // 获取选中的蓝牙设备
// val bluetoothDevice: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE, BluetoothDevice::class.java)
// } else {
// @Suppress("DEPRECATION")
// data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
// }
// 获取关联ID (Android 13+)
val associationId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
// 由于EXTRA_ASSOCIATION_ID常量仅在API 33+可用,直接使用已知的常量字符串值
// 不再尝试使用反射获取字段
val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID"
data.getStringExtra(extraAssociationIdKey)
} catch (e: Exception) {
Log.e(TAG, "获取关联ID失败: ${e.message}", e)
null
}
} else {
null
}
// // 获取关联ID (Android 13+)
// val associationId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// try {
// // 由于EXTRA_ASSOCIATION_ID常量仅在API 33+可用,直接使用已知的常量字符串值
// // 不再尝试使用反射获取字段
// val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID"
// data.getStringExtra(extraAssociationIdKey)
// } catch (e: Exception) {
// Log.e(TAG, "获取关联ID失败: ${e.message}", e)
// null
// }
// } else {
// null
// }
// 获取并验证MAC地址
if (bluetoothDevice != null) {
val deviceMac = bluetoothDevice.address
val deviceName = bluetoothDevice.name ?: "未知设备"
// bluetoothDevice?.let { device ->
// val deviceMac = device.address
// Log.i(TAG, "设备关联成功: ${device.name} ($deviceMac)")
// 验证MAC地址格式
if (!isValidMacAddress(deviceMac)) {
Log.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac")
return false
}
// // 验证MAC地址是否匹配预期
// pendingMacAddress?.let { expectedMac ->
// if (deviceMac.equals(expectedMac, ignoreCase = true)) {
// Log.i(TAG, "MAC地址验证成功: $deviceMac")
// } else {
// Log.w(TAG, "MAC地址不匹配: 预期=$expectedMac, 实际=$deviceMac")
// }
// }
// 检查是否与预期的MAC地址匹配
if (pendingMacAddress != null && pendingMacAddress != deviceMac) {
Log.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配")
// 如果要严格匹配MAC,可以在这里返回false
return false
}
// 匹配成功后,保存新的MAC地址
savePairedMacAddress(deviceMac)
Log.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId")
// // 保存设备MAC地址
// savePairedMacAddress(deviceMac)
// 清除临时MAC地址
pendingMacAddress = null
// // 保存关联ID (如果可用)
// associationId?.let { id ->
// Log.i(TAG, "保存关联ID: $id")
// // 这里可以保存关联ID到SharedPreferences或其他存储
// }
// 🎯 立即发起BLE连接
// try {
// Log.i(TAG, "配对成功,立即发起BLE连接: $deviceMac")
// BleService.connect(deviceMac)
// } catch (e: Exception) {
// Log.e(TAG, "配对成功后发起BLE连接失败: ${e.message}", e)
// }
// // 清除临时MAC地址
// pendingMacAddress = null
return true
} else {
Log.e(TAG, "无法获取关联的蓝牙设备信息")
}
} else {
Log.w(TAG, "设备关联被取消或失败: resultCode=$resultCode")
// 清除临时MAC地址
pendingMacAddress = null
}
return false
}
// return true
// } ?: run {
// Log.e(TAG, "无法获取蓝牙设备信息")
// return false
// }
// } else {
// Log.w(TAG, "设备关联被取消或失败")
// // 清除临时MAC地址
// pendingMacAddress = null
// return false
// }
// }
/**
* 注册后台扫描
@ -382,12 +483,14 @@ class BleCompanionManager(private val context: Context) {
) {
try {
Log.d(TAG, "添加后台扫描过滤器UUID: $uuids, macAddress: $macAddress")
val mac = getPairedMacAddress()
if (mac.isNullOrEmpty()) {
//val mac = getPairedMacAddress()
if (macAddress.isNullOrEmpty()||uuids.isNullOrEmpty()) {
Log.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址")
return
}
savePairedMacAddress(macAddress)
savePairedUuid(uuids)
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
val bluetoothAdapter = bluetoothManager?.adapter
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
@ -440,24 +543,24 @@ class BleCompanionManager(private val context: Context) {
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.build()
// 创建PendingIntent
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(context, BleWakeReceiver::class.java)
// // 创建PendingIntent
// val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
// .setClass(context, BleWakeReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
// val pendingIntent = PendingIntent.getBroadcast(
// context,
// 0,
// scanIntent,
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
// } else {
// PendingIntent.FLAG_UPDATE_CURRENT
// }
// )
// 启动后台扫描
bluetoothLeScanner.startScan(filters, settings, pendingIntent)
Log.i(TAG, "已注册后台扫描,目标设备MAC: $mac")
Log.i(TAG, "已注册后台扫描,目标设备MAC: $macAddress")
} catch (e: Exception) {
Log.e(TAG, "注册后台扫描失败: ${e.message}", e)
}
@ -477,20 +580,20 @@ class BleCompanionManager(private val context: Context) {
return
}
// 创建PendingIntent
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
.setClass(context, BleWakeReceiver::class.java)
// // 创建PendingIntent
// val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT)
// .setClass(context, BleWakeReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
scanIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
// val pendingIntent = PendingIntent.getBroadcast(
// context,
// 0,
// scanIntent,
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
// } else {
// PendingIntent.FLAG_UPDATE_CURRENT
// }
// )
// 停止后台扫描
bluetoothLeScanner.stopScan(pendingIntent)
@ -500,42 +603,42 @@ class BleCompanionManager(private val context: Context) {
}
}
/**
* 清除所有关联
*/
@RequiresApi(Build.VERSION_CODES.O)
fun clearAllAssociations() {
try {
// 获取CompanionDeviceManager
val companionDeviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager
if (companionDeviceManager == null) {
Log.e(TAG, "无法获取CompanionDeviceManager")
return
}
// /**
// * 清除所有关联
// */
// @RequiresApi(Build.VERSION_CODES.O)
// fun clearAllAssociations() {
// try {
// // 获取CompanionDeviceManager
// val companionDeviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager
// if (companionDeviceManager == null) {
// Log.e(TAG, "无法获取CompanionDeviceManager")
// return
// }
// 清除本地保存的MAC地址
clearPairedMacAddress()
// // 清除本地保存的MAC地址
// clearPairedMacAddress()
// 获取所有关联ID
val associationIds = getAssociationIds(companionDeviceManager)
// // 获取所有关联ID
// val associationIds = getAssociationIds(companionDeviceManager)
// 遍历解除关联
for (id in associationIds) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
companionDeviceManager.disassociate(id)
} else {
companionDeviceManager.disassociate(id)
}
Log.i(TAG, "已解除设备关联: $id")
} catch (e: Exception) {
Log.e(TAG, "解除设备关联失败: $id, ${e.message}", e)
}
}
} catch (e: Exception) {
Log.e(TAG, "清除所有关联失败: ${e.message}", e)
}
}
// // 遍历解除关联
// for (id in associationIds) {
// try {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// companionDeviceManager.disassociate(id)
// } else {
// companionDeviceManager.disassociate(id)
// }
// Log.i(TAG, "已解除设备关联: $id")
// } catch (e: Exception) {
// Log.e(TAG, "解除设备关联失败: $id, ${e.message}", e)
// }
// }
// } catch (e: Exception) {
// Log.e(TAG, "清除所有关联失败: ${e.message}", e)
// }
// }
/**
* 验证是否是有效的MAC地址格式

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

@ -1,72 +0,0 @@
package com.yunqiinnovation.ble_service
import android.companion.AssociationInfo
import android.companion.CompanionDeviceService
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import android.util.Log
/**
* BleCompanionService
* 用于监听配对设备的出现和消失
* 注:此服务仅在Android 11 (API 30)及以上版本可用
*/
@RequiresApi(Build.VERSION_CODES.R)
class BleCompanionService : CompanionDeviceService() {
private val TAG = "BleCompanionService"
override fun onCreate() {
super.onCreate()
Log.i(TAG, "BleCompanionService已创建")
}
override fun onDeviceAppeared(info: AssociationInfo) {
super.onDeviceAppeared(info)
Log.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}")
// 当配对设备出现时,启动前台服务
try {
val serviceIntent = Intent().apply {
// 尝试使用BleMediaService,如果不存在则回退到BleService
val serviceClassName = try {
Class.forName("com.yunqiinnovation.agent_service.BleMediaService").name
} catch (e: ClassNotFoundException) {
BleService::class.java.name
}
setClassName(this@BleCompanionService, serviceClassName)
action = "android.intent.action.DEVICE_APPEARED"
putExtra("assocId", info.id)
putExtra("displayName", info.displayName)
// 如果设备信息中有MAC地址,也可以传递
// 使用反射获取MAC地址,因为deviceMacAddress在不同API版本可能不同或不存在
try {
val macAddressMethod = info.javaClass.getMethod("getDeviceMacAddress")
val mac = macAddressMethod.invoke(info) as? String
mac?.let { putExtra("mac", it) }
} catch (e: Exception) {
Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
}
}
// 启动前台服务
startForegroundService(serviceIntent)
Log.i(TAG, "已启动服务")
} catch (e: Exception) {
Log.e(TAG, "启动服务失败: ${e.message}", e)
}
}
override fun onDeviceDisappeared(info: AssociationInfo) {
super.onDeviceDisappeared(info)
Log.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}")
// 可以选择停止服务或执行其他清理操作
}
// 注意: 不需要重写 onBindCompanionDeviceService 和 onUnbindCompanionDeviceService
// 这些方法在 Android 12+ 才添加,我们可以使用父类的默认实现
}

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

File diff suppressed because it is too large

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

@ -68,7 +68,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
BleService.addCallback(this)
// 初始化CompanionManager
bleCompanionManager = BleCompanionManager(context)
bleCompanionManager = BleCompanionManager.getInstance(context)
}
// 设置事件通道 - 简化版
@ -123,21 +123,21 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
// 实现ActivityResultListener接口
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
// 处理设备关联结果
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val handled = bleCompanionManager.handleAssociationResult(requestCode, resultCode, data)
if (handled) {
// 返回设备关联结果
pendingAssociationResult?.success(true)
pendingAssociationResult = null
return true
} else {
pendingAssociationResult?.success(false)
pendingAssociationResult = null
return true
}
}
return false
// // 处理设备关联结果
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// val handled = bleCompanionManager.handleAssociationResult(requestCode, resultCode, data)
// if (handled) {
// // 返回设备关联结果
// pendingAssociationResult?.success(true)
// pendingAssociationResult = null
// return true
// } else {
// pendingAssociationResult?.success(false)
// pendingAssociationResult = null
// return true
// }
// }
return true
}
// 添加sendEvent函数
@ -247,18 +247,18 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
return
}
// 启动设备关联
val success = bleCompanionManager.startAssociation(
currentActivity,
deviceNamePattern,
serviceUuid,
macAddress
)
// // 启动设备关联
// val success = bleCompanionManager.startAssociation(
// currentActivity,
// deviceNamePattern,
// serviceUuid,
// macAddress
// )
if (!success) {
pendingAssociationResult = null
result.error("FAILED", "启动设备关联失败", null)
}
// if (!success) {
// pendingAssociationResult = null
// result.error("FAILED", "启动设备关联失败", null)
// }
// 不要立即返回结果,等待onActivityResult回调
}
"getPairedMacAddress" -> {
@ -304,7 +304,8 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
}
"clearAssociations" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
bleCompanionManager.clearAllAssociations()
bleCompanionManager.unregisterBackgroundScan() //停止后台扫描
bleCompanionManager.clearPairedMacAddress()
result.success(true)
} else {
result.error("UNSUPPORTED", "设备不支持Companion Device Manager", null)

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

@ -37,32 +37,70 @@ class BleWakeReceiver : BroadcastReceiver() {
}
}
/**
* 启动BLE服务的改进方法
* 处理Android系统对后台服务启动的限制
*/
private fun startBleService(context: Context) {
try {
// 检查是否存在BleMediaService类,如果不存在则使用BleService
// 检查是否存在BleMediaService类
val serviceClass = try {
Log.d(TAG, "startBleService存在")
Class.forName("com.yunqiinnovation.agent_service.BleMediaService")
} catch (e: ClassNotFoundException) {
Log.i(TAG, "BleMediaService不存在")
return
}
// 创建服务Intent,并携带MAC地址
// 创建服务Intent
val serviceIntent = Intent().apply {
setClassName(context, serviceClass.name)
action = BleConst.ACTION_SCAN_RESULT
}
// 启动前台服务
// 启动服务的改进策略
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
Log.i(TAG, "已启动前台服务: ${serviceClass.name}")
try {
// 首先尝试启动前台服务
Log.i(TAG, "尝试启动前台服务: ${serviceClass.name}")
context.startForegroundService(serviceIntent)
Log.i(TAG, "前台服务启动成功")
} catch (e: IllegalStateException) {
Log.w(TAG, "前台服务启动失败,尝试其他方式: ${e.message}")
// 如果前台服务启动失败,尝试通过Activity启动
try {
Log.i(TAG, "通过Activity启动服务成功: ${serviceClass.name}")
val activityIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra("auto_start_ble_service", true)
putExtra("ble_service_action", BleConst.ACTION_SCAN_RESULT)
}
if (activityIntent != null) {
context.startActivity(activityIntent)
Log.i(TAG, "通过Activity启动应用,将在前台启动服务")
} else {
Log.e(TAG, "无法获取应用启动Intent")
}
} catch (activityException: Exception) {
Log.e(TAG, "通过Activity启动也失败: ${activityException.message}")
}
} catch (e: Exception) {
Log.e(TAG, "启动前台服务时发生未知错误: ${e.message}", e)
}
} else {
context.startService(serviceIntent)
Log.i(TAG, "已启动服务: ${serviceClass.name}")
// Android 8.0以下直接启动服务
try {
context.startService(serviceIntent)
Log.i(TAG, "已启动服务: ${serviceClass.name}")
} catch (e: Exception) {
Log.e(TAG, "启动服务失败: ${e.message}", e)
}
}
} catch (e: Exception) {
Log.e(TAG, "启动BLE服务失败: ${e.message}", e)
}
}
}
}

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

@ -12,15 +12,22 @@ class BootReceiver : BroadcastReceiver() {
private val TAG = "BootReceiver"
override fun onReceive(ctx: Context, i: Intent) {
val serviceClass = try {
Log.d(TAG, "BleMediaService不存在")
Class.forName("com.yunqiinnovation.agent_service.BleMediaService")
} catch (e: ClassNotFoundException) {
Log.i(TAG, "BleMediaService不存在")
Log.d(TAG, "BleMediaService不存在")
return
}
if (i.action == Intent.ACTION_BOOT_COMPLETED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
Log.d(TAG, "BleMediaService")
ctx.startForegroundService(Intent(ctx, serviceClass))
else ctx.startService(Intent(ctx, serviceClass))
}
else{
Log.d(TAG, "BleMediaService")
ctx.startService(Intent(ctx, serviceClass))
}
}
}
}

506
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt

@ -0,0 +1,506 @@
package com.yunqiinnovation.ble_service
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.jieli.jl_audio_decode.opus.model.OpusOption
import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback
import com.jieli.jl_audio_decode.callback.OnEncodeStreamCallback
import com.jieli.jl_audio_decode.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
/**
* Opus音频编解码管理器
* 负责处理音频数据的Opus编码和解码功能
*
* 功能包括:
* 1. Opus解码流管理
* 2. Opus编码流管理
* 3. 音频数据缓冲和处理
* 4. 编解码状态管理
*/
class OpusAudioManager {
companion object {
private const val TAG = "OpusAudioManager"
// 音频数据分块发送的常量
private const val AUDIO_CHUNK_SIZE = 120 // 每次发送120字节
private const val AUDIO_SEND_INTERVAL = 60L // 发送间隔60ms
// 重发机制相关常量
private const val MAX_RETRY_COUNT = 1 // 最大重试次数
private const val RETRY_DELAY = 30L // 重试延迟时间(毫秒)
}
// ======================================================================================================
// 回调接口定义
// ======================================================================================================
/**
* Opus音频管理器回调接口
*/
interface OpusAudioCallback {
/**
* 音频数据解码完成回调
* @param data 解码后的音频数据
* @param channel 音频通道数
*/
fun onAudioDataDecoded(data: ByteArray, channel: Int)
/**
* 音频数据编码完成回调
* @param data 编码后的音频数据
*/
fun onAudioDataEncoded(data: ByteArray)
/**
* 解码流状态变化回调
* @param isStarted 是否已启动
*/
fun onDecodeStreamStateChanged(isStarted: Boolean)
/**
* 编码流状态变化回调
* @param isStarted 是否已启动
*/
fun onEncodeStreamStateChanged(isStarted: Boolean)
/**
* 错误回调
* @param errorType 错误类型 (decode/encode)
* @param code 错误代码
* @param message 错误信息
*/
fun onError(errorType: String, code: Int, message: String?)
}
// ======================================================================================================
// 成员变量
// ======================================================================================================
// Opus解码器实例
private var opusManager: OpusManager? = null
private var option: OpusOption? = null
private var opusManager1: OpusManager? = null
private var option1: OpusOption? = null
private val mainHandler = Handler(Looper.getMainLooper())
// 解码音频数据队列和处理线程
private val audioDataQueue = LinkedBlockingQueue<ByteArray>()
private var audioQueueProcessorThread: Thread? = null
// 音频数据缓存
private val audioDataBuffer = mutableListOf<Byte>()
// 音频数据发送相关
private val audioSendQueue = LinkedBlockingQueue<ByteArray>()
private var audioSendThread: Thread? = null
private val audioSendHandler = Handler(Looper.getMainLooper())
private val isAudioSending = AtomicBoolean(false)
// 音频数据缓冲区,用于累积数据到指定字节再发送
private val audioBuffer = mutableListOf<Byte>()
// 回调接口
private var callback: OpusAudioCallback? = null
// 初始化状态
private var isInitialized = false
// ======================================================================================================
// 初始化和配置方法
// ======================================================================================================
/**
* 初始化Opus音频管理器
* @return 是否初始化成功
*/
fun initialize(): Boolean {
if (isInitialized) return true
try {
// 初始化OpusManager和OpusOption
opusManager = OpusManager()
option = OpusOption()
opusManager1 = OpusManager()
option1 = OpusOption()
// 启动队列处理
startAudioQueueProcessing()
isInitialized = true
Log.d(TAG, "OpusAudioManager初始化成功")
return true
} catch (e: OpusException) {
Log.e(TAG, "OpusAudioManager初始化失败: ${e.message}", e)
return false
}
}
/**
* 设置回调接口
* @param callback 回调接口实例
*/
fun setCallback(callback: OpusAudioCallback?) {
this.callback = callback
}
/**
* 清理资源
*/
fun cleanup() {
Log.d(TAG, "开始清理OpusAudioManager资源")
// 停止编解码流
stopDecodeStream()
stopEncodeStream()
// 停止音频发送线程
stopAudioSendThread()
// 停止音频队列处理线程
audioQueueProcessorThread?.interrupt()
audioQueueProcessorThread = null
// 清理缓存
synchronized(audioDataBuffer) {
audioDataBuffer.clear()
}
synchronized(audioBuffer) {
audioBuffer.clear()
}
audioDataQueue.clear()
audioSendQueue.clear()
// 清理Opus相关资源
opusManager = null
option = null
opusManager1 = null
option1 = null
callback = null
isInitialized = false
Log.d(TAG, "OpusAudioManager资源清理完成")
}
// ======================================================================================================
// Opus解码相关方法
// ======================================================================================================
/**
* 开始Opus数据流解码
* @param hasHeader 是否包含文件头,通常BLE传输的Opus没有文件头
* @param channel 音频通道数
* @param sampleRate 采样率,确认设备端Opus编码采样率
* @param packetSize 包大小,确认设备端Opus编码帧长
* @return 是否成功开始解码
*/
fun startDecodeStream(
hasHeader: Boolean = false,
channel: Int = 1,
sampleRate: Int = 16000,
packetSize: Int = 40
): Boolean {
if (!isInitialized || opusManager == null) {
Log.e(TAG, "OpusManager未初始化,无法开始解码")
return false
}
// 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
Log.d(TAG, "已停止正在进行的Opus解码流")
}
// 清理音频数据缓存,确保开始时是干净的状态
synchronized(audioDataBuffer) {
audioDataBuffer.clear()
Log.d(TAG, "开始解码前已清理音频数据缓存")
}
// 配置解码参数
option?.apply {
setHasHead(hasHeader)
setChannel(channel)
setSampleRate(sampleRate)
setPacketSize(packetSize)
}
Log.d(TAG, "准备开始Opus数据流解码, 参数: hasHeader=$hasHeader, channel=$channel, sampleRate=$sampleRate, packetSize=$packetSize")
try {
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
val channelCount = option?.getChannel() ?: 1
if (channelCount >= 1) {
callback?.onAudioDataDecoded(data, channelCount)
} else {
// Log.e(TAG, "Opus解码数据错误: ${data.size} bytes")
}
}
}
override fun onStart() {
Log.i(TAG, "Opus数据流解码已开始")
callback?.onDecodeStreamStateChanged(true)
}
override fun onComplete(outPath: String?) {
Log.i(TAG, "Opus数据流解码完成: $outPath")
callback?.onDecodeStreamStateChanged(false)
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "Opus数据流解码错误: [$code] $message")
callback?.onError("decode", code, message)
callback?.onDecodeStreamStateChanged(false)
}
})
return true
} catch (e: Exception) {
Log.e(TAG, "启动Opus解码流失败: ${e.message}", e)
callback?.onError("decode", -1, e.message)
return false
}
}
/**
* 停止Opus数据流解码
* @return 是否成功停止解码
*/
fun stopDecodeStream(): Boolean {
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
// 清理音频数据缓存
synchronized(audioDataBuffer) {
audioDataBuffer.clear()
Log.d(TAG, "已清理音频数据缓存")
}
Log.i(TAG, "已停止Opus数据流解码")
callback?.onDecodeStreamStateChanged(false)
return true
}
Log.d(TAG, "Opus数据流未在解码或OpusManager未初始化")
return false
}
/**
* 处理接收到的音频数据进行解码
* @param data 音频数据 (Opus编码)
*/
fun processAudioData(data: ByteArray) {
if (!isInitialized) {
Log.w(TAG, "OpusAudioManager未初始化,忽略音频数据")
return
}
if (opusManager?.isDecodeStream == true) {
// 将数据加入队列进行异步处理
audioDataQueue.offer(data)
} else {
Log.d(TAG, "Opus解码流未启动,忽略音频数据")
}
}
// ======================================================================================================
// Opus编码相关方法
// ======================================================================================================
/**
* 开始Opus数据流编码
* @return 是否成功开始编码
*/
fun startEncodeStream(): Boolean {
if (!isInitialized || opusManager1 == null) {
Log.e(TAG, "OpusManager未初始化,无法开始编码")
return false
}
// 如果已经在编码流,先停止
if (opusManager1?.isEncodeStream == true) {
Log.d(TAG, "Opus编码流已在运行")
return true
}
// 配置解码参数
option1?.apply {
setHasHead(false)
setChannel(1)
setSampleRate(16000)
setPacketSize(40)
}
try {
Log.d(TAG, "准备开始Opus数据流编码")
opusManager1?.startEncodeStream(object : OnEncodeStreamCallback {
override fun onEncodeStream(data: ByteArray?) {
Log.d(TAG, "Opus编码回调收到数据,大小: ${data?.size ?: 0} bytes")
if (data != null) {
// 编码完成的数据处理:
// 1. 通知回调
callback?.onAudioDataEncoded(data)
// // 2. 将编码后的数据加入发送队列进行分块发送
// addAudioDataToSendQueue(data)
} else {
Log.w(TAG, "编码回调收到空数据")
}
}
override fun onStart() {
Log.i(TAG, "Opus数据流编码已开始")
callback?.onEncodeStreamStateChanged(true)
}
override fun onComplete(outPath: String?) {
Log.i(TAG, "Opus数据流编码完成: $outPath")
callback?.onEncodeStreamStateChanged(false)
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "Opus数据流编码错误: [$code] $message")
callback?.onError("encode", code, message)
callback?.onEncodeStreamStateChanged(false)
}
})
return true
} catch (e: Exception) {
Log.e(TAG, "启动Opus编码流失败: ${e.message}", e)
callback?.onError("encode", -1, e.message)
return false
}
}
/**
* 停止Opus数据流编码
* @return 是否成功停止编码
*/
fun stopEncodeStream(): Boolean {
if (opusManager1?.isEncodeStream == true) {
opusManager1?.stopEncodeStream()
stopAudioSendThread() // 停止音频发送线程
Log.i(TAG, "已停止Opus数据流编码")
callback?.onEncodeStreamStateChanged(false)
return true
}
Log.d(TAG, "Opus数据流未在编码或OpusManager未初始化")
return false
}
/**
* 写入外部音频数据到Opus编码流
* 该函数用于将外部音频数据(如麦克风录音)写入到Opus编码器中
*
* @param data 音频数据字节数组,通常为PCM格式
*/
fun writeExternalAudioData(data: ByteArray) {
if (!isInitialized || opusManager1 == null) {
Log.e(TAG, "OpusManager未初始化")
return
}
// 如果已经在编码流中,直接写入数据
if (opusManager1?.isEncodeStream == true) {
Log.d(TAG, "正在进行Opus编码流,写入音频数据")
// 将外部音频数据写入编码流
opusManager1?.writeEncodeStream(data)
} else {
Log.w(TAG, "Opus编码流未启动,无法写入音频数据")
}
}
// ======================================================================================================
// 状态查询方法
// ======================================================================================================
/**
* 检查是否正在解码
* @return 是否正在解码
*/
fun isDecoding(): Boolean {
return opusManager?.isDecodeStream == true
}
/**
* 检查是否正在编码
* @return 是否正在编码
*/
fun isEncoding(): Boolean {
return opusManager1?.isEncodeStream == true
}
/**
* 检查是否已初始化
* @return 是否已初始化
*/
fun isInitialized(): Boolean {
return isInitialized
}
// ======================================================================================================
// 私有方法 - 音频数据处理
// ======================================================================================================
/**
* 停止音频发送线程
*/
private fun stopAudioSendThread() {
isAudioSending.set(false)
audioSendThread?.interrupt()
try {
audioSendThread?.join(1000) // 等待最多1秒
} catch (e: InterruptedException) {
Log.w(TAG, "等待音频发送线程停止时被中断")
}
audioSendThread = null
audioSendQueue.clear()
synchronized(audioBuffer) {
audioBuffer.clear()
}
Log.d(TAG, "音频发送线程已停止")
}
/**
* 启动音频队列处理线程
*/
private fun startAudioQueueProcessing() {
audioQueueProcessorThread = Thread {
Log.d(TAG, "音频队列处理线程已启动")
while (!Thread.currentThread().isInterrupted) {
try {
// 从队列中取出音频数据进行处理
val audioData = audioDataQueue.take() // 阻塞等待
// 将音频数据写入Opus解码流
opusManager?.writeAudioStream(audioData)
} catch (e: InterruptedException) {
Log.d(TAG, "音频队列处理线程被中断")
break
} catch (e: Exception) {
Log.e(TAG, "音频队列处理异常: ${e.message}", e)
}
}
Log.d(TAG, "音频队列处理线程已停止")
}
audioQueueProcessorThread?.start()
}
}

160
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/RecordingFile.kt

@ -0,0 +1,160 @@
package com.yunqiinnovation.ble_service
import android.content.Context
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
/**
* 录音文件管理类:负责音频数据的异步写入和文件管理
*/
class RecordingFile(private val context: Context) {
private var currentAudioFile: File? = null
private var fos: FileOutputStream? = null
// 新增:用于异步写入的队列和线程
private val writeQueue = LinkedBlockingQueue<ByteArray>()
private val isWriting = AtomicBoolean(false)
private var writeThread: Thread? = null
// 用于存储音频数据的缓冲区
private val dataBuffer = mutableListOf<ByteArray>()
private var totalBytesWritten = 0
var fileName = ""
internal fun creatingFiles() {
if (fos != null || currentAudioFile != null) {
return
}
// 创建新的音频文件
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
val timestamp = dateFormat.format(Date())
val filePath =
File(context.getExternalFilesDir(null), "${fileName}_${timestamp}.txt").absolutePath
currentAudioFile = File(filePath)
currentAudioFile?.createNewFile()
// 追加音频数据到文件
fos = FileOutputStream(currentAudioFile, true)
startWriteThread() // 新增:启动写入线程
Log.d("TAG", "创建文件: ${currentAudioFile?.absolutePath}")
}
// 新增:启动写入线程
private fun startWriteThread() {
if (isWriting.get()) return
isWriting.set(true)
writeThread = Thread {
try {
while (isWriting.get() || writeQueue.isNotEmpty()) {
val data = writeQueue.poll() ?: continue
fos?.write(data)
}
} catch (e: Exception) {
Log.e("", "异步写入音频数据失败: ${e.message}")
}
}
writeThread?.start()
}
/**
* 保存音频数据到 opus 文件(异步)
* @param buffer 音频数据字节数组
*/
internal fun saveAudioDataToWav(buffer: ByteArray) {
if (fos == null || currentAudioFile == null) return
// 放入队列,由写线程写入
// // 1. 将字节数据转换为十六进制字符串
// val hexData = buildString {
// buffer.forEachIndexed { index, byte ->
// append("%02X".format(byte))
// }
// }
// 2. 写入十六进制字符串
writeQueue.offer(buffer)
totalBytesWritten += buffer.size
}
/**
* 保存日志信息到文件(重载方法)
* 使用UTF-8编码处理字符串
* @param logMessage 日志消息字符串
*/
internal fun saveAudioDataToWav(logMessage: String) {
if (fos == null || currentAudioFile == null) return
try {
// 使用UTF-8编码将字符串转换为字节数组,并添加换行符
val logBytes = (logMessage + "\n").toByteArray(Charsets.UTF_8)
writeQueue.offer(logBytes)
totalBytesWritten += logBytes.size
Log.d("RecordingFile", "保存日志信息: ${logMessage.length} 字符, ${logBytes.size} 字节 (UTF-8)")
} catch (e: Exception) {
Log.e("RecordingFile", "UTF-8编码转换失败: ${e.message}")
}
}
/**
* 将音频数据转换为UTF-8编码的十六进制字符串格式并保存
* @param buffer 音频数据字节数组
* @param addTimestamp 是否添加时间戳,默认为true
*/
internal fun saveAudioDataAsUtf8Hex(buffer: ByteArray, addTimestamp: Boolean = true) {
if (fos == null || currentAudioFile == null) return
try {
val timestamp = if (addTimestamp) {
"[${SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())}] "
} else ""
// 将字节数组转换为十六进制字符串
val hexString = buffer.joinToString(" ") { "%02X".format(it) }
val dataLine = "${timestamp}音频数据 (${buffer.size} 字节): $hexString\n"
// 使用UTF-8编码转换为字节数组
val utf8Bytes = dataLine.toByteArray(Charsets.UTF_8)
writeQueue.offer(utf8Bytes)
totalBytesWritten += utf8Bytes.size
Log.d("RecordingFile", "保存UTF-8格式音频数据: ${buffer.size} 字节 -> ${utf8Bytes.size} UTF-8字节")
} catch (e: Exception) {
Log.e("RecordingFile", "UTF-8格式保存失败: ${e.message}")
}
}
internal fun closeFile() {
try {
if (fos == null || currentAudioFile == null) {
return
}
isWriting.set(false)
writeThread?.join(500) // 等待写线程结束
fos?.close()
// 更新WAV文件头
currentAudioFile?.let { file ->
// 如果文件小于44字节,直接删除
if (file.length() <= 0) {
file.delete()
Log.d("TAG", "音频文件过小已删除: ${file.absolutePath}")
} else {
Log.d("TAG", "音频文件保存完成: ${file.absolutePath}")
}
}
} catch (e: Exception) {
Log.e("TAG", "更新opus失败: ${e.message}")
} finally {
fos = null
currentAudioFile = null
}
}
}

20
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

@ -635,7 +635,25 @@ private var cmdReplyType: UInt8 = 0
//return result
return true
}
/// 打开B1编码器
public func openB1Encoder() -> Bool {
os_log("🎙️ 准备打开编码(设备侧)...", log: logger, type: .info)
// 确保Opus解码器处于活跃状态(如果不是则启动)
if opusProcessor?.isDecoding() != true {
startOpusDecoding()
}
let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT])
let result = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
if result {
os_log("✅ 编码器命令发送成功", log: logger, type: .info)
} else {
os_log("❌ 编码器命令发送失败", log: logger, type: .error)
}
return result
}
/// 打开解码器
func openDecoder() -> Bool {
os_log("打开解码...", log: logger, type: .info)

2
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift

@ -141,7 +141,7 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
case "getPairedMacAddress":
let pairedUUID = BleService.shared.getPairedMacAddress()
result(pairedUUID)
result(true)
case "clearAssociations":
BleService.shared.clearAssociations()
result(true)

2
local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift

@ -282,7 +282,7 @@ class SwiftOpusAudioProcessor: NSObject {
return
}
print("Opus解码数据: \(data.count) bytes, 声道数: \(channels)")
// print("Opus解码数据: \(data.count) bytes, 声道数: \(channels)")
DispatchQueue.main.async { [weak self] in
self?.callback?.onAudioDataReceived(data: data, channel: self?.channels ?? 0)
}

2
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -631,7 +631,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
// 调用MCP工具
val toolResult = try {
withTimeout(10000) { // 100秒超时
withTimeout(20000) { // 100秒超时
// 再次检查会话状态
if (sessionid != currSessionId) {
throw CancellationException("Session cancelled")

2
local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift

@ -501,7 +501,7 @@ private func processToolCalls(_ sessionId:String) async -> Bool {
} else {
// 使用8秒超时调用MCP工具
do {
let toolResult = try await withTimeout(seconds: 8.0) {
let toolResult = try await withTimeout(seconds: 20.0) {
return await client.callTool(name: firstToolCall.name, arguments: args)
}

2
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -195,7 +195,7 @@ public class SystemFunctionHandler: NSObject {
let handler = GetContactsHandler()
_ = client.registerLocalFunction(
name: "get_contacts",
description: "获取通讯录中的所有联系人",
description: "获取联系人列表,读取通讯录信息,联系人名称和电话",
parameters: parameters,
handler: handler
)

70
local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift

@ -16,21 +16,21 @@ public class ChatStorageHelper {
private let logger = OSLog(subsystem: "com.yunqiinnovation.chat_storage", category: "ChatStorageHelper")
private init() {
// 获取文档目录路径
// 获取文档目录路径(不变)
let fileURL = try! FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("chat_storage.db")
dbPath = fileURL.path
// 打开数据库
// 打开数据库(不变)
if sqlite3_open(dbPath, &db) != SQLITE_OK {
let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("无法打开数据库: %{public}@", log: logger, type: .error, errmsg)
return
}
// 创建消息表
// 关键修改:新增 UNIQUE (agent_id, session_id, sender) 联合唯一索引
let createTableString = """
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -39,9 +39,10 @@ public class ChatStorageHelper {
message TEXT NOT NULL,
sender TEXT NOT NULL,
metadata TEXT,
created_at INTEGER NOT NULL
created_at INTEGER NOT NULL,
UNIQUE (agent_id, session_id, sender) ON CONFLICT REPLACE
);
CREATE INDEX IF NOT EXISTS idx_agent_id ON messages (session_id);
CREATE INDEX IF NOT EXISTS idx_agent_id ON messages (agent_id);
CREATE INDEX IF NOT EXISTS idx_session_id ON messages (session_id);
CREATE INDEX IF NOT EXISTS idx_created_at ON messages (created_at);
"""
@ -66,38 +67,46 @@ public class ChatStorageHelper {
* @param metadata 可选的元数据
* @return 插入的消息ID,失败则返回-1
*/
public func saveMessage(agentId:String,sessionId: String, message: String, sender: String, metadata: String?) -> Int64 {
let insertStatementString = "INSERT INTO messages (agent_id,session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?);"
public func saveMessage(agentId: String, sessionId: String, message: String, sender: String, metadata: String?) -> Int64 {
// 关键修改:用 INSERT OR REPLACE 替换 INSERT,支持冲突时更新
let insertStatementString = """
INSERT OR REPLACE INTO messages
(agent_id, session_id, message, sender, metadata, created_at)
VALUES (?, ?, ?, ?, ?, ?);
"""
var insertStatement: OpaquePointer?
if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
// 绑定参数(逻辑不变,确保三个唯一字段正确传入)
sqlite3_bind_text(insertStatement, 1, (agentId as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 1, (sessionId as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 2, (message as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 3, (sender as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 2, (sessionId as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 3, (message as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 4, (sender as NSString).utf8String, -1, nil)
if let metadata = metadata {
sqlite3_bind_text(insertStatement, 4, (metadata as NSString).utf8String, -1, nil)
sqlite3_bind_text(insertStatement, 5, (metadata as NSString).utf8String, -1, nil)
} else {
sqlite3_bind_null(insertStatement, 4)
sqlite3_bind_null(insertStatement, 5)
}
let currentTime = Int(Date().timeIntervalSince1970)
sqlite3_bind_int(insertStatement, 5, Int32(currentTime))
sqlite3_bind_int(insertStatement, 6, Int32(currentTime))
// 执行语句(冲突时会自动替换,返回新的 rowid)
if sqlite3_step(insertStatement) == SQLITE_DONE {
let id = sqlite3_last_insert_rowid(db)
let id = sqlite3_last_insert_rowid(db) // 替换后返回新的 id(原 id 会被删除)
sqlite3_finalize(insertStatement)
os_log("消息保存成功(新增/更新),id: %{public}lld", log: logger, type: .info, id)
return id
} else {
let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("插入消息失败: %{public}@", log: logger, type: .error, errmsg)
os_log("插入/更新消息失败: %{public}@", log: logger, type: .error, errmsg)
}
sqlite3_finalize(insertStatement)
} else {
let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("插入消息语句准备失败: %{public}@", log: logger, type: .error, errmsg)
os_log("插入/更新消息语句准备失败: %{public}@", log: logger, type: .error, errmsg)
}
return -1
@ -131,7 +140,7 @@ public class ChatStorageHelper {
let totalPages = totalCount == 0 ? 0 : (totalCount + pageSize - 1) / pageSize
let queryString = """
SELECT id, message, sender, metadata, created_at
SELECT id, session_id, message, sender, metadata, created_at
FROM messages
WHERE agent_id = ?
ORDER BY created_at DESC
@ -148,16 +157,19 @@ public class ChatStorageHelper {
while sqlite3_step(queryStatement) == SQLITE_ROW {
let id = sqlite3_column_int(queryStatement, 0)
let messagePtr = sqlite3_column_text(queryStatement, 1)
let sessionIdPtr = sqlite3_column_text(queryStatement, 1)
let sessionId = sessionIdPtr != nil ? String(cString: sessionIdPtr!) : ""
let messagePtr = sqlite3_column_text(queryStatement, 2)
let message = messagePtr != nil ? String(cString: messagePtr!) : ""
let senderPtr = sqlite3_column_text(queryStatement, 2)
let senderPtr = sqlite3_column_text(queryStatement, 3)
let sender = senderPtr != nil ? String(cString: senderPtr!) : ""
let metadataPtr = sqlite3_column_text(queryStatement, 3)
let metadataPtr = sqlite3_column_text(queryStatement, 4)
let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil
let createdAt = sqlite3_column_int(queryStatement, 4)
let createdAt = sqlite3_column_int(queryStatement, 5)
// 将时间戳转换为ISO 8601格式的字符串
let date = Date(timeIntervalSince1970: TimeInterval(createdAt))
@ -167,6 +179,7 @@ public class ChatStorageHelper {
var messageDict: [String: Any] = [
"id": id,
"agentId": agentId, // 添加agentId字段
"sessionId": sessionId, // 添加sessionId字段
"message": message,
"sender": sender,
"timestamp": timestamp // 使用timestamp而不是created_at
@ -323,7 +336,7 @@ public class ChatStorageHelper {
// 构建查询语句 - 按时间倒序获取最近的N条,然后在结果中再按时间正序
let queryString = """
SELECT * FROM (
SELECT id, message, sender, metadata, created_at
SELECT id, session_id, message, sender, metadata, created_at
FROM messages
WHERE agent_id = ?
ORDER BY created_at DESC
@ -340,19 +353,24 @@ public class ChatStorageHelper {
while sqlite3_step(queryStatement) == SQLITE_ROW {
let id = sqlite3_column_int(queryStatement, 0)
let messagePtr = sqlite3_column_text(queryStatement, 1)
let sessionIdPtr = sqlite3_column_text(queryStatement, 1)
let sessionId = sessionIdPtr != nil ? String(cString: sessionIdPtr!) : ""
let messagePtr = sqlite3_column_text(queryStatement, 2)
let message = messagePtr != nil ? String(cString: messagePtr!) : ""
let senderPtr = sqlite3_column_text(queryStatement, 2)
let senderPtr = sqlite3_column_text(queryStatement, 3)
let sender = senderPtr != nil ? String(cString: senderPtr!) : ""
let metadataPtr = sqlite3_column_text(queryStatement, 3)
let metadataPtr = sqlite3_column_text(queryStatement, 4)
let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil
let createdAt = sqlite3_column_int(queryStatement, 4)
let createdAt = sqlite3_column_int(queryStatement, 5)
var messageDict: [String: Any] = [
"id": id,
"sessionId":sessionId,
"message": message,
"sender": sender,
"created_at": createdAt

2
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans
description: "Voitrans - AI Voice Assistant."
publish_to: "none"
version: 1.0.16+41
version: 1.0.17+46
environment:
sdk: ">=3.3.0 <4.0.0"

Loading…
Cancel
Save