Browse Source

上传调试部署代码

newdev_shunjiawei
liwei1dao 4 months ago
parent
commit
d46a66675f
  1. 27
      lib/data/services/ble_manager.dart
  2. 10
      lib/modules/call_translation_debug/bindings/call_translation_debug_binding.dart
  3. 148
      lib/modules/call_translation_debug/controllers/call_translation_debug_controller.dart
  4. 300
      lib/modules/call_translation_debug/views/call_translation_debug_view.dart
  5. 20
      lib/modules/settings/views/settings_view.dart
  6. 9
      lib/routes/app_pages.dart
  7. 1
      lib/routes/app_routes.dart
  8. 3
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  9. 3
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  10. 18
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt
  11. 8
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  12. 248
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  13. 23
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  14. 42
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt
  15. 37
      local_plugins/ble_service/lib/ble_service.dart
  16. 3
      local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt
  17. 486
      pubspec.lock

27
lib/data/services/ble_manager.dart

@ -1247,6 +1247,33 @@ class BleManager extends GetxService {
}
}
/// 设置通话翻译调试参数(运行时生效)
/// [sendIntervalMs] 音频下行发送间隔(ms);[bundleFrameCount] 下行合包帧数
Future<bool> setCallTranslationDebugParams({
int? sendIntervalMs,
int? bundleFrameCount,
}) async {
try {
return await _bleService.setCallTranslationDebugParams(
sendIntervalMs: sendIntervalMs,
bundleFrameCount: bundleFrameCount,
);
} catch (e) {
Logger.e('CALL_TRANS_DEBUG', '设置通话翻译调试参数失败: ${e.toString()}');
return false;
}
}
/// 获取当前通话翻译调试参数 {'sendIntervalMs': int, 'bundleFrameCount': int}
Future<Map<String, int>> getCallTranslationDebugParams() async {
try {
return await _bleService.getCallTranslationDebugParams();
} catch (e) {
Logger.e('CALL_TRANS_DEBUG', '获取通话翻译调试参数失败: ${e.toString()}');
return {};
}
}
/// 打开解码器
Future<bool> openCallRecordDecoder() async {
try {

10
lib/modules/call_translation_debug/bindings/call_translation_debug_binding.dart

@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/call_translation_debug_controller.dart';
class CallTranslationDebugBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<CallTranslationDebugController>(
() => CallTranslationDebugController());
}
}

148
lib/modules/call_translation_debug/controllers/call_translation_debug_controller.dart

@ -0,0 +1,148 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../../data/services/ble_manager.dart';
import '../../../core/utils/logger.dart';
/// 通话翻译调试控制器
/// 负责调试参数(音频下行发送间隔、下行合包帧数)的读取、保存、下发,
/// 以及通话翻译编解码的启动/停止。
class CallTranslationDebugController extends GetxController {
final BleManager _bleManager = Get.find<BleManager>();
final GetStorage _storage = GetStorage();
// 持久化键
static const String _kSendInterval = 'call_trans_debug_send_interval';
static const String _kBundleFrameCount = 'call_trans_debug_bundle_frames';
// 默认值(与原生默认保持一致)
static const int defaultSendIntervalMs = 100;
static const int defaultBundleFrameCount = 4;
// 取值范围
static const int minSendIntervalMs = 20;
static const int maxSendIntervalMs = 1000;
static const int sendIntervalStepMs = 20; // 音频下行间隔步长 20ms
static const int minBundleFrameCount = 1;
static const int maxBundleFrameCount = 20;
/// 音频下行发送间隔(ms)
final RxInt sendIntervalMs = defaultSendIntervalMs.obs;
/// 下行合包帧数
final RxInt bundleFrameCount = defaultBundleFrameCount.obs;
/// 通话翻译编解码是否已开启
RxBool get isCodecActive => _bleManager.isCodecActiveRx;
/// 蓝牙是否已连接
RxBool get isConnected => _bleManager.isConnectedRx;
@override
void onInit() {
super.onInit();
_loadFromStorage();
// 进入界面时尝试用原生当前值同步(若原生已被修改)
_syncFromNative();
}
/// 从本地存储加载参数
void _loadFromStorage() {
final savedInterval = _storage.read<int>(_kSendInterval);
final savedFrames = _storage.read<int>(_kBundleFrameCount);
if (savedInterval != null) {
sendIntervalMs.value =
savedInterval.clamp(minSendIntervalMs, maxSendIntervalMs);
}
if (savedFrames != null) {
bundleFrameCount.value =
savedFrames.clamp(minBundleFrameCount, maxBundleFrameCount);
}
}
/// 用原生当前实际值同步显示(不覆盖用户本地保存的偏好,仅在无本地保存时参考)
Future<void> _syncFromNative() async {
try {
final params = await _bleManager.getCallTranslationDebugParams();
if (params.isEmpty) return;
// 仅当本地没有保存过时,采用原生返回值
if (_storage.read<int>(_kSendInterval) == null) {
final ni = params['sendIntervalMs'];
if (ni != null && ni > 0) {
sendIntervalMs.value = ni.clamp(minSendIntervalMs, maxSendIntervalMs);
}
}
if (_storage.read<int>(_kBundleFrameCount) == null) {
final nf = params['bundleFrameCount'];
if (nf != null && nf > 0) {
bundleFrameCount.value =
nf.clamp(minBundleFrameCount, maxBundleFrameCount);
}
}
} catch (e) {
Logger.e('CALL_TRANS_DEBUG', '同步原生参数失败: $e');
}
}
void setSendInterval(int value) {
// 按 20ms 步长对齐
final snapped =
(value / sendIntervalStepMs).round() * sendIntervalStepMs;
sendIntervalMs.value = snapped.clamp(minSendIntervalMs, maxSendIntervalMs);
}
void setBundleFrameCount(int value) {
bundleFrameCount.value =
value.clamp(minBundleFrameCount, maxBundleFrameCount);
}
/// 恢复默认值
void resetDefaults() {
sendIntervalMs.value = defaultSendIntervalMs;
bundleFrameCount.value = defaultBundleFrameCount;
}
/// 保存并下发参数到原生(运行时立即生效)
Future<void> applyParams() async {
_storage.write(_kSendInterval, sendIntervalMs.value);
_storage.write(_kBundleFrameCount, bundleFrameCount.value);
final ok = await _bleManager.setCallTranslationDebugParams(
sendIntervalMs: sendIntervalMs.value,
bundleFrameCount: bundleFrameCount.value,
);
Logger.i('CALL_TRANS_DEBUG',
'下发调试参数 interval=${sendIntervalMs.value}ms frames=${bundleFrameCount.value} result=$ok');
Get.snackbar(
'通话翻译调试',
ok ? '参数已应用' : '参数下发失败(请确认设备已连接)',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2),
);
}
/// 应用参数并启动通话翻译编解码
Future<void> applyAndStart() async {
if (!isConnected.value) {
Get.snackbar('通话翻译调试', '设备未连接,无法启动',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2));
return;
}
// 先下发参数再启动,确保编解码以新参数运行
await applyParams();
final ok = await _bleManager.openA2DPDecoder();
Logger.i('CALL_TRANS_DEBUG', '启动通话翻译编解码 result=$ok');
Get.snackbar('通话翻译调试', ok ? '已启动通话翻译' : '启动失败',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2));
}
/// 停止通话翻译编解码
Future<void> stop() async {
final ok = await _bleManager.closeCodec();
Logger.i('CALL_TRANS_DEBUG', '停止通话翻译编解码 result=$ok');
Get.snackbar('通话翻译调试', ok ? '已停止通话翻译' : '停止失败',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2));
}
}

