Browse Source

上传眼睛按钮直接进入通话翻译模式

weicu
liwei1dao 6 months ago
parent
commit
cec61d8a9a
  1. 52
      android/app/src/main/kotlin/com/saitong/voitrans/MyApplication.kt
  2. 63
      lib/data/services/ble_manager.dart
  3. 48
      lib/modules/translation/controllers/translation_controller.dart
  4. 26
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt
  5. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  6. 6
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  7. 4
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift
  8. 40
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

52
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

63
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<bool> _callTranslationRequestController =
StreamController<bool>.broadcast();
Stream<bool> 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<dynamic, dynamic> 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<dynamic, dynamic> 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;
}

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

@ -151,6 +151,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
StreamSubscription? _recognitionSubscription;
StreamSubscription? _astEventSubscription;
StreamSubscription? _voiceInteractionSubscription;
StreamSubscription<bool>? _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,避免频繁销毁/重建造成卡顿

26
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 -> {
// 对于未知上报类型,返回原始数据

4
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
// ================== 语音唤醒状态值 ==================
/** 语音唤醒关闭 */

6
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
}
/**

4
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
// ================== 语音唤醒状态值 ==================
/** 语音唤醒关闭 */

40
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] = [

Loading…
Cancel
Save