From cec61d8a9ab7b7f9c78430fddd42a665c4dc0f9d Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Wed, 22 Apr 2026 16:37:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=9C=BC=E7=9D=9B=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E7=9B=B4=E6=8E=A5=E8=BF=9B=E5=85=A5=E9=80=9A=E8=AF=9D?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/saitong/voitrans/MyApplication.kt | 52 +++++++++++++++ lib/data/services/ble_manager.dart | 63 ++++++++++++++++++- .../controllers/translation_controller.dart | 48 ++++++++++++++ .../ble_service/BleCommandSender.kt | 26 ++++++++ .../yunqiinnovation/ble_service/BleConst.kt | 4 ++ .../yunqiinnovation/ble_service/BleService.kt | 6 +- .../Sources/ble_service/BleConst.swift | 4 ++ .../Sources/ble_service/BleService.swift | 40 ++++++++++-- 8 files changed, 234 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt b/android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt index 17e5ee3b0..88cf5f539 100644 --- a/android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt +++ b/android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt @@ -8,6 +8,8 @@ import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.plugins.GeneratedPluginRegistrant +import java.io.OutputStream +import java.io.PrintStream // Application.kt class MyApp : FlutterApplication() { @@ -29,6 +31,9 @@ class MyApp : FlutterApplication() { super.onCreate() appStartTime = System.currentTimeMillis() Log.d(TAG, "MyApp onCreate - 进程启动") + + // 屏蔽高德定位 SDK(CgiManager) 的 NumberFormatException 噪音日志 + installAmapStderrFilter() val cache = FlutterEngineCache.getInstance() val existingEngine = cache.get(ENGINE_ID) @@ -105,6 +110,53 @@ class MyApp : FlutterApplication() { return true } + /** + * 过滤 System.err 中来自高德 SDK 的 NumberFormatException 栈(com.autonavi.aps.amapapi / CgiManager / ApsManager)。 + * 仅丢弃这类已知噪音,其他 stderr 输出原样打印。 + */ + private fun installAmapStderrFilter() { + val origin = System.err ?: return + val noiseKeywords = arrayOf( + "com.autonavi.aps.amapapi", + "CgiManager", + "ApsManager", + "onCellLocationChanged", + "PhoneStateListener", + ) + val bufferRef = StringBuilder() + var suppressing = false + + val filtered = PrintStream(object : OutputStream() { + override fun write(b: Int) { + val ch = b.toChar() + bufferRef.append(ch) + if (ch == '\n') { + val line = bufferRef.toString() + bufferRef.setLength(0) + val trimmed = line.trimStart() + if (trimmed.startsWith("java.lang.NumberFormatException")) { + suppressing = true + return + } + if (suppressing) { + // 只要是栈帧 (at xxx) 或 caused by,就继续丢弃 + if (trimmed.startsWith("at ") || + trimmed.startsWith("Caused by") || + trimmed.startsWith("... ") || + noiseKeywords.any { trimmed.contains(it) } + ) { + return + } + suppressing = false + } + origin.print(line) + } + } + }, true) + System.setErr(filtered) + Log.d(TAG, "已安装高德 SDK stderr 过滤器") + } + private fun isAppInForeground(): Boolean { val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val processes = am.runningAppProcesses ?: return false diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index f73e0cb2e..14d137ccf 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -15,6 +15,7 @@ import '../../core/utils/logger.dart'; import 'bluetooth_manager.dart'; import 'language_manager.dart'; import 'network/dio_manager.dart'; +import '../../routes/app_routes.dart'; // 重新导出BLE连接状态常量,让外部可以通过BleManager访问 export 'package:ble_service/ble_service.dart' show BleConnectionState; @@ -29,6 +30,10 @@ class BleCommands { static const int GET_WAKEUP_VERSION = 0x08; static const int GET_VOICE_WAKEUP_STATUS = 0x14; static const int SET_VOICE_WAKEUP_STATUS = 0x15; + /// 设备上报开启通话翻译 0x16 (帧头0xCC, cmdLen=0x01, status=0xFF) + static const int CALL_TRANSLATION_ON = 0x16; + /// 设备上报关闭通话翻译 0x17 (帧头0xCC, cmdLen=0x01, status=0xFF) + static const int CALL_TRANSLATION_OFF = 0x17; } /// 判断编解码控制指令是否成功 @@ -239,6 +244,14 @@ class BleManager extends GetxService { 0.obs; // 唤醒语音升级状态 0: 未升级 1: 升级中 2: 升级成功 3: 升级失败 RxInt get wakeupVoiceUpgradeStatus => _wakeupVoiceUpgradeStatus; + /// 设备上报通话翻译开关信号流 + /// true = 设备上报开启通话翻译 (0x16),false = 设备上报关闭通话翻译 (0x17) + /// 外部(如 TranslationController)可监听该流,收到信号后启动/停止通话翻译功能。 + final StreamController _callTranslationRequestController = + StreamController.broadcast(); + Stream get onCallTranslationRequest => + _callTranslationRequestController.stream; + /// 初始化 @override void onInit() { @@ -254,6 +267,7 @@ class BleManager extends GetxService { void onClose() { _statusSubscription?.cancel(); _dataSubscription?.cancel(); + _callTranslationRequestController.close(); super.onClose(); } @@ -392,11 +406,51 @@ class BleManager extends GetxService { case BleCommands.SET_VOICE_WAKEUP_STATUS: _updateVoiceWakeupSetResultFromData(data); break; + case BleCommands.CALL_TRANSLATION_ON: + _handleCallTranslationRequest(true, data); + break; + case BleCommands.CALL_TRANSLATION_OFF: + _handleCallTranslationRequest(false, data); + break; default: Logger.w('BleManager', '未处理的命令ID: $commandId'); } } + /// 处理设备上报的通话翻译开关指令 (0x16 / 0x17) + /// - [enable] true=开启通话翻译, false=关闭 + /// 行为: + /// 1. 打印 [CALL_TRANS_CMD] 日志; + /// 2. 向 [onCallTranslationRequest] 流广播信号(TranslationController 监听并启停); + /// 3. 未在翻译页时,自动导航到 Routes.translation 并以 call 模式 + autoStart 进入。 + void _handleCallTranslationRequest( + bool enable, Map data) { + final status = data['status']; + Logger.i('BleManager', + '[CALL_TRANS_CMD] 收到设备上报${enable ? "开启" : "关闭"}通话翻译 cmd=0x${(enable ? BleCommands.CALL_TRANSLATION_ON : BleCommands.CALL_TRANSLATION_OFF).toRadixString(16)} status=$status'); + + // 广播给监听者(如 TranslationController 已在 call 模式可直接启/停) + if (!_callTranslationRequestController.isClosed) { + _callTranslationRequestController.add(enable); + } + + // 开启:若当前不在翻译页,则导航并请求自动开始 + if (enable) { + final currentRoute = Get.currentRoute; + if (currentRoute != Routes.translation) { + Logger.i('BleManager', + '[CALL_TRANS_CMD] 当前路由=$currentRoute,导航到翻译页(call 模式, autoStart=true)'); + Get.toNamed( + Routes.translation, + arguments: {'translationId': 'call', 'autoStart': true}, + ); + } else { + Logger.i('BleManager', + '[CALL_TRANS_CMD] 已在翻译页,由 TranslationController 监听信号自行启动'); + } + } + } + /// 根据命令名称处理设备信息 void _processDeviceInfoByCommandName( String commandName, Map data) { @@ -1125,9 +1179,14 @@ class BleManager extends GetxService { _awaitingCodecOpen = true; _awaitingCodecClose = false; _isCodecActive.value = false; - return await _bleService.openA2DPDecoder(); + Logger.i('CALL_TRANS_CMD', + '准备发送通话翻译指令 cmd=0x05 sub=0xA2(A2DP_PLAY) → openA2DPDecoder'); + final ok = await _bleService.openA2DPDecoder(); + Logger.i('CALL_TRANS_CMD', + '通话翻译指令下发结果: ${ok ? "成功" : "失败"} (等待设备回包确认)'); + return ok; } catch (e) { - Logger.error('打开解码器失败: ${e.toString()}'); + Logger.e('CALL_TRANS_CMD', '通话翻译指令异常: ${e.toString()}'); _awaitingCodecOpen = false; return false; } diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 0962a74f8..756b932f9 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -151,6 +151,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { StreamSubscription? _recognitionSubscription; StreamSubscription? _astEventSubscription; StreamSubscription? _voiceInteractionSubscription; + StreamSubscription? _deviceCallTranslationSubscription; Timer? _translationDebounceTimer; // ==================== 重新初始化守卫 ==================== @@ -570,10 +571,55 @@ class TranslationController extends GetxController with WidgetsBindingObserver { // 应用模式逻辑(含ASR/TTS配置等) changeTranslationMode(initMode); + // 监听设备上报的通话翻译开关 (0x16 / 0x17) + _setupDeviceCallTranslationListener(initMode); + // 监听应用生命周期:当应用切到后台/失焦时兜底重置 activeSpeaker WidgetsBinding.instance.addObserver(this); } + /// 监听 BleManager 下发的设备通话翻译开关信号 (0x16 / 0x17) + /// - 收到 true: 若当前非 call 模式则切换到 call 模式,并自动启动识别; + /// - 收到 false: 停止当前识别(stopAll)。 + /// 首次进入时若 autoStart=true,也立即触发一次开启。 + void _setupDeviceCallTranslationListener(String initMode) { + _deviceCallTranslationSubscription?.cancel(); + _deviceCallTranslationSubscription = + bleManager.onCallTranslationRequest.listen((enable) async { + Logger.info( + '[CALL_TRANS_CMD] TranslationController 收到信号: ${enable ? "开启" : "关闭"}'); + try { + if (enable) { + if (currentMode.value != 'call') { + await changeTranslationMode('call'); + } + if (!isRecording.value) { + await startRecording(); + } + } else { + await stopAll(); + } + } catch (e, s) { + Logger.e('TRANSL', '[CALL_TRANS_CMD] 处理设备通话翻译信号失败: $e', e, s); + } + }); + + // 支持从 BleManager 导航进入时携带 autoStart 参数自动开启 + final autoStart = Get.arguments?['autoStart'] == true; + if (autoStart && initMode == 'call') { + Logger.info('[CALL_TRANS_CMD] 检测到 autoStart=true, 进入通话翻译并自启动'); + Future.microtask(() async { + try { + if (!isRecording.value) { + await startRecording(); + } + } catch (e, s) { + Logger.e('TRANSL', '[CALL_TRANS_CMD] autoStart 启动失败: $e', e, s); + } + }); + } + } + // @override // void onReady() { // super.onReady(); @@ -903,6 +949,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver { _astEventSubscription?.cancel(); _astEventSubscription = null; _voiceInteractionSubscription?.cancel(); + _deviceCallTranslationSubscription?.cancel(); + _deviceCallTranslationSubscription = null; _translationDebounceTimer?.cancel(); restoreOtherServices(); // 移除对服务的 dispose,避免频繁销毁/重建造成卡顿 diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt index 8ad4d2eb9..8c27ca82f 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt @@ -592,6 +592,32 @@ class BleCommandSender { // 接收到结束 AI 单次对话信号,关闭编码器 (设备侧) callback?.notifyEndSingleDialogReceived() } + BleConst.CMD_CALL_TRANSLATION_ON -> { + // 设备上报: 开启通话翻译 0x16 (cmdLen=0x01, status=0xFF) + val status: Int = if (notifyData.size >= 4) (notifyData[3].toInt() and 0xFF) else -1 + Log.i(TAG, "[CALL_TRANS_CMD] 收到设备上报开启通话翻译 cmd=0x16 status=0x${"%02X".format(status)}") + val resultMap = mapOf( + "success" to true, + "command" to infoType, + "callTranslation" to true, + "status" to status, + "isNotification" to true + ) + callback?.notifyDeviceInfoReceived(infoType, resultMap) + } + BleConst.CMD_CALL_TRANSLATION_OFF -> { + // 设备上报: 关闭通话翻译 0x17 (cmdLen=0x01, status=0xFF) + val status: Int = if (notifyData.size >= 4) (notifyData[3].toInt() and 0xFF) else -1 + Log.i(TAG, "[CALL_TRANS_CMD] 收到设备上报关闭通话翻译 cmd=0x17 status=0x${"%02X".format(status)}") + val resultMap = mapOf( + "success" to true, + "command" to infoType, + "callTranslation" to false, + "status" to status, + "isNotification" to true + ) + callback?.notifyDeviceInfoReceived(infoType, resultMap) + } // 可以在这里添加其他主动上报类型的处理 else -> { // 对于未知上报类型,返回原始数据 diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt index 6f35798a4..fba780cfe 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt @@ -101,6 +101,10 @@ object BleConst { const val CMD_GET_VOICE_WAKEUP_STATUS = 0x14 /** 设置语音唤醒状态 */ const val CMD_SET_VOICE_WAKEUP_STATUS = 0x15 + /** 设备上报开启通话翻译 0x16 (帧头0xCC, cmdLen=0x01, status=0xFF) */ + const val CMD_CALL_TRANSLATION_ON = 0x16 + /** 设备上报关闭通话翻译 0x17 (帧头0xCC, cmdLen=0x01, status=0xFF) */ + const val CMD_CALL_TRANSLATION_OFF = 0x17 // ================== 语音唤醒状态值 ================== /** 语音唤醒关闭 */ diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 777981938..0e19216c8 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -1570,7 +1570,7 @@ object BleService { * @return 是否成功发送命令 */ fun openA2DPDecoder(channelMode: Int = BleConst.AUDIO_CHANNEL_STEREO): Boolean { - Log.i(TAG, "打开A2DP解码器...") + Log.i(TAG, "[CALL_TRANS_CMD] 打开A2DP解码器(通话翻译) channelMode=$channelMode") startOpusEncodeStream() recordfile1?.creatingFiles(); startOpusStreamDecoding(false, 2, 16000, 80) @@ -1578,7 +1578,9 @@ object BleService { BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), channelMode.toByte() ) - return commandSender.sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), cmdData) + val ok = commandSender.sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), cmdData) + Log.i(TAG, "[CALL_TRANS_CMD] 通话翻译指令下发 cmd=0x05 sub=0xA2 result=$ok bytes=${cmdData.joinToString(",") { "0x%02X".format(it) }}") + return ok } /** diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift index e4fdc1f2d..4c4344422 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift @@ -96,6 +96,10 @@ class BleConst { static let CMD_GET_VOICE_WAKEUP_STATUS: UInt8 = 0x14 /** 设置语音唤醒状态 */ static let CMD_SET_VOICE_WAKEUP_STATUS: UInt8 = 0x15 + /** 设备上报开启通话翻译 0x16 (帧头0xCC, cmdLen=0x01, status=0xFF) */ + static let CMD_CALL_TRANSLATION_ON: UInt8 = 0x16 + /** 设备上报关闭通话翻译 0x17 (帧头0xCC, cmdLen=0x01, status=0xFF) */ + static let CMD_CALL_TRANSLATION_OFF: UInt8 = 0x17 // ================== 语音唤醒状态值 ================== /** 语音唤醒关闭 */ diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift index a519a03c5..b3a66b38c 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift @@ -746,14 +746,18 @@ public class BleService: NSObject { /// 打开A2DP解码器并开始录制 func openA2DPDecoder() -> Bool { // 确保Opus解码器处于活跃状态(如果不是则启动) - + startOpusDecoding(sampleRate: 16000, channels: 2, frameSize: 1280) opusProcessor?.startEncoding(sampleRate: 16000, channels: 1, frameRate: 20) - - - os_log("打开A2DP解码并开始录制...", log: logger, type: .info) + + + os_log("[CALL_TRANS_CMD] 打开A2DP解码并开始录制(通话翻译)...", log: logger, type: .info) let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_STEREO]) - return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) + let ok = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData) + os_log("[CALL_TRANS_CMD] 通话翻译指令下发 cmd=0x05 sub=0xA2 result=%{public}@ bytes=[0x%02X,0x%02X]", + log: logger, type: .info, + ok ? "成功" : "失败", BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_STEREO) + return ok } /// 打开通话记录解码器并开始录制 func openCallRecordDecoder() -> Bool { @@ -1384,6 +1388,32 @@ public class BleService: NSObject { case BleConst.CMD_END_SINGLE_DIALOG: os_log("📢 收到结束 AI 单次对话指令", log: logger, type: .info) notifyEndSingleDialogReceived() + case BleConst.CMD_CALL_TRANSLATION_ON: + // 设备上报: 开启通话翻译 0x16 (cmdLen=0x01, status=0xFF) + let status: Int = notifyData.count >= 4 ? Int(notifyData[3]) : -1 + os_log("[CALL_TRANS_CMD] 收到设备上报开启通话翻译 cmd=0x16 status=0x%02X", + log: logger, type: .info, status) + let resultMap: [String: Any] = [ + "success": true, + "command": Int(infoType), + "callTranslation": true, + "status": status, + "isNotification": true + ] + notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap) + case BleConst.CMD_CALL_TRANSLATION_OFF: + // 设备上报: 关闭通话翻译 0x17 (cmdLen=0x01, status=0xFF) + let status: Int = notifyData.count >= 4 ? Int(notifyData[3]) : -1 + os_log("[CALL_TRANS_CMD] 收到设备上报关闭通话翻译 cmd=0x17 status=0x%02X", + log: logger, type: .info, status) + let resultMap: [String: Any] = [ + "success": true, + "command": Int(infoType), + "callTranslation": false, + "status": status, + "isNotification": true + ] + notifyDeviceInfoReceived(infoType: Int(infoType), infoData: resultMap) default: // 对于未知上报类型,返回原始数据 let resultMap: [String: Any] = [