300
lib/modules/call_translation_debug/views/call_translation_debug_view.dart

@ -0,0 +1,300 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/call_translation_debug_controller.dart';
/// 通话翻译调试界面
/// 可调整音频下行发送间隔与下行合包帧数,应用后启动/停止通话翻译。
class CallTranslationDebugView
extends GetView<CallTranslationDebugController> {
const CallTranslationDebugView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDark ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDark ? Colors.black : Colors.white,
elevation: 0.5,
centerTitle: true,
title: Text(
'通话翻译调试',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black,
),
),
),
body: ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [
_buildStatusCard(isDark),
SizedBox(height: 16.h),
_buildSendIntervalCard(isDark),
SizedBox(height: 16.h),
_buildBundleFrameCard(isDark),
SizedBox(height: 24.h),
_buildActionButtons(isDark),
],
),
);
}
Widget _card(bool isDark, {required Widget child}) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
borderRadius: BorderRadius.circular(16.r),
border: isDark
? Border.all(color: const Color(0xFF2C2C2C), width: 1)
: null,
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.4)
: Colors.black.withValues(alpha: 0.06),
blurRadius: 12,
offset: const Offset(0, 2),
),
],
),
child: child,
);
}
// 连接 / 编解码状态
Widget _buildStatusCard(bool isDark) {
return _card(
isDark,
child: Obx(() {
final connected = controller.isConnected.value;
final active = controller.isCodecActive.value;
return Row(
children: [
_statusDot(connected ? Colors.green : Colors.red),
SizedBox(width: 8.w),
Text(
connected ? '设备已连接' : '设备未连接',
style: TextStyle(
fontSize: 14.sp,
color: isDark ? Colors.white : Colors.black87,
),
),
const Spacer(),
_statusDot(active ? Colors.green : Colors.grey),
SizedBox(width: 8.w),
Text(
active ? '翻译运行中' : '翻译未启动',
style: TextStyle(
fontSize: 14.sp,
color: isDark ? Colors.white70 : Colors.black54,
),
),
],
);
}),
);
}
Widget _statusDot(Color color) {
return Container(
width: 10.w,
height: 10.w,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
}
// 音频下行发送间隔
Widget _buildSendIntervalCard(bool isDark) {
return _card(
isDark,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_title(isDark, '音频下行发送间隔',
'每包音频之间的发送间隔,值越小发送越快、占用带宽越高(${CallTranslationDebugController.minSendIntervalMs}~${CallTranslationDebugController.maxSendIntervalMs} ms,步长 ${CallTranslationDebugController.sendIntervalStepMs} ms)'),
SizedBox(height: 8.h),
Obx(
() => Row(
children: [
Expanded(
child: Slider(
value: controller.sendIntervalMs.value
.clamp(
CallTranslationDebugController.minSendIntervalMs,
CallTranslationDebugController.maxSendIntervalMs,
)
.toDouble(),
min: CallTranslationDebugController.minSendIntervalMs
.toDouble(),
max: CallTranslationDebugController.maxSendIntervalMs
.toDouble(),
divisions: (CallTranslationDebugController
.maxSendIntervalMs -
CallTranslationDebugController.minSendIntervalMs) ~/
CallTranslationDebugController.sendIntervalStepMs,
label: '${controller.sendIntervalMs.value} ms',
onChanged: (v) =>
controller.setSendInterval(v.round()),
),
),
_valueChip(isDark, '${controller.sendIntervalMs.value} ms'),
],
),
),
],
),
);
}
// 下行合包帧数
Widget _buildBundleFrameCard(bool isDark) {
return _card(
isDark,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_title(isDark, '下行合包帧数',
'左右声道各自累积多少帧 opus 合并为一包后下发,值越大单包越大、发送频率越低(${CallTranslationDebugController.minBundleFrameCount}~${CallTranslationDebugController.maxBundleFrameCount} 帧)'),
SizedBox(height: 8.h),
Obx(
() => Row(
children: [
Expanded(
child: Slider(
value: controller.bundleFrameCount.value
.clamp(
CallTranslationDebugController.minBundleFrameCount,
CallTranslationDebugController.maxBundleFrameCount,
)
.toDouble(),
min: CallTranslationDebugController.minBundleFrameCount
.toDouble(),
max: CallTranslationDebugController.maxBundleFrameCount
.toDouble(),
divisions:
CallTranslationDebugController.maxBundleFrameCount -
CallTranslationDebugController.minBundleFrameCount,
label: '${controller.bundleFrameCount.value} 帧',
onChanged: (v) =>
controller.setBundleFrameCount(v.round()),
),
),
_valueChip(isDark, '${controller.bundleFrameCount.value} 帧'),
],
),
),
],
),
);
}
Widget _title(bool isDark, String title, String subtitle) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black87,
),
),
SizedBox(height: 4.h),
Text(
subtitle,
style: TextStyle(
fontSize: 12.sp,
color: isDark ? Colors.white60 : Colors.black54,
height: 1.3,
),
),
],
);
}
Widget _valueChip(bool isDark, String text) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h),
decoration: BoxDecoration(
color: isDark
? Colors.blue[900]!.withValues(alpha: 0.3)
: Colors.blue[50],
borderRadius: BorderRadius.circular(10.r),
),
child: Text(
text,
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
color: isDark ? Colors.blue[200] : Colors.blue[700],
),
),
);
}
Widget _buildActionButtons(bool isDark) {
return Column(
children: [
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: controller.resetDefaults,
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
child: const Text('恢复默认'),
),
),
SizedBox(width: 12.w),
Expanded(
child: ElevatedButton(
onPressed: controller.applyParams,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
child: const Text('保存并应用'),
),
),
],
),
SizedBox(height: 12.h),
Obx(
() {
final active = controller.isCodecActive.value;
return SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: active ? controller.stop : controller.applyAndStart,
icon: Icon(active ? Icons.stop : Icons.play_arrow),
label: Text(active ? '停止通话翻译' : '应用并启动通话翻译'),
style: ElevatedButton.styleFrom(
backgroundColor: active ? Colors.red[600] : Colors.green[600],
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 14.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
),
);
},
),
],
);
}
}

