diff --git a/local_plugins/azure_speech/android/app/build.gradle.kts b/local_plugins/azure_speech/android/app/build.gradle.kts
deleted file mode 100644
index bd9431936..000000000
--- a/local_plugins/azure_speech/android/app/build.gradle.kts
+++ /dev/null
@@ -1,44 +0,0 @@
-plugins {
- id("com.android.application")
- id("kotlin-android")
- // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
- id("dev.flutter.flutter-gradle-plugin")
-}
-
-android {
- namespace = "com.example.azure_speech"
- compileSdk = flutter.compileSdkVersion
- ndkVersion = flutter.ndkVersion
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
-
- kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11.toString()
- }
-
- defaultConfig {
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId = "com.example.azure_speech"
- // You can update the following values to match your application needs.
- // For more information, see: https://flutter.dev/to/review-gradle-config.
- minSdk = flutter.minSdkVersion
- targetSdk = flutter.targetSdkVersion
- versionCode = flutter.versionCode
- versionName = flutter.versionName
- }
-
- buildTypes {
- release {
- // TODO: Add your own signing config for the release build.
- // Signing with the debug keys for now, so `flutter run --release` works.
- signingConfig = signingConfigs.getByName("debug")
- }
- }
-}
-
-flutter {
- source = "../.."
-}
diff --git a/local_plugins/azure_speech/android/app/src/debug/AndroidManifest.xml b/local_plugins/azure_speech/android/app/src/debug/AndroidManifest.xml
deleted file mode 100644
index 399f6981d..000000000
--- a/local_plugins/azure_speech/android/app/src/debug/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/local_plugins/azure_speech/android/app/src/main/AndroidManifest.xml b/local_plugins/azure_speech/android/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 388b60653..000000000
--- a/local_plugins/azure_speech/android/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/local_plugins/azure_speech/android/app/src/main/kotlin/com/example/azure_speech/MainActivity.kt b/local_plugins/azure_speech/android/app/src/main/kotlin/com/example/azure_speech/MainActivity.kt
deleted file mode 100644
index 60cc81f0d..000000000
--- a/local_plugins/azure_speech/android/app/src/main/kotlin/com/example/azure_speech/MainActivity.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.example.azure_speech
-
-import io.flutter.embedding.android.FlutterActivity
-
-class MainActivity : FlutterActivity()
diff --git a/local_plugins/azure_speech/android/app/src/main/res/drawable-v21/launch_background.xml b/local_plugins/azure_speech/android/app/src/main/res/drawable-v21/launch_background.xml
deleted file mode 100644
index f74085f3f..000000000
--- a/local_plugins/azure_speech/android/app/src/main/res/drawable-v21/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/local_plugins/azure_speech/android/app/src/main/res/drawable/launch_background.xml b/local_plugins/azure_speech/android/app/src/main/res/drawable/launch_background.xml
deleted file mode 100644
index 304732f88..000000000
--- a/local_plugins/azure_speech/android/app/src/main/res/drawable/launch_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/local_plugins/azure_speech/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/local_plugins/azure_speech/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index db77bb4b7..000000000
Binary files a/local_plugins/azure_speech/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/local_plugins/azure_speech/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/local_plugins/azure_speech/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 17987b79b..000000000
Binary files a/local_plugins/azure_speech/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/local_plugins/azure_speech/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/local_plugins/azure_speech/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 09d439148..000000000
Binary files a/local_plugins/azure_speech/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/local_plugins/azure_speech/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/local_plugins/azure_speech/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index d5f1c8d34..000000000
Binary files a/local_plugins/azure_speech/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/local_plugins/azure_speech/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/local_plugins/azure_speech/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 4d6372eeb..000000000
Binary files a/local_plugins/azure_speech/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/local_plugins/azure_speech/android/app/src/main/res/values-night/styles.xml b/local_plugins/azure_speech/android/app/src/main/res/values-night/styles.xml
deleted file mode 100644
index 06952be74..000000000
--- a/local_plugins/azure_speech/android/app/src/main/res/values-night/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/local_plugins/azure_speech/android/app/src/main/res/values/styles.xml b/local_plugins/azure_speech/android/app/src/main/res/values/styles.xml
deleted file mode 100644
index cb1ef8805..000000000
--- a/local_plugins/azure_speech/android/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/local_plugins/azure_speech/android/app/src/profile/AndroidManifest.xml b/local_plugins/azure_speech/android/app/src/profile/AndroidManifest.xml
deleted file mode 100644
index 399f6981d..000000000
--- a/local_plugins/azure_speech/android/app/src/profile/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
index 687135011..815a067db 100644
--- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
+++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
@@ -657,6 +657,14 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
val useExternalAudio = call.argument("audioSourceType") ?: false
val removeFirstPunctuation =
call.argument("isRemoveFirstPunctuation") ?: true
+
+ // 通话翻译模式(external):AST端到端服务已处理识别+翻译+TTS,无需启动ASR
+ if (useExternalAudio) {
+ FileLogger.d(tag, "通话模式,AST端到端服务已激活(provider=$currentAstProvider),跳过ASR")
+ result.success(true)
+ return
+ }
+
// 确保事件通道已准备好
if (asrEventSink == null) {
result.error(
@@ -668,15 +676,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
}
try {
- FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
- // 选择音频源类型
- val audioSourceType = if (useExternalAudio) {
- AzureAsrHelper.AudioSourceType.EXTERNAL
- } else {
- AzureAsrHelper.AudioSourceType.MICROPHONE
- }
+ FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}")
+ val audioSourceType = AzureAsrHelper.AudioSourceType.MICROPHONE
val success = azureAsrHelper.startContinuousRecognition(audioSourceType)
- // val success = azureAsrHelper.startContinuousRecognition(audioSourceType, isRemoveFirstPunctuation = removeFirstPunctuation)
result.success(true)
} catch (e: Exception) {
result.error("START_RECOGNITION_ERROR", e.message, null)
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
index a5e261755..61590139e 100644
--- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
@@ -86,6 +86,14 @@ public class AzureAsrHelper: NSObject {
return useXunfei ? "xunfei" : "azure"
}
+ /// 通话模式下强制禁用讯飞,使用微软 ASR(与 Android 对齐)
+ public func forceDisableXunfei() {
+ if useXunfei {
+ os_log("forceDisableXunfei: 通话模式强制切换到微软ASR", log: log, type: .info)
+ useXunfei = false
+ }
+ }
+
/**
* 初始化Azure语音服务
*
diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
index cae8e7341..cd1e2fc17 100644
--- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
+++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
@@ -19,7 +19,9 @@ import ble_service
private let tag = "AzureSpeechPlugin"
// 日志对象
internal let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin")
-
+ /// 通话翻译专用日志,Xcode Console 过滤: subsystem=com.azure.speech category=CallTranslation
+ internal let ctLog = OSLog(subsystem: "com.azure.speech", category: "CallTranslation")
+
// MARK: - 防抖相关(iOS 原生层)
// 函数级注释:用于限制 startContinuousRecognition/stopContinuousRecognition 在极短时间内的重复触发
private var lastStartRecognitionAt: TimeInterval = 0
@@ -91,7 +93,9 @@ import ble_service
private var astChannel: FlutterMethodChannel?
private var astEventChannel: FlutterEventChannel?
internal var astEventSink: FlutterEventSink?
- private let azureAstHelper = IntegratedSpeechTranslationService()
+ // 双向 AST 服务:A(己方)和 B(对方),与 Android 对齐
+ private let azureAstHelperA = IntegratedSpeechTranslationService()
+ private let azureAstHelperB = IntegratedSpeechTranslationService()
// 端到端翻译 Helpers(豆包 / 阿里百炼)
private var doubaoAstHelperA = DoubaoE2ETranslateHelper()
@@ -110,6 +114,9 @@ import ble_service
private var audioDataEventChannel: FlutterEventChannel?
internal var audioDataEventSink: FlutterEventSink?
+ // 通话翻译音频统计(日志用)
+ private var bleAudioRecvCount: Int64 = 0
+
// 录音相关
public var recordfile: RecordFile?
private var isRecord = false
@@ -414,15 +421,22 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
AzureAsrHelper.AudioSourceType.external :
AzureAsrHelper.AudioSourceType.microphone
- print("startContinuousRecognition: \(audioSourceType)")
-
- // 启动连续识别
- let success = azureAsrHelper.startContinuousRecognition(
- mod: mode,
- audioSourceType: audioSourceType,
- isRemoveFirstPunctuation: removeFirstPunctuation
- )
- result(success)
+ os_log("startContinuousRecognition: mode=%@, audioSourceType=%@, isExternal=%d, astProvider=%@",
+ log: ctLog, type: .info, mode, String(describing: audioSourceType), isExternalActive ? 1 : 0, currentAstProvider)
+
+ // 通话翻译模式(external):AST 端到端服务已处理识别+翻译+TTS,无需启动 ASR
+ if isExternalActive {
+ os_log("startContinuousRecognition: 通话模式,AST端到端服务已激活(provider=%@),跳过ASR", log: ctLog, type: .info, currentAstProvider)
+ result(true)
+ } else {
+ let success = azureAsrHelper.startContinuousRecognition(
+ mod: mode,
+ audioSourceType: audioSourceType,
+ isRemoveFirstPunctuation: removeFirstPunctuation
+ )
+ os_log("startContinuousRecognition 结果=%d", log: ctLog, type: .info, success ? 1 : 0)
+ result(true)
+ }
case "recognizeCallback":
// 确保事件通道已准备好
guard asrEventSink != nil else {
@@ -451,11 +465,17 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
result(true)
return
}
-
- print("stopContinuousRecognition")
- let success = azureAsrHelper.stopContinuousRecognition()
- currentAsrCallback = nil
- result(success)
+
+ // ASR 没在运行时(通话模式下被跳过了)直接返回
+ if azureAsrHelper.audioStream?.isContinuousRecognitionActive != true {
+ os_log("stopContinuousRecognition: ASR未运行,跳过", log: ctLog, type: .info)
+ result(true)
+ } else {
+ os_log("stopContinuousRecognition: 停止ASR", log: ctLog, type: .info)
+ let success = azureAsrHelper.stopContinuousRecognition()
+ currentAsrCallback = nil
+ result(success)
+ }
case "isContinuousRecognitionActive":
result(azureAsrHelper.isContinuousRecognitionActive())
@@ -757,50 +777,64 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
// }
case "startContinuousTranslation":
+ os_log("[AST-START] provider=%{public}@", log: ctLog, type: .info, currentAstProvider)
if currentAstProvider == "volcano" {
- _ = doubaoAstHelperA.startContinuousTranslation()
- _ = doubaoAstHelperB.startContinuousTranslation()
+ let rA = doubaoAstHelperA.startContinuousTranslation()
+ let rB = doubaoAstHelperB.startContinuousTranslation()
+ os_log("[AST-START] volcano: A=%d, B=%d", log: ctLog, type: .info, rA ? 1 : 0, rB ? 1 : 0)
result(true)
} else if currentAstProvider == "alibaba" {
- _ = bailianAstHelperA.startContinuousConversation()
- _ = bailianAstHelperB.startContinuousConversation()
+ let rA = bailianAstHelperA.startContinuousConversation()
+ let rB = bailianAstHelperB.startContinuousConversation()
+ os_log("[AST-START] alibaba: A=%d, B=%d", log: ctLog, type: .info, rA ? 1 : 0, rB ? 1 : 0)
result(true)
} else {
do {
- os_log("开启翻译", log: log, type: .info)
- try azureAstHelper.startContinuousTranslation()
+ os_log("[AST-START] azure: starting A+B", log: ctLog, type: .info)
+ try azureAstHelperA.startContinuousTranslation()
+ try azureAstHelperB.startContinuousTranslation()
+ os_log("[AST-START] azure: A+B started successfully", log: ctLog, type: .info)
result(true)
} catch {
+ os_log("[AST-START] azure: FAILED: %{public}@", log: ctLog, type: .error, error.localizedDescription)
result(FlutterError(code: "START_CONTINUOUS_TRANSLATION_ERROR", message: error.localizedDescription, details: nil))
}
}
case "stopContinuousTranslation":
+ os_log("[AST-STOP] provider=%{public}@", log: ctLog, type: .info, currentAstProvider)
if currentAstProvider == "volcano" {
- _ = doubaoAstHelperA.stopContinuousTranslation()
- _ = doubaoAstHelperB.stopContinuousTranslation()
+ let rA = doubaoAstHelperA.stopContinuousTranslation()
+ let rB = doubaoAstHelperB.stopContinuousTranslation()
+ os_log("[AST-STOP] volcano: A=%d, B=%d", log: ctLog, type: .info, rA ? 1 : 0, rB ? 1 : 0)
result(true)
} else if currentAstProvider == "alibaba" {
- _ = bailianAstHelperA.stopContinuousConversation()
- _ = bailianAstHelperB.stopContinuousConversation()
+ let rA = bailianAstHelperA.stopContinuousConversation()
+ let rB = bailianAstHelperB.stopContinuousConversation()
+ os_log("[AST-STOP] alibaba: A=%d, B=%d", log: ctLog, type: .info, rA ? 1 : 0, rB ? 1 : 0)
result(true)
} else {
do {
- os_log("停止翻译", log: log, type: .info)
- try azureAstHelper.stopContinuousTranslation()
+ os_log("[AST-STOP] azure: stopping A+B", log: ctLog, type: .info)
+ try azureAstHelperA.stopContinuousTranslation()
+ try azureAstHelperB.stopContinuousTranslation()
+ os_log("[AST-STOP] azure: A+B stopped successfully", log: ctLog, type: .info)
result(true)
} catch {
+ os_log("[AST-STOP] azure: FAILED: %{public}@", log: ctLog, type: .error, error.localizedDescription)
result(FlutterError(code: "STOP_CONTINUOUS_TRANSLATION_ERROR", message: error.localizedDescription, details: nil))
}
}
case "dispose":
- os_log("释放AST资源", log: log, type: .info)
- azureAstHelper.dispose()
+ os_log("[AST-DISPOSE] Releasing all AST resources, provider=%{public}@", log: ctLog, type: .info, currentAstProvider)
+ azureAstHelperA.dispose()
+ azureAstHelperB.dispose()
doubaoAstHelperA.dispose()
doubaoAstHelperB.dispose()
bailianAstHelperA.dispose()
bailianAstHelperB.dispose()
+ os_log("[AST-DISPOSE] All AST resources released", log: ctLog, type: .info)
result(true)
case "recognizeCallback":
@@ -849,9 +883,25 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
let alibabaAppId = (args["alibabaAppId"] as? String) ?? ""
let alibabaAppURL = (args["alibabaAppURL"] as? String) ?? ""
- os_log("initialize provider=%@ lang0=%@ lang1=%@ trans0=%@ trans1=%@", log: log, type: .info, provider, lang0, lang1, translationLang0, translationLang1)
+ os_log("[AST-INIT] provider=%{public}@ lang0=%{public}@ lang1=%{public}@ trans0=%{public}@ trans1=%{public}@ tts0=%{public}@ tts1=%{public}@",
+ log: ctLog, type: .info, provider, lang0, lang1, translationLang0, translationLang1, ttsLang0, ttsLang1)
+ os_log("[AST-INIT] subscriptionKey=%{public}@ region=%{public}@ wsUrl=%{public}@ appKey=%{public}@ accessKey=%{public}@ resourceId=%{public}@",
+ log: ctLog, type: .info,
+ String(subscriptionKeyI.prefix(4)) + "***",
+ regionI,
+ wsUrl,
+ String(appKey.prefix(4)) + "***",
+ String(accessKeyD.prefix(4)) + "***",
+ resourceId)
+ os_log("[AST-INIT] alibabaAppKey=%{public}@ alibabaAppId=%{public}@ alibabaAppURL=%{public}@",
+ log: ctLog, type: .info,
+ alibabaAppKey.isEmpty ? "(empty)" : String(alibabaAppKey.prefix(4)) + "***",
+ alibabaAppId,
+ alibabaAppURL)
if currentAstProvider == "azure" {
+ os_log("[AST-INIT] Azure: configuring A(src=%{public}@ tgt=%{public}@) B(src=%{public}@ tgt=%{public}@)",
+ log: ctLog, type: .info, lang0, lang1, lang1, lang0)
let svcA = IntegratedSpeechTranslationService.ServiceConfiguration(
sourceLanguage: lang0,
targetLanguage: lang1,
@@ -866,18 +916,26 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
translationSourceLanguage: translationLang1,
translationTargetLanguage: translationLang0
)
- let callbackA = AstEventCallback(plugin: self)
- let callbackB = AstEventCallback(plugin: self)
+ let callbackA = AstEventCallback(plugin: self, serviceId: "A")
+ let callbackB = AstEventCallback(plugin: self, serviceId: "B")
astEventCallback = callbackA
let azureConfigI = AzureConfiguration(subscriptionKey: subscriptionKeyI, region: regionI)
let translationConfigI = TranslationConfiguration(region: azureTranslationRegion, subscriptionKey: azureTranslationKey, location: azureTranslationRegion)
Task {
- await azureAstHelper.initialize(azureConfig: azureConfigI, translationConfig: translationConfigI, serviceConfig: svcA, callback: callbackA)
+ os_log("[AST-INIT] Azure: initializing A service...", log: self.ctLog, type: .info)
+ await azureAstHelperA.initialize(azureConfig: azureConfigI, translationConfig: translationConfigI, serviceConfig: svcA, callback: callbackA)
+ os_log("[AST-INIT] Azure: A initialized, waiting 1.2s before B...", log: self.ctLog, type: .info)
+ try? await Task.sleep(nanoseconds: 1_200_000_000)
+ os_log("[AST-INIT] Azure: initializing B service...", log: self.ctLog, type: .info)
+ await azureAstHelperB.initialize(azureConfig: azureConfigI, translationConfig: translationConfigI, serviceConfig: svcB, callback: callbackB)
+ os_log("[AST-INIT] Azure: A+B both initialized", log: self.ctLog, type: .info)
}
} else if currentAstProvider == "volcano" {
// 初始化豆包 AST(双路)
+ os_log("[AST-INIT] Volcano: configuring A(%{public}@->%{public}@) B(%{public}@->%{public}@)",
+ log: ctLog, type: .info, translationLang0, translationLang1, translationLang1, translationLang0)
let cfg = DoubaoE2ETranslateHelper.Config(wsUrl: wsUrl, appKey: appKey, accessKey: accessKeyD, resourceId: resourceId, sourceLanguage: translationLang0, targetLanguage: translationLang1, clientWaitMs: 60_000)
astWsConfig = cfg
let cbA = DoubaoCallbackProxy(plugin: self, serviceId: "A", direction: "\(translationLang0)->\(translationLang1)", targetLanguage: translationLang1)
@@ -892,12 +950,18 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
cfgB.targetLanguage = translationLang0
Task {
- _ = await doubaoAstHelperA.initialize(config: cfgA, cb: cbA)
+ os_log("[AST-INIT] Volcano: initializing A service...", log: self.ctLog, type: .info)
+ let rA = await doubaoAstHelperA.initialize(config: cfgA, cb: cbA)
+ os_log("[AST-INIT] Volcano: A service result=%d, sleeping 500ms before B...", log: self.ctLog, type: .info, rA ? 1 : 0)
try? await Task.sleep(nanoseconds: 500_000_000)
- _ = await doubaoAstHelperB.initialize(config: cfgB, cb: cbB)
+ os_log("[AST-INIT] Volcano: initializing B service...", log: self.ctLog, type: .info)
+ let rB = await doubaoAstHelperB.initialize(config: cfgB, cb: cbB)
+ os_log("[AST-INIT] Volcano: B service result=%d. Both services ready.", log: self.ctLog, type: .info, rB ? 1 : 0)
}
} else if currentAstProvider == "alibaba" {
// 初始化阿里百炼(双路)
+ os_log("[AST-INIT] Alibaba: configuring A(%{public}@->%{public}@) B(%{public}@->%{public}@)",
+ log: ctLog, type: .info, translationLang0, translationLang1, translationLang1, translationLang0)
var cfgA = AliyunBailianE2EHelper.Config()
cfgA.wsUrl = alibabaAppURL
cfgA.apiKey = alibabaAppKey
@@ -922,8 +986,11 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
bailianCallbackB = cbB
Task {
- _ = await bailianAstHelperA.initialize(config: cfgA, cb: cbA)
- _ = await bailianAstHelperB.initialize(config: cfgB, cb: cbB)
+ os_log("[AST-INIT] Alibaba: initializing A service...", log: self.ctLog, type: .info)
+ let rA = await bailianAstHelperA.initialize(config: cfgA, cb: cbA)
+ os_log("[AST-INIT] Alibaba: A service result=%d, initializing B service...", log: self.ctLog, type: .info, rA ? 1 : 0)
+ let rB = await bailianAstHelperB.initialize(config: cfgB, cb: cbB)
+ os_log("[AST-INIT] Alibaba: B service result=%d. Both services ready.", log: self.ctLog, type: .info, rB ? 1 : 0)
}
}
result(true)
@@ -935,7 +1002,18 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
/// 发送AST事件到Flutter层
internal func sendAstEvent(_ event: [String: Any]) {
- if astEventSink == nil { return }
+ let eventType = event["type"] as? String ?? "unknown"
+ let sid = event["serviceId"] as? String ?? "?"
+ if astEventSink == nil {
+ os_log("[AST-EVENT] DROPPED (no sink): type=%{public}@ serviceId=%{public}@",
+ log: ctLog, type: .error, eventType, sid)
+ return
+ }
+ // 对于非高频事件(非 recognizing/translatedInterim),打印详细日志
+ if eventType != "recognizing" && eventType != "translatedInterim" {
+ os_log("[AST-EVENT] sending: type=%{public}@ serviceId=%{public}@ direction=%{public}@",
+ log: ctLog, type: .info, eventType, sid, event["direction"] as? String ?? "N/A")
+ }
DispatchQueue.main.async { [weak self] in
self?.astEventSink?(event)
}
@@ -949,13 +1027,11 @@ private func sendAudioDataEvent(_ event: [String: Any]) {
*/
private class AstEventCallback: IntegratedSpeechTranslationService.ServiceEventCallback {
private weak var plugin: AzureSpeechPlugin?
-
- /**
- * 构造函数
- * @param plugin AzureSpeechPlugin 实例的弱引用
- */
- init(plugin: AzureSpeechPlugin) {
+ private let serviceId: String
+
+ init(plugin: AzureSpeechPlugin, serviceId: String = "A") {
self.plugin = plugin
+ self.serviceId = serviceId
}
/**
@@ -1135,23 +1211,15 @@ private class AstEventCallback: IntegratedSpeechTranslationService.ServiceEventC
*/
public func onSynthesisAudioGenerated(text: String, audioData: Data) {
guard let plugin = plugin else { return }
-
- os_log("语音合成音频生成,文本=%@,音频数据大小=%d", log: plugin.log, type: .info, text, audioData.count)
-
- // Azure TTS返回的是RIFF WAV格式,需要去除文件头提取PCM数据
+
+ // 只有 A 通道(己方翻译后的语音)推送到耳机,B 通道不推
+ guard serviceId == "A" else { return }
+
let pcmData = plugin.extractPcmFromWav(audioData)
if !pcmData.isEmpty {
- os_log("提取PCM数据,大小=%d字节", log: plugin.log, type: .info, pcmData.count)
+ os_log("[AzureAST-%{public}@] TTS音频推送到耳机,大小=%d字节", log: plugin.ctLog, type: .info, serviceId, pcmData.count)
BleService.shared.writeExternalAudioData(data: pcmData)
- } else {
- os_log("提取PCM数据失败,音频数据可能不是WAV格式", log: plugin.log, type: .error)
}
-
-// plugin?.sendAsrEvent([
-// "type": "synthesisAudioGenerated",
-// "text": text,
-// "audioDataSize": audioData.count
-// ])
}
}
@@ -1185,13 +1253,14 @@ extension AzureSpeechPlugin: BleService.Callback {
azureAsrHelper.audioStream?.saveAudioDataTo(data: data)
}
else if channel == 2 {
+ bleAudioRecvCount += 1
if (isRecord) {
recordfile?.setAudioConfig(sampleRate: 16000, channels: 2)
recordfile?.saveAudioDataToWav(data)
handleAudioData(data)
}
let sampleCount = data.count / 4 // 每个样本4字节(左右声道各2字节)
-
+
guard sampleCount > 0 else {
print("双声道数据样本数为0")
return
@@ -1219,23 +1288,53 @@ extension AzureSpeechPlugin: BleService.Callback {
}
}
- // print("分离音频数据 - 左声道: \(leftBuffer.count) bytes, 右声道: \(rightBuffer.count) bytes")
-
- // 安全解包版本
- guard let audioStream = azureAsrHelper.audioStream else {
- // os_log("音频流未初始化", type: .error)
- return
+ // 每200帧打印一次声道音量(与 Android 对齐)
+ if bleAudioRecvCount % 200 == 1 {
+ var leftRms: Double = 0
+ var rightRms: Double = 0
+ for i in stride(from: 0, to: leftBuffer.count - 1, by: 2) {
+ let lSample = Int16(leftBuffer[i]) | (Int16(leftBuffer[i+1]) << 8)
+ let rSample = Int16(rightBuffer[i]) | (Int16(rightBuffer[i+1]) << 8)
+ leftRms += Double(lSample) * Double(lSample)
+ rightRms += Double(rSample) * Double(rSample)
+ }
+ let samples = max(leftBuffer.count / 2, 1)
+ leftRms = sqrt(leftRms / Double(samples))
+ rightRms = sqrt(rightRms / Double(samples))
+ os_log("声道音量: left(己方)=%d, right(对方)=%d, provider=%@, count=%lld",
+ log: ctLog, type: .info, Int(leftRms), Int(rightRms), currentAstProvider, bleAudioRecvCount)
}
- azureAsrHelper.audioStream?.saveAudioDataTo(data: rightBuffer)
+ // 保存右声道到 ASR audioStream(可能为 nil)
+ if let audioStream = azureAsrHelper.audioStream {
+ audioStream.saveAudioDataTo(data: rightBuffer)
+ } else if bleAudioRecvCount % 200 == 1 {
+ os_log("audioStream 未初始化,rightBuffer 未保存到ASR", log: ctLog, type: .default)
+ }
// 根据当前 AST 提供商路由音频数据
+ // 左声道 -> A(己方语音),右声道 -> B(对方语音)
if currentAstProvider == "volcano" {
doubaoAstHelperA.pushAudioData(leftBuffer)
+ doubaoAstHelperB.pushAudioData(rightBuffer)
+ if bleAudioRecvCount % 200 == 1 {
+ os_log("[STS] pushAudio: volcano A(left)=%d bytes, B(right)=%d bytes, count=%lld",
+ log: ctLog, type: .info, leftBuffer.count, rightBuffer.count, bleAudioRecvCount)
+ }
} else if currentAstProvider == "alibaba" {
bailianAstHelperA.pushAudioData(leftBuffer)
+ bailianAstHelperB.pushAudioData(rightBuffer)
+ if bleAudioRecvCount % 200 == 1 {
+ os_log("[STS] pushAudio: alibaba A(left)=%d bytes, B(right)=%d bytes, count=%lld",
+ log: ctLog, type: .info, leftBuffer.count, rightBuffer.count, bleAudioRecvCount)
+ }
} else {
- azureAstHelper.pushAudioData(audioData: leftBuffer)
+ azureAstHelperA.pushAudioData(audioData: leftBuffer)
+ azureAstHelperB.pushAudioData(audioData: rightBuffer)
+ if bleAudioRecvCount % 200 == 1 {
+ os_log("[STS] pushAudio: azure A(left)=%d bytes, B(right)=%d bytes, count=%lld",
+ log: ctLog, type: .info, leftBuffer.count, rightBuffer.count, bleAudioRecvCount)
+ }
}
}
@@ -1328,70 +1427,95 @@ private class DoubaoCallbackProxy: DoubaoE2ETranslateHelper.Callback {
private let serviceId: String
private let direction: String
private let targetLanguage: String
+ private let ctLog = OSLog(subsystem: "com.azure.speech", category: "CallTranslation")
+
+ /// 从 direction(如 "zh-Hans->en-US")提取源语言("zh-Hans")
+ private var sourceLanguage: String {
+ if let range = direction.range(of: "->") {
+ return String(direction[direction.startIndex.. 0 {
- if isRightChannel {
- BleService.shared.writeExternalAudioData(data: data)
- } else {
- BleService.shared.writeExternalAudioData(data: data)
- }
+ let isServiceA = (serviceId == "A")
+ if data.count > 0 && isServiceA {
+ os_log("[DoubaoCallback-%{public}@] onPartialAudio writing %d bytes to BLE",
+ log: ctLog, type: .debug, serviceId, data.count)
+ BleService.shared.writeExternalAudioData(data: data)
}
}
func onSessionFinished(sessionId: String, finalText: String, finalAudio: Data) {
let key = "\(serviceId):\(sessionId)"
let original = plugin?.doubaoFinalSourceTextCache.removeValue(forKey: key) ?? ""
+ os_log("[DoubaoCallback-%{public}@] onSessionFinished finalText=%{public}@ original=%{public}@ audioLen=%d",
+ log: ctLog, type: .info, serviceId, finalText, original, finalAudio.count)
plugin?.sendAstEvent([
"type": "translated",
"serviceId": serviceId,
+ "direction": direction,
"translatedText": finalText,
"targetLanguage": targetLanguage,
"utteranceId": sessionId,
@@ -1400,6 +1524,8 @@ private class DoubaoCallbackProxy: DoubaoE2ETranslateHelper.Callback {
}
func onSessionError(sessionId: String, code: Int, message: String) {
+ os_log("[DoubaoCallback-%{public}@] onSessionError code=%d msg=%{public}@",
+ log: ctLog, type: .error, serviceId, code, message)
plugin?.sendAstEvent([
"type": "error",
"serviceId": serviceId,
@@ -1413,9 +1539,12 @@ private class DoubaoCallbackProxy: DoubaoE2ETranslateHelper.Callback {
func onFinalTranslatedText(sessionId: String, finalText: String) {
let key = "\(serviceId):\(sessionId)"
let original = plugin?.doubaoFinalSourceTextCache.removeValue(forKey: key) ?? ""
+ os_log("[DoubaoCallback-%{public}@] onFinalTranslatedText finalText=%{public}@ original=%{public}@",
+ log: ctLog, type: .info, serviceId, finalText, original)
plugin?.sendAstEvent([
"type": "translated",
"serviceId": serviceId,
+ "direction": direction,
"translatedText": finalText,
"targetLanguage": targetLanguage,
"utteranceId": sessionId,
@@ -1430,74 +1559,106 @@ private class AliyunCallbackProxy: AliyunBailianE2EHelper.Callback {
private let serviceId: String
private let direction: String
private let targetLanguage: String
+ private let ctLog = OSLog(subsystem: "com.azure.speech", category: "CallTranslation")
+ private var sourceTextCache: [String: String] = [:]
+
+ /// 从 direction(如 "zh-Hans->en-US")提取源语言("zh-Hans")
+ private var sourceLanguage: String {
+ if let range = direction.range(of: "->") {
+ return String(direction[direction.startIndex.. 0 {
- if isRightChannel {
- BleService.shared.writeExternalAudioData(data: data)
- } else {
- BleService.shared.writeExternalAudioData(data: data)
- }
+ let isServiceA = (serviceId == "A")
+ if data.count > 0 && isServiceA {
+ os_log("[AliyunCallback-%{public}@] onPartialAudio writing %d bytes to BLE",
+ log: ctLog, type: .debug, serviceId, data.count)
+ BleService.shared.writeExternalAudioData(data: data)
}
}
func onSessionFinished(sessionId: String, finalText: String, finalAudio: Data) {
+ let key = "\(serviceId):\(sessionId)"
+ let original = sourceTextCache.removeValue(forKey: key) ?? ""
+ os_log("[AliyunCallback-%{public}@] onSessionFinished finalText=%{public}@ original=%{public}@ audioLen=%d",
+ log: ctLog, type: .info, serviceId, finalText, original, finalAudio.count)
plugin?.sendAstEvent([
"type": "translated",
"serviceId": serviceId,
+ "direction": direction,
"translatedText": finalText,
"targetLanguage": targetLanguage,
- "utteranceId": sessionId
+ "utteranceId": sessionId,
+ "originalText": original
])
}
func onSessionError(sessionId: String, code: Int, message: String) {
+ os_log("[AliyunCallback-%{public}@] onSessionError code=%d msg=%{public}@",
+ log: ctLog, type: .error, serviceId, code, message)
plugin?.sendAstEvent([
"type": "error",
"serviceId": serviceId,
@@ -1509,12 +1670,18 @@ private class AliyunCallbackProxy: AliyunBailianE2EHelper.Callback {
}
func onFinalTranslatedText(sessionId: String, finalText: String) {
+ let key = "\(serviceId):\(sessionId)"
+ let original = sourceTextCache.removeValue(forKey: key) ?? ""
+ os_log("[AliyunCallback-%{public}@] onFinalTranslatedText finalText=%{public}@ original=%{public}@",
+ log: ctLog, type: .info, serviceId, finalText, original)
plugin?.sendAstEvent([
"type": "translated",
"serviceId": serviceId,
+ "direction": direction,
"translatedText": finalText,
"targetLanguage": targetLanguage,
- "utteranceId": sessionId
+ "utteranceId": sessionId,
+ "originalText": original
])
}
}
@@ -1660,8 +1827,11 @@ private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
* @param errorDetails 错误详情
*/
func onCanceled(sessionid:String,_ reason: String, _ errorDetails: String) {
+ if let p = plugin {
+ os_log("ASR onCanceled: sessionid=%@, reason=%@, error=%@", log: p.ctLog, type: .error, sessionid, reason, errorDetails)
+ }
plugin?.sendAsrEvent([
- "type": "canceled",
+ "type": "canceled",
"sessionid": sessionid,
"provider": plugin?.currentAsrProvider() ?? "",
"reason": reason,
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 7c7e3025c..db8323493 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
@@ -92,10 +92,13 @@ public class BleService: NSObject {
private var opusProcessor: SwiftOpusAudioProcessor?
// MARK: - 新增命令队列相关属性
private var commandQueue = [Data]()
- //是否回复
+ //是否回复(与 Android isWaitingReply 对应,true=可发送,false=等待回复)
private var isReply = true
// 是否回复指令码
-private var cmdReplyType: UInt8 = 0
+ private var cmdReplyType: UInt8 = 0
+ /// 回复超时计时器(与 Android REPLY_TIMEOUT_MS = 1000 对齐)
+ private var replyTimeoutTimer: Timer?
+ private let REPLY_TIMEOUT_MS: TimeInterval = 1.0
// 新增:音频配置参数
private var sampleRate: Int32 = 16000
@@ -482,7 +485,13 @@ private var cmdReplyType: UInt8 = 0
audioBuffer.removeAll()
audioDataQueue.removeAll()
audioQueueLock.unlock()
-
+
+ // 重置命令队列(与 Android reset 对齐)
+ replyTimeoutTimer?.invalidate()
+ replyTimeoutTimer = nil
+ commandQueue.removeAll()
+ isReply = true
+
updateConnectionState(BleConst.STATE_DISCONNECTING)
centralManager?.cancelPeripheralConnection(peripheral)
os_log("🔌 开始断开设备连接", log: logger, type: .info)
@@ -706,7 +715,7 @@ private var cmdReplyType: UInt8 = 0
if opusProcessor?.isDecoding() != true {
startOpusDecoding()
}
- let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_RIGHT])
+ let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, BleConst.AUDIO_CHANNEL_STEREO])
let result = sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
if result {
@@ -728,7 +737,7 @@ private var cmdReplyType: UInt8 = 0
//}
// 发送命令通知设备开始解码
- let paramData = Data([BleConst.CODEC_CONTROL_DECODE_ON, BleConst.AUDIO_CHANNEL_RIGHT])
+ let paramData = Data([BleConst.CODEC_CONTROL_DECODE_ON, BleConst.AUDIO_CHANNEL_STEREO])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
}
/// 获取与Flutter getExternalStorageDirectories()一致的存储目录
@@ -747,13 +756,14 @@ private var cmdReplyType: UInt8 = 0
os_log("打开A2DP解码并开始录制...", log: logger, type: .info)
- let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_RIGHT])
+ let paramData = Data([BleConst.CODEC_CONTROL_A2DP_PLAY, BleConst.AUDIO_CHANNEL_STEREO])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
}
/// 打开通话记录解码器并开始录制
func openCallRecordDecoder() -> Bool {
os_log("打开通话记录解码并开始录制...", log: logger, type: .info)
- let paramData = Data([BleConst.CODEC_CONTROL_CALL_RECORD_PLAY, BleConst.AUDIO_CHANNEL_RIGHT])
+ startOpusDecoding(sampleRate: 16000, channels: 2, frameSize: 1280)
+ let paramData = Data([BleConst.CODEC_CONTROL_CALL_RECORD_PLAY, BleConst.AUDIO_CHANNEL_STEREO])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
}
@@ -777,14 +787,15 @@ private var cmdReplyType: UInt8 = 0
requestStopOpusRecording()
// 不停止Opus解码流,只发送命令通知设备关闭编解码
- let paramData = Data([BleConst.CODEC_CONTROL_CLOSE, BleConst.AUDIO_CHANNEL_RIGHT])
+ let paramData = Data([BleConst.CODEC_CONTROL_CLOSE, BleConst.AUDIO_CHANNEL_STEREO])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
}
/// 播放A2DP
func playA2DP() -> Bool {
- os_log("切换至A2DP播放模式...", log: logger, type: .info)
- return sendCommand(BleConst.CMD_PLAY_A2DP)
+ // 与 Android 保持一致:不发送命令,直接返回 true
+ // Android 已注释掉此命令,且 0x08 在 Android 端定义为 CMD_GET_WAKEUP_VERSION
+ return true
}
/// 音量增大
@@ -815,7 +826,7 @@ private var cmdReplyType: UInt8 = 0
}
os_log("设置%@模式...", log: logger, type: .info, modeDesc)
- let paramData = Data([0xA1, channelMode])
+ let paramData = Data([BleConst.CODEC_CONTROL_ENCODE_ON, channelMode])
return sendCommand(BleConst.CMD_CONTROL_CODEC, data: paramData)
}
@@ -881,24 +892,57 @@ private var cmdReplyType: UInt8 = 0
- // 加入队列并触发发送
-
- if isReply{
+ // 与 Android 对齐:isReply=true 时直接发送,否则入队列
+ if isReply {
peripheralDevice?.writeValue(cmdPacket, for: characteristic, type: .withoutResponse)
- isReply = false;
- return true
-
+ isReply = false
+ startReplyTimeout()
+ return true
} else {
commandQueue.append(cmdPacket)
+ os_log("命令已加入队列,当前队列长度: %d", log: logger, type: .debug, commandQueue.count)
+ return true
}
- // 发送数据(使用无响应写入,与Android端保持一致)
-
- return true
} catch {
os_log("liwei---- 发送命令异常: %@", log: logger, type: .error, error.localizedDescription)
return false
}
}
+
+ /// 处理下一条队列命令(与 Android processNextCommand 对齐)
+ private func processNextCommand() {
+ guard !commandQueue.isEmpty else { return }
+ guard let characteristic = writeCharacteristic,
+ connectionState == BleConst.STATE_CONNECTED else { return }
+
+ let nextCmd = commandQueue.removeFirst()
+ peripheralDevice?.writeValue(nextCmd, for: characteristic, type: .withoutResponse)
+ isReply = false // 与 Android 一致:发送后置为 false,等待回复
+ startReplyTimeout()
+ os_log("发送队列中的下一个命令,剩余队列长度: %d", log: logger, type: .debug, commandQueue.count)
+ }
+
+ /// 启动回复超时计时器(与 Android REPLY_TIMEOUT_MS=1000 对齐)
+ private func startReplyTimeout() {
+ replyTimeoutTimer?.invalidate()
+ replyTimeoutTimer = Timer.scheduledTimer(withTimeInterval: REPLY_TIMEOUT_MS, repeats: false) { [weak self] _ in
+ guard let self = self else { return }
+ if !self.isReply {
+ os_log("命令响应超时,自动重置 isReply 为 true", log: self.logger, type: .default)
+ self.isReply = true
+ self.processNextCommand()
+ }
+ }
+ }
+
+ /// 命令回复处理(与 Android onCommandResponse 对齐)
+ private func onCommandResponse(_ cmdType: UInt8) {
+ isReply = true
+ cmdReplyType = cmdType
+ replyTimeoutTimer?.invalidate()
+ replyTimeoutTimer = nil
+ processNextCommand()
+ }
/// 计算CRC8/MAXIM校验值(与Android端保持一致)
private func calculateCRC8(data: Data) -> UInt8 {
@@ -1024,7 +1068,8 @@ private var cmdReplyType: UInt8 = 0
var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ")
// os_log("liwei---- 收到设备响应: %@", log: logger, type: .debug, hexString)
- // 检查数据长度(与 Android 统一失败判定保持一致)
+ // 统一失败判定:长度字段与实际长度不匹配时,跳过 CRC 直接上报
+ // (设备的短帧响应 CRC 可能基于不同帧格式计算,强校验会丢弃有效数据)
if data.count != Int(length) + 4 { // 帧头 + 命令 + 长度 + 数据 + CRC
os_log("设备响应数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count)
@@ -1038,27 +1083,21 @@ private var cmdReplyType: UInt8 = 0
let resultMap: [String: Any] = [
"success": true, // 与 Android 对齐(Dart 层按 rawData 判断)
"command": Int(cmd),
- "rawData": Data(raw)
+ "rawData": raw // [UInt8] 数组,确保 Dart 端收到 List
]
- // 推进命令队列(与后续正常路径一致)
- isReply = true
- cmdReplyType = cmd
- if !commandQueue.isEmpty {
- let nextCmd = commandQueue.removeFirst()
- peripheralDevice?.writeValue(nextCmd, for: characteristic, type: .withoutResponse)
- }
+ // 与 Android 对齐:通过 onCommandResponse 推进队列
+ onCommandResponse(cmd)
// 上报到 Dart 层
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
return
}
- // 计算CRC校验 - 不包括最后一个字节(CRC)
+ // 长度匹配的正常帧:进行 CRC 校验
let payload = data.prefix(data.count - 1)
let calculatedCrc = calculateCRC8(data: payload)
let receivedCrc = data[data.count - 1]
-
if calculatedCrc != receivedCrc {
os_log("设备响应CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc)
return
@@ -1066,12 +1105,8 @@ private var cmdReplyType: UInt8 = 0
// 提取响应数据
let responseData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data()
- isReply = true
- cmdReplyType = cmd
- if !commandQueue.isEmpty {
- let nextCmd = commandQueue.removeFirst()
- peripheralDevice?.writeValue(nextCmd, for: characteristic, type: .withoutResponse)
- }
+ // 与 Android 对齐:通过 onCommandResponse 推进队列
+ onCommandResponse(cmd)
// 根据命令类型处理设备响应
switch cmd {
@@ -1163,10 +1198,12 @@ private var cmdReplyType: UInt8 = 0
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
case BleConst.CMD_CONTROL_CODEC:
+ // 与 Android 对齐:rawData 使用 [UInt8] 数组,确保 Dart 端收到 List
+ let rawDataArray = [UInt8](responseData)
var resultMap: [String: Any] = [
"success": true,
"command": Int(cmd),
- "rawData": responseData
+ "rawData": rawDataArray
]
if !responseData.isEmpty && responseData.count >= 2 {