20
lib/modules/settings/views/settings_view.dart

@ -616,6 +616,26 @@ class SettingsView extends GetView<SettingsController> {
)
: const SizedBox.shrink()),
Obx(() => controller.showDebugInfo.value
? _buildSimpleNavigationSetting(
title: '通话翻译调试',
subtitle: '调整发送间隔/合包帧数后启动通话翻译',
icon: Icons.tune,
iconBgColor: isDarkMode
? Colors.blue[900]!.withValues(alpha: 0.3)
: Colors.blue[100]!,
iconColor: isDarkMode
? Colors.blue[300]!
: Colors.blue[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.callTransDebug);
},
isDarkMode: isDarkMode,
)
: const SizedBox.shrink()),
Obx(() => controller.showDebugInfo.value
? _buildSimpleNavigationSetting(
title: '日志导出',

9
lib/routes/app_pages.dart

@ -26,6 +26,8 @@ import '../modules/opus_test/bindings/opus_test_binding.dart';
import '../modules/opus_test/views/opus_test_view.dart';
import '../modules/log_export/bindings/log_export_binding.dart';
import '../modules/log_export/views/log_export_view.dart';
import '../modules/call_translation_debug/bindings/call_translation_debug_binding.dart';
import '../modules/call_translation_debug/views/call_translation_debug_view.dart';
import '../modules/profile/views/profile_view.dart';
import '../modules/profile/views/profile_meeting_view.dart';
import '../modules/profile/bindings/profile_binding.dart';
@ -293,6 +295,13 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.callTransDebug,
page: () => const CallTranslationDebugView(),
binding: CallTranslationDebugBinding(),
transition: Transition.rightToLeft,
transitionDuration: const Duration(milliseconds: 250),
),
GetPage(
name: Routes.agent,
page: () => const AgentView(),

1
lib/routes/app_routes.dart

@ -47,6 +47,7 @@ abstract class Routes {
static const jieliTest = '/jieli_test'; // 杰理芯片测试
static const opusTest = '/opus_test'; // Opus编码测试
static const logExport = '/log_export'; // 日志导出
static const callTransDebug = '/call_trans_debug'; // 通话翻译调试
static const bleTest = '/ble_test'; // 蓝牙测试
static const speechTest = '/speech_test'; // 综合语音测试

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

@ -209,6 +209,9 @@ object BleAgent : BleService.Callback, AgentServiceListener {
override fun onCallSleepReceived(channel: Int) {
}
override fun onCallResumeReceived(channel: Int) {
}
//============================================================================================

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

@ -2070,6 +2070,9 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin, ActivityAware,
override fun onCallSleepReceived(channel: Int) {
}
override fun onCallResumeReceived(channel: Int) {
}
// TODO: 双端翻译时启用副芯片回调
// override fun onSecondaryChipConnectionStateChangedDetail(state: Int, deviceAddress: String?, reason: String?) { }
// override fun onSecondaryChipConnectionsCountChanged(count: Int, addresses: List<String>) { }

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

@ -91,6 +91,12 @@ class BleCommandSender {
*/
fun notifyCallSleepReceived(channel: Int)
/**
* 通知声道通话恢复指令接收
* @param channel 恢复声道:BleConst.AUDIO_CHANNEL_LEFT(左) / BleConst.AUDIO_CHANNEL_RIGHT(右)
*/
fun notifyCallResumeReceived(channel: Int)
/**
* 通知开始 AI 单次对话
*/
@ -385,6 +391,8 @@ class BleCommandSender {
BleConst.CODEC_CONTROL_ENCODE_ON -> "已打开编码"
BleConst.CODEC_CONTROL_CALL_SLEEP_LEFT -> "左声道通话休眠"
BleConst.CODEC_CONTROL_CALL_SLEEP_RIGHT -> "右声道通话休眠"
BleConst.CODEC_CONTROL_CALL_RESUME_LEFT -> "左声道通话恢复"
BleConst.CODEC_CONTROL_CALL_RESUME_RIGHT -> "右声道通话恢复"
else -> "未知状态($codecStatus)"
}
@ -591,7 +599,7 @@ class BleCommandSender {
}
BleConst.CMD_CONTROL_CODEC -> {
// 设备主动上报编解码控制状态,关心左/右声道通话休眠 (0xF0 / 0xF1)
// 设备主动上报编解码控制状态,关心左/右声道通话休眠 (0xF0 / 0xF1) 与恢复 (0xF2 / 0xF3)
// 帧格式: 0xCC 0x05 [len] [codecStatus, channelMode, ...] [crc]
val codecStatus =
if (notifyData.isNotEmpty()) notifyData[0].toInt() and 0xFF else -1
@ -604,6 +612,14 @@ class BleCommandSender {
Log.i(TAG, "收到主动上报: 右声道通话休眠 (codecStatus=0xF1)")
callback?.notifyCallSleepReceived(BleConst.AUDIO_CHANNEL_RIGHT)
}
BleConst.CODEC_CONTROL_CALL_RESUME_LEFT -> {
Log.i(TAG, "收到主动上报: 左声道通话恢复 (codecStatus=0xF2)")
callback?.notifyCallResumeReceived(BleConst.AUDIO_CHANNEL_LEFT)
}
BleConst.CODEC_CONTROL_CALL_RESUME_RIGHT -> {
Log.i(TAG, "收到主动上报: 右声道通话恢复 (codecStatus=0xF3)")
callback?.notifyCallResumeReceived(BleConst.AUDIO_CHANNEL_RIGHT)
}
else -> {
Log.d(TAG, "收到主动上报编解码控制: codecStatus=0x${codecStatus.toString(16)}")
}

8
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt

@ -121,10 +121,14 @@ object BleConst {
const val CODEC_CONTROL_A2DP_PLAY = 0xA2
/** mic和dac(音乐或者通话远端)声音 */
const val CODEC_CONTROL_CALL_RECORD_PLAY = 0xA3
/** 左声道通话休眠(休眠期间暂停下发左声道,仅传输右声道,固定 1s 后自动恢复) */
/** 左声道通话休眠(休眠期间暂停下发左声道,仅传输右声道,直到收到 F2 恢复指令才恢复) */
const val CODEC_CONTROL_CALL_SLEEP_LEFT = 0xF0
/** 右声道通话休眠(休眠期间暂停下发右声道,仅传输左声道,固定 1s 后自动恢复) */
/** 右声道通话休眠(休眠期间暂停下发右声道,仅传输左声道,直到收到 F3 恢复指令才恢复) */
const val CODEC_CONTROL_CALL_SLEEP_RIGHT = 0xF1
/** 左声道通话恢复(收到后立即恢复左声道下发,按序补发休眠期间积压的左声道包,与 F0 配对) */
const val CODEC_CONTROL_CALL_RESUME_LEFT = 0xF2
/** 右声道通话恢复(收到后立即恢复右声道下发,按序补发休眠期间积压的右声道包,与 F1 配对) */
const val CODEC_CONTROL_CALL_RESUME_RIGHT = 0xF3
/** 打开编码指令 */
const val CODEC_CONTROL_ENCODE_ON = 0xB1
/** 左声道 */

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

@ -74,6 +74,9 @@ object BleService {
// 声道通话休眠相关回调
fun onCallSleepReceived(channel: Int)
// 声道通话恢复相关回调
fun onCallResumeReceived(channel: Int)
}
// ======================================================================================================
@ -140,25 +143,37 @@ object BleService {
private var audioSendThread: Thread? = null
private val audioSendHandler = Handler(Looper.getMainLooper())
private val isAudioSending = AtomicBoolean(false)
// 上一包音频实际发送的时间戳(ms),仅用于发送节拍日志统计间隔;-1 表示尚未发送过
private var lastAudioSendTime = -1L
// 左/右声道各自上一包已发送的序号,用于在日志里核对每个声道的包序是否连续(+1);-1 表示尚未发送过
private var lastLeftSeq = -1L
private var lastRightSeq = -1L
// 下行写入连续失败(底层发送缓冲拥塞)的计数,用于节流告警与恢复提示;写成功即清零
private var writeFailRetryCount = 0
// 写失败(拥塞)后的退避时长(ms):给底层发送缓冲腾空的时间,再重试同一包,期间绝不丢包
private const val WRITE_RETRY_BACKOFF_MS = 5L
// 音频数据分块发送的常量
private val AUDIO_CHUNK_SIZE = 120 // 每次发送120字节
private const val AUDIO_SEND_INTERVAL_NORMAL = 60L // 正常发送间隔60ms
private const val CHANNEL_SLEEP_DURATION = 400L // 收到 F0/F1 后该声道固定休眠 1s,到期自动恢复
// 左/右声道休眠到期时间戳(SystemClock.elapsedRealtime 基准,0 表示未休眠)。
// 收到 F0(左)/F1(右) 时置为 now + CHANNEL_SLEEP_DURATION,
// 休眠窗口内该声道的包暂不发送(转入积压缓存),仅传输另一声道,到期后按序补发,不丢包。
private const val DEFAULT_AUDIO_SEND_INTERVAL_NORMAL = 40L // 默认音频下行发送间隔(ms)
// 音频下行发送间隔(ms),调试界面可在运行时调整;通过 setCallTranslationDebugParams() 修改
@Volatile
private var audioSendIntervalNormal = DEFAULT_AUDIO_SEND_INTERVAL_NORMAL
// 左/右声道休眠标记(true 表示该声道处于休眠,暂停下发)。
// 收到 F0(左)/F1(右) 时置 true,收到 F2(左)/F3(右) 恢复指令时置 false。
// 休眠期间该声道的包暂不发送(转入积压缓存),仅传输另一声道,恢复后按序补发,不丢包。
@Volatile
private var leftChannelSleepUntil: Long = 0L
private var leftChannelSleeping: Boolean = false
@Volatile
private var rightChannelSleepUntil: Long = 0L
private var rightChannelSleeping: Boolean = false
// 声道休眠期间该声道的包不丢弃,转入对应积压缓存,待声道唤醒后按 FIFO 顺序补发。
// 仅在音频发送线程内访问,无需额外加锁。
private val leftHoldBuffer = ArrayDeque<ByteArray>()
private val rightHoldBuffer = ArrayDeque<ByteArray>()
// 积压缓存安全上限(防止设备持续上报休眠导致内存无限增长)。
// 1s 休眠约积压 16 包,留足余量;超限时丢弃最旧包并告警(仅在异常场景触发)。
private const val CHANNEL_HOLD_BUFFER_MAX = 200
// 要求绝对不丢包,故大幅抬高上限作为纯防 OOM 的兜底;单包约 165B,10000 包≈1.6MB。
private const val CHANNEL_HOLD_BUFFER_MAX = 10000
// 左右积压同时可补发时的轮转标记,保证两声道公平补发、避免一方饿死。
private var holdDrainPreferLeft = true
// 音频数据缓冲区,用于累积数据到80字节再发送
@ -372,6 +387,14 @@ object BleService {
this@BleService.notifyCallSleepReceived(channel)
}
/**
* 通知声道通话恢复指令接收
*/
override fun notifyCallResumeReceived(channel: Int) {
// 使用外部类的方法来处理声道通话恢复指令,避免无限递归
this@BleService.notifyCallResumeReceived(channel)
}
/**
* 通知开始 AI 单次对话
*/
@ -1364,6 +1387,11 @@ object BleService {
audioSendThread = Thread {
Log.i(TAG, "音频发送线程已启动")
// 固定节拍的下一拍绝对时刻(ms)。每发完一包就 += 间隔,按这个时刻补偿性 sleep,
// 把 poll 等待和发送耗时一起抵消掉,保证实际发送周期稳定贴近 audioSendIntervalNormal。
// 0 表示尚未起拍,由首包发送时初始化。
var nextSendAt = 0L
while (isAudioSending.get() && !Thread.currentThread().isInterrupted) {
try {
// 1) 从主队列取新包,按声道分流到积压缓存。
@ -1377,11 +1405,10 @@ object BleService {
}
}
// 2) 选出一包可发送的数据:声道已唤醒(不在休眠窗口)且缓存非空。
// 2) 选出一包可发送的数据:声道未休眠且缓存非空。
// 左右都可发时按 holdDrainPreferLeft 轮转,保证两声道公平补发、避免一方饿死。
val now = SystemClock.elapsedRealtime()
val leftReady = leftHoldBuffer.isNotEmpty() && now >= leftChannelSleepUntil
val rightReady = rightHoldBuffer.isNotEmpty() && now >= rightChannelSleepUntil
val leftReady = leftHoldBuffer.isNotEmpty() && !leftChannelSleeping
val rightReady = rightHoldBuffer.isNotEmpty() && !rightChannelSleeping
val audioChunk: ByteArray? = when {
leftReady && rightReady -> {
holdDrainPreferLeft = !holdDrainPreferLeft
@ -1394,27 +1421,83 @@ object BleService {
}
if (audioChunk != null) {
// 调试录音文件只保留拼接后的纯 opus 数据,剥离 5 字节包头再写入
if (audioChunk.size > 5) {
val payload = audioChunk.copyOfRange(5, audioChunk.size)
recordfile1?.saveAudioDataToWav(payload)
}
// 解析包头供发送节拍日志使用
if (audioChunk.size >= 5) {
val seq = ((audioChunk[0].toInt() and 0xFF) shl 24) or
((audioChunk[1].toInt() and 0xFF) shl 16) or
((audioChunk[2].toInt() and 0xFF) shl 8) or
(audioChunk[3].toInt() and 0xFF)
val chTag = when (audioChunk[4]) {
// 解析包头:声道(byte[4]) 与 序号(byte[0..3] 小端),供日志与重试归位使用
val channelByte = if (audioChunk.size >= 5) audioChunk[4] else null
val seq = if (audioChunk.size >= 5)
((audioChunk[3].toInt() and 0xFF) shl 24) or
((audioChunk[2].toInt() and 0xFF) shl 16) or
((audioChunk[1].toInt() and 0xFF) shl 8) or
(audioChunk[0].toInt() and 0xFF)
else -1
val chTag = when (channelByte) {
OpusAudioManager.CHANNEL_LEFT -> "L"
OpusAudioManager.CHANNEL_RIGHT -> "R"
else -> "?(${audioChunk[4]})"
else -> "?($channelByte)"
}
// 同步写入,拿到真实的成功/失败:false=底层发送缓冲已满(拥塞),作为背压信号
val sent = sendAudioChunkBlocking(audioChunk)
if (!sent) {
// 写失败:把这一包放回原声道队头,下轮重试,绝不丢弃(满足“绝对不丢包”)。
// 包仍在队头,声道内顺序不变。
if (channelByte == OpusAudioManager.CHANNEL_RIGHT) rightHoldBuffer.addFirst(audioChunk)
else leftHoldBuffer.addFirst(audioChunk)
writeFailRetryCount++
// 节流告警,避免拥塞期刷屏
if (writeFailRetryCount == 1 || writeFailRetryCount % 50 == 0) {
Log.w(TAG, "下行写入拥塞,重试中 ch=$chTag seq=$seq 连续失败=$writeFailRetryCount " +
"hold(L=${leftHoldBuffer.size},R=${rightHoldBuffer.size})")
}
// 退避,给底层缓冲腾空时间;重置节拍,待恢复后重新起拍
nextSendAt = 0L
Thread.sleep(WRITE_RETRY_BACKOFF_MS)
} else {
if (writeFailRetryCount > 0) {
Log.i(TAG, "下行写入已恢复,之前连续失败=$writeFailRetryCount")
writeFailRetryCount = 0
}
Log.i(TAG, "音频下行发送 ch=$chTag seq=$seq size=${audioChunk.size}B " +
// 调试录音文件只保留拼接后的纯 opus 数据,剥离 5 字节包头再写入(仅成功时写,避免重试重复)
if (audioChunk.size > 5) {
recordfile1?.saveAudioDataToWav(audioChunk.copyOfRange(5, audioChunk.size))
}
// 发送成功后才推进发送节拍统计与声道包序
if (audioChunk.size >= 5) {
val now = System.currentTimeMillis()
val deltaMs = if (lastAudioSendTime < 0) 0 else now - lastAudioSendTime
lastAudioSendTime = now
// 按声道统计各自的包序增量:正常应为 +1,出现非 +1 即说明该声道丢包或乱序
val seqGap = when (channelByte) {
OpusAudioManager.CHANNEL_LEFT -> {
val gap = if (lastLeftSeq < 0) 1L else seq.toLong() - lastLeftSeq
lastLeftSeq = seq.toLong()
gap
}
OpusAudioManager.CHANNEL_RIGHT -> {
val gap = if (lastRightSeq < 0) 1L else seq.toLong() - lastRightSeq
lastRightSeq = seq.toLong()
gap
}
else -> 0L
}
val gapTag = if (seqGap == 1L) "" else " !gap=$seqGap"
Log.i(TAG, "音频下行发送 ch=$chTag seq=$seq Δ=${deltaMs}ms size=${audioChunk.size}B " +
"lastSeq(L=$lastLeftSeq,R=$lastRightSeq)$gapTag " +
"hold(L=${leftHoldBuffer.size},R=${rightHoldBuffer.size}) queue=${audioSendQueue.size}")
}
sendAudioChunkWithCallback(audioChunk)
Thread.sleep(AUDIO_SEND_INTERVAL_NORMAL)
// 固定节拍:睡到 nextSendAt 这个绝对时刻,把 poll/发送耗时算进同一周期,间隔不随它们抖动。
val nowMs = System.currentTimeMillis()
if (nextSendAt == 0L) nextSendAt = nowMs
nextSendAt += audioSendIntervalNormal
val sleepMs = nextSendAt - nowMs
if (sleepMs > 0) {
Thread.sleep(sleepMs)
} else {
// 落后于节拍(发送/积压处理超时),重新对齐,避免之后疯狂追发
nextSendAt = System.currentTimeMillis()
}
}
}
} catch (e: InterruptedException) {
Log.d(TAG, "音频发送线程被中断")
@ -1459,11 +1542,43 @@ object BleService {
audioBuffer.clear() // 清空音频缓冲区
leftHoldBuffer.clear() // 清空左声道积压缓存
rightHoldBuffer.clear() // 清空右声道积压缓存
leftChannelSleepUntil = 0L
rightChannelSleepUntil = 0L
leftChannelSleeping = false
rightChannelSleeping = false
lastAudioSendTime = -1L // 重置发送节拍计时,下次首包 Δ 从 0 开始
lastLeftSeq = -1L // 重置左右声道包序追踪
lastRightSeq = -1L
writeFailRetryCount = 0 // 重置写入拥塞计数
Log.i(TAG, "音频发送线程已停止,队列和缓冲区已清空")
}
/**
* 同步发送一包音频数据,返回 writeCharacteristic 的真实结果。
* 直接在发送线程调用 writeCharacteristic(GATT 写允许在工作线程执行),
* 这样发送线程能拿到 true/false 作为背压信号:
* - true : 已成功塞入底层发送缓冲
* - false : 底层发送缓冲已满(拥塞) 或 连接异常 → 调用方应放回队头重试,不丢包
* 写类型为 WRITE_TYPE_NO_RESPONSE(不等对端 ACK),保证吞吐。
* @return 是否成功写入
*/
private fun sendAudioChunkBlocking(chunk: ByteArray): Boolean {
return try {
val gatt = bluetoothGatt
val ch = callWriteChar
if (ch == null || gatt == null) {
Log.e(TAG, "蓝牙连接或特征值未准备就绪")
false
} else {
ch.value = chunk
val ok = gatt.writeCharacteristic(ch) == true
if (ok) bytesSentInCurrentSecond += chunk.size
ok
}
} catch (e: Exception) {
Log.e(TAG, "发送音频数据块异常: ${e.message}", e)
false
}
}
/**
* 发送音频数据块并等待写入回调(新方法)
* @param chunk 要发送的音频数据块
@ -1592,6 +1707,37 @@ object BleService {
Log.e(TAG, failureReason.toString())
}
// ======================================================================================================
// 通话翻译调试参数 - 运行时可调
// ======================================================================================================
/**
* 设置通话翻译调试参数(运行时生效,无需重连)。
* @param sendIntervalMs 音频下行发送间隔(ms),null 或 <=0 表示不修改
* @param bundleFrameCount 下行合包帧数,null 或 <=0 表示不修改
*/
fun setCallTranslationDebugParams(sendIntervalMs: Long?, bundleFrameCount: Int?) {
if (sendIntervalMs != null && sendIntervalMs > 0) {
audioSendIntervalNormal = sendIntervalMs.coerceIn(1L, 1000L)
Log.i(TAG, "[CALL_TRANS_DEBUG] 设置音频下行发送间隔: ${audioSendIntervalNormal}ms")
}
if (bundleFrameCount != null && bundleFrameCount > 0) {
opusAudioManager.setBundleFrameCount(bundleFrameCount)
Log.i(TAG, "[CALL_TRANS_DEBUG] 设置下行合包帧数: ${opusAudioManager.getBundleFrameCount()}")
}
}
/**
* 获取当前通话翻译调试参数。
* @return Map: sendIntervalMs(Long) / bundleFrameCount(Int)
*/
fun getCallTranslationDebugParams(): Map<String, Any> {
return mapOf(
"sendIntervalMs" to audioSendIntervalNormal,
"bundleFrameCount" to opusAudioManager.getBundleFrameCount()
)
}
// ======================================================================================================
// 设备控制方法 - 使用新的指令发送器
// ======================================================================================================
@ -1997,21 +2143,20 @@ object BleService {
/**
* 收到设备主动上报的左/右声道通话休眠 (F0/F1) 时调用。
* 将对应声道的休眠到期时间置为 now + CHANNEL_SLEEP_DURATION(1s),
* 休眠窗口内该声道的包转入积压缓存暂不发送、仅传输另一声道,到期后按序补发,不丢包。
* 重复触发只是刷新到期时间,不会叠加。
* 将对应声道置为休眠:该声道的包转入积压缓存暂不发送、仅传输另一声道,
* 直到收到对应的恢复指令 (F2/F3) 才恢复并按序补发,不丢包。
* 重复触发幂等,不会叠加。
* @param channel BleConst.AUDIO_CHANNEL_LEFT(左) / BleConst.AUDIO_CHANNEL_RIGHT(右)
*/
private fun notifyCallSleepReceived(channel: Int) {
val until = SystemClock.elapsedRealtime() + CHANNEL_SLEEP_DURATION
when (channel) {
BleConst.AUDIO_CHANNEL_LEFT -> {
leftChannelSleepUntil = until
Log.i(TAG, "收到左声道休眠指令,暂停左声道下发 ${CHANNEL_SLEEP_DURATION}ms")
leftChannelSleeping = true
Log.i(TAG, "收到左声道休眠指令(F0),暂停左声道下发,等待恢复指令(F2)")
}
BleConst.AUDIO_CHANNEL_RIGHT -> {
rightChannelSleepUntil = until
Log.i(TAG, "收到右声道休眠指令,暂停右声道下发 ${CHANNEL_SLEEP_DURATION}ms")
rightChannelSleeping = true
Log.i(TAG, "收到右声道休眠指令(F1),暂停右声道下发,等待恢复指令(F3)")
}
else -> Log.w(TAG, "收到未知声道休眠指令: channel=$channel")
}
@ -2024,6 +2169,33 @@ object BleService {
}
}
/**
* 收到设备主动上报的左/右声道通话恢复 (F2/F3) 时调用。
* 解除对应声道的休眠标记,发送线程随即按 FIFO 顺序补发休眠期间积压的该声道包,不丢包。
* 未处于休眠时收到恢复指令为幂等操作。
* @param channel BleConst.AUDIO_CHANNEL_LEFT(左) / BleConst.AUDIO_CHANNEL_RIGHT(右)
*/
private fun notifyCallResumeReceived(channel: Int) {
when (channel) {
BleConst.AUDIO_CHANNEL_LEFT -> {
leftChannelSleeping = false
Log.i(TAG, "收到左声道恢复指令(F2),恢复左声道下发,补发积压 ${leftHoldBuffer.size} 包")
}
BleConst.AUDIO_CHANNEL_RIGHT -> {
rightChannelSleeping = false
Log.i(TAG, "收到右声道恢复指令(F3),恢复右声道下发,补发积压 ${rightHoldBuffer.size} 包")
}
else -> Log.w(TAG, "收到未知声道恢复指令: channel=$channel")
}
for (callback in callbacks) {
try {
callback.onCallResumeReceived(channel)
} catch (e: Exception) {
Log.e(TAG, "分发声道通话恢复回调异常", e)
}
}
}
/**
* 向所有回调监听器分发开始 AI 单次对话
*/

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

@ -230,6 +230,15 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
val success = BleService.playA2DP()
result.success(success)
}
"setCallTranslationDebugParams" -> {
val sendIntervalMs = (call.argument<Number>("sendIntervalMs"))?.toLong()
val bundleFrameCount = (call.argument<Number>("bundleFrameCount"))?.toInt()
BleService.setCallTranslationDebugParams(sendIntervalMs, bundleFrameCount)
result.success(true)
}
"getCallTranslationDebugParams" -> {
result.success(BleService.getCallTranslationDebugParams())
}
// 新增Companion Device相关方法
"isCompanionSupported" -> {
val isSupported = bleCompanionManager.isCompanionDeviceManagerSupported()
@ -464,4 +473,18 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
"发送声道通话休眠事件异常"
)
}
override fun onCallResumeReceived(channel: Int) {
val channelName = when (channel) {
BleConst.AUDIO_CHANNEL_LEFT -> "左声道"
BleConst.AUDIO_CHANNEL_RIGHT -> "右声道"
else -> "未知声道($channel)"
}
Log.i(TAG, "[Flutter推送] 声道通话恢复: $channelName (channel=$channel)")
sendEvent(
statusEventSink,
mapOf("type" to "callResume", "channel" to channel),
"发送声道通话恢复事件异常"
)
}
}

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

@ -42,8 +42,11 @@ class OpusAudioManager {
const val CHANNEL_LEFT: Byte = 0
const val CHANNEL_RIGHT: Byte = 1
// 下行合包:左右各自累积 BUNDLE_FRAME_COUNT 帧 opus 合并为 1 包后才下发,降低 BLE 发送频率
private const val BUNDLE_FRAME_COUNT = 4
// 下行合包默认帧数:左右各自累积 bundleFrameCount 帧 opus 合并为 1 包后才下发,降低 BLE 发送频率
// 调试界面可在运行时调整;通过 setBundleFrameCount() 修改
const val DEFAULT_BUNDLE_FRAME_COUNT = 4
@Volatile
private var bundleFrameCount = DEFAULT_BUNDLE_FRAME_COUNT
// 合包头部:bytes 0-3 序号(小端 uint32),byte 4 声道标识(0=左/1=右)
private const val BUNDLE_HEADER_SIZE = 5
}
@ -108,10 +111,10 @@ class OpusAudioManager {
private var rightEncodeManager: OpusManager? = null
private var isDualEncoding = false
// 左右各自的 opus 帧缓存与发送序号;累满 BUNDLE_FRAME_COUNT 帧后打包下发
// 左右各自的 opus 帧缓存与发送序号;累满 bundleFrameCount 帧后打包下发
private val bundleLock = Any()
private val leftBundleFrames = ArrayList<ByteArray>(BUNDLE_FRAME_COUNT)
private val rightBundleFrames = ArrayList<ByteArray>(BUNDLE_FRAME_COUNT)
private val leftBundleFrames = ArrayList<ByteArray>(DEFAULT_BUNDLE_FRAME_COUNT)
private val rightBundleFrames = ArrayList<ByteArray>(DEFAULT_BUNDLE_FRAME_COUNT)
private var leftBundleSeq = 0
private var rightBundleSeq = 0
@ -435,7 +438,7 @@ class OpusAudioManager {
// ======================================================================================================
// 双声道独立编码方法(左右声道各用独立 OpusManager,互不污染预测状态)
// 输出格式:每包 = [4B 序号(小端 uint32)] + [1B 声道(0=左/1=右)] + [N × opus 帧]
// 左右各自累积 BUNDLE_FRAME_COUNT 帧后打包一次,通过 onAudioDataEncoded 下发
// 左右各自累积 bundleFrameCount 帧后打包一次,通过 onAudioDataEncoded 下发
// ======================================================================================================
/**
@ -535,7 +538,7 @@ class OpusAudioManager {
/**
* 写入左声道 mono PCM(本端翻译音频)
* 累满 BUNDLE_FRAME_COUNT 帧 opus 后通过 onAudioDataEncoded 下发:[4B 序号] + [1B 声道=0] + [N × opus]
* 累满 bundleFrameCount 帧 opus 后通过 onAudioDataEncoded 下发:[4B 序号] + [1B 声道=0] + [N × opus]
*/
fun writeLeftPcm(pcm: ByteArray) {
if (!isDualEncoding) {
@ -548,7 +551,7 @@ class OpusAudioManager {
/**
* 写入右声道 mono PCM(对端翻译音频)
* 累满 BUNDLE_FRAME_COUNT 帧 opus 后通过 onAudioDataEncoded 下发:[4B 序号] + [1B 声道=1] + [N × opus]
* 累满 bundleFrameCount 帧 opus 后通过 onAudioDataEncoded 下发:[4B 序号] + [1B 声道=1] + [N × opus]
*/
fun writeRightPcm(pcm: ByteArray) {
if (!isDualEncoding) {
@ -560,15 +563,32 @@ class OpusAudioManager {
}
/**
* 单帧入合包缓冲;左右各自累满 BUNDLE_FRAME_COUNT 帧后整包通过回调下发
* 设置下行合包帧数(调试用)。取值范围 1..20,超出自动钳制。
* 立即生效:后续累满新帧数即打包下发,已缓存的不足帧待补满后随新值打包。
* @param count 每包累积的 opus 帧数
*/
fun setBundleFrameCount(count: Int) {
val clamped = count.coerceIn(1, 20)
bundleFrameCount = clamped
Log.i(TAG, "设置下行合包帧数: $clamped")
}
/**
* 获取当前下行合包帧数
*/
fun getBundleFrameCount(): Int = bundleFrameCount
/**
* 单帧入合包缓冲;左右各自累满 bundleFrameCount 帧后整包通过回调下发
* 输出包布局:[seq(4B 小端 uint32)] + [channel(1B)] + [N 帧 opus 顺次拼接]
*/
private fun emitChannelFrame(channelPrefix: Byte, opusFrame: ByteArray) {
val frameCount = bundleFrameCount
val packet: ByteArray? = synchronized(bundleLock) {
when (channelPrefix) {
CHANNEL_LEFT -> {
leftBundleFrames.add(opusFrame)
if (leftBundleFrames.size >= BUNDLE_FRAME_COUNT) {
if (leftBundleFrames.size >= frameCount) {
val p = buildBundlePacket(leftBundleSeq, CHANNEL_LEFT, leftBundleFrames)
leftBundleSeq++
leftBundleFrames.clear()
@ -577,7 +597,7 @@ class OpusAudioManager {
}
CHANNEL_RIGHT -> {
rightBundleFrames.add(opusFrame)
if (rightBundleFrames.size >= BUNDLE_FRAME_COUNT) {
if (rightBundleFrames.size >= frameCount) {
val p = buildBundlePacket(rightBundleSeq, CHANNEL_RIGHT, rightBundleFrames)
rightBundleSeq++
rightBundleFrames.clear()

37
local_plugins/ble_service/lib/ble_service.dart

@ -172,6 +172,43 @@ class BleService {
}
}
/// 设置通话翻译调试参数(运行时生效)
/// [sendIntervalMs] 音频下行发送间隔(ms)
/// [bundleFrameCount] 下行合包帧数
Future<bool> setCallTranslationDebugParams({
int? sendIntervalMs,
int? bundleFrameCount,
}) async {
try {
final result = await _methodChannel
.invokeMethod<bool>('setCallTranslationDebugParams', {
if (sendIntervalMs != null) 'sendIntervalMs': sendIntervalMs,
if (bundleFrameCount != null) 'bundleFrameCount': bundleFrameCount,
});
return result ?? false;
} catch (e) {
print('设置通话翻译调试参数失败: $e');
return false;
}
}
/// 获取当前通话翻译调试参数
/// 返回 {'sendIntervalMs': int, 'bundleFrameCount': int},失败返回空 Map
Future<Map<String, int>> getCallTranslationDebugParams() async {
try {
final result = await _methodChannel
.invokeMethod<Map>('getCallTranslationDebugParams');
if (result == null) return {};
return {
'sendIntervalMs': (result['sendIntervalMs'] as num?)?.toInt() ?? 0,
'bundleFrameCount': (result['bundleFrameCount'] as num?)?.toInt() ?? 0,
};
} catch (e) {
print('获取通话翻译调试参数失败: $e');
return {};
}
}
/// 启动BLE后台服务
Future<bool> startBleService(Map<String, dynamic> config) async {

3
local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt

@ -415,4 +415,7 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
override fun onCallSleepReceived(channel: Int) {
}
override fun onCallResumeReceived(channel: Int) {
}
}

486
pubspec.lock

File diff suppressed because it is too large
Loading…
Cancel
Save