Browse Source

加入ota升级,加入ble音视频翻译

weicu
fdp 1 year ago
parent
commit
6f2b593401
  1. 4
      lib/data/services/asr_service.dart
  2. 10
      lib/data/services/ble_manager.dart
  3. 9
      lib/data/services/network/api.dart
  4. 7
      lib/data/services/speech_impl/azure_asr_service.dart
  5. 3
      lib/data/services/speech_impl/flutter_asr_service.dart
  6. 8
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  7. 10
      lib/data/services/speech_impl/volcano_asr_service.dart
  8. 3
      lib/data/services/speech_impl/xunfei_asr_service.dart
  9. 27
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  10. 14
      lib/modules/chat/controllers/chat_controller.dart
  11. 3
      lib/modules/meeting/controllers/meeting_record_controller.dart
  12. 9
      lib/modules/ota_upgrade/bindings/ota_upgrade_binding.dart
  13. 213
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  14. 54
      lib/modules/ota_upgrade/ota.dart
  15. 109
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  16. 28
      lib/modules/settings/views/settings_view.dart
  17. 12
      lib/modules/speech_test/controllers/speech_test_controller.dart
  18. 44
      lib/modules/translation/controllers/translation_controller.dart
  19. 14
      lib/routes/app_pages.dart
  20. 2
      lib/routes/app_routes.dart
  21. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  22. 3
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  23. 39
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  24. 4
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  25. 41
      local_plugins/ble_service/lib/ble_service.dart

4
lib/data/services/asr_service.dart

@ -19,7 +19,9 @@ abstract class AsrService {
/// 开始连续语音识别 /// 开始连续语音识别
/// ///
/// 返回一个包含识别事件的流 /// 返回一个包含识别事件的流
Future<Stream<RecognitionEvent>> startContinuousRecognition(); Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType,
);
/// 停止连续语音识别 /// 停止连续语音识别
Future<bool> stopContinuousRecognition(); Future<bool> stopContinuousRecognition();

10
lib/data/services/ble_manager.dart

@ -537,6 +537,16 @@ class BleManager extends GetxService {
} }
} }
/// 打开解码器
Future<bool> openA2DPDecoder() async {
try {
return await _bleService.openA2DPDecoder();
} catch (e) {
Logger.error('打开解码器失败: $e');
return false;
}
}
/// 关闭编解码器 /// 关闭编解码器
Future<bool> closeCodec() async { Future<bool> closeCodec() async {
try { try {

9
lib/data/services/network/api.dart

@ -73,4 +73,13 @@ class Api {
params: params, params: params,
); );
} }
//app获取设备信息
static getequipmentinfo([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_getequipmentinfo',
params: params,
);
}
} }

7
lib/data/services/speech_impl/azure_asr_service.dart

@ -150,7 +150,8 @@ class AzureAsrService extends GetxService implements AsrService {
} }
@override @override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async { Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
if (!_isInitialized) { if (!_isInitialized) {
await initialize(); await initialize();
} }
@ -164,7 +165,9 @@ class AzureAsrService extends GetxService implements AsrService {
// 开始连续识别 // 开始连续识别
final bool result = final bool result =
await _channel.invokeMethod('startContinuousRecognition'); await _channel.invokeMethod('startContinuousRecognition', {
'audioSourceType': audioSourceType,
});
if (!result) { if (!result) {
_cleanupEventStream(); _cleanupEventStream();

3
lib/data/services/speech_impl/flutter_asr_service.dart

@ -363,7 +363,8 @@ class FlutterAsrService extends GetxService implements AsrService {
} }
@override @override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async { Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
try { try {
if (!_isAvailable.value) { if (!_isAvailable.value) {
final controller = StreamController<RecognitionEvent>.broadcast(); final controller = StreamController<RecognitionEvent>.broadcast();

8
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -36,7 +36,8 @@ class VolcanoAsrApiService implements AsrService {
final int _bitsPerSample = 16; final int _bitsPerSample = 16;
final int _channels = 1; final int _channels = 1;
final String _encoding = 'pcm'; final String _encoding = 'pcm';
// 音频输入源
bool _audioSourceType = false;
// 连接相关 // 连接相关
WebSocketChannel? _channel; WebSocketChannel? _channel;
Completer<void>? _connectionCompleter; Completer<void>? _connectionCompleter;
@ -113,7 +114,7 @@ class VolcanoAsrApiService implements AsrService {
final resultCompleter = Completer<RecognitionEvent>(); final resultCompleter = Completer<RecognitionEvent>();
// 开始连续识别流程 // 开始连续识别流程
final stream = await startContinuousRecognition(); final stream = await startContinuousRecognition(_audioSourceType);
// 监听结果 // 监听结果
StreamSubscription? subscription; StreamSubscription? subscription;
@ -156,7 +157,8 @@ class VolcanoAsrApiService implements AsrService {
} }
@override @override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async { Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
if (!_isInitialized) { if (!_isInitialized) {
await initialize(); await initialize();
} }

10
lib/data/services/speech_impl/volcano_asr_service.dart

@ -265,7 +265,8 @@ class VolcanoAsrService extends GetxService implements AsrService {
/// 开始连续语音识别 /// 开始连续语音识别
@override @override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async { Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
if (!await _ensureInitialized()) { if (!await _ensureInitialized()) {
_eventStreamController.add(RecognitionEvent( _eventStreamController.add(RecognitionEvent(
type: RecognitionEventType.error, type: RecognitionEventType.error,
@ -275,9 +276,10 @@ class VolcanoAsrService extends GetxService implements AsrService {
} }
try { try {
final success = final success = await _channel.invokeMethod<bool>('audioSourceType', {
await _channel.invokeMethod<bool>('startContinuousRecognition') ?? 'audioSourceType': audioSourceType,
false; }) ??
false;
if (success) { if (success) {
_isContinuousRecognitionActive = true; _isContinuousRecognitionActive = true;

3
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -75,7 +75,8 @@ class XunfeiAsrService extends GetxService implements AsrService {
} }
@override @override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async { Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
if (!_isInitialized) { if (!_isInitialized) {
await initialize(); await initialize();
} }

27
lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart

@ -9,8 +9,8 @@ import '../../models/events/voice_interaction_event.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
enum VoiceInteractionState { enum VoiceInteractionState {
idle, // 等待唤醒 idle, // 等待唤醒
active, // 活跃状态 - 可以同时识别和播放 active, // 活跃状态 - 可以同时识别和播放
} }
class IosVoiceInteractionService extends GetxService { class IosVoiceInteractionService extends GetxService {
@ -45,14 +45,16 @@ class IosVoiceInteractionService extends GetxService {
// 当前识别的文本 // 当前识别的文本
String _currentRecognizedText = ''; String _currentRecognizedText = '';
// 音频输入源
bool _audioSourceType = false;
// 事件流控制器 // 事件流控制器
final StreamController<VoiceInteractionEvent> _eventStreamController = final StreamController<VoiceInteractionEvent> _eventStreamController =
StreamController<VoiceInteractionEvent>.broadcast(); StreamController<VoiceInteractionEvent>.broadcast();
// 获取事件流 // 获取事件流
@override @override
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream; Stream<VoiceInteractionEvent> get eventStream =>
_eventStreamController.stream;
// 标识符,用于存储聊天历史 // 标识符,用于存储聊天历史
final String _agentId = 'ios_assistant'; final String _agentId = 'ios_assistant';
@ -102,21 +104,19 @@ class IosVoiceInteractionService extends GetxService {
try { try {
// 开始连续识别 // 开始连续识别
final recognitionStream = await _asrService.startContinuousRecognition(); final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
// 监听识别结果 // 监听识别结果
_recognitionSubscription = recognitionStream.listen( _recognitionSubscription =
_handleRecognitionEvent, recognitionStream.listen(_handleRecognitionEvent, onError: (error) {
onError: (error) { Logger.error('语音识别错误: $error');
Logger.error('语音识别错误: $error'); _startContinuousListening(); // 尝试重新启动
_startContinuousListening(); // 尝试重新启动 });
}
);
Logger.info('开始连续语音识别'); Logger.info('开始连续语音识别');
} catch (e) { } catch (e) {
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: $e');
} }
} }
@ -464,4 +464,3 @@ class IosVoiceInteractionService extends GetxService {
Logger.info('已发送识别开始事件'); Logger.info('已发送识别开始事件');
} }
} }

14
lib/modules/chat/controllers/chat_controller.dart

@ -78,6 +78,9 @@ class ChatController extends GetxController {
// 取消标志 // 取消标志
bool _shouldCancelAiResponse = false; bool _shouldCancelAiResponse = false;
// 音频输入源
bool _audioSourceType = false;
// 自动停止语音识别计时器 // 自动停止语音识别计时器
Timer? _autoStopTimer; Timer? _autoStopTimer;
@ -184,7 +187,6 @@ class ChatController extends GetxController {
// 自动停止计时器会在startVoiceRecognition中启动 // 自动停止计时器会在startVoiceRecognition中启动
} }
_getLocation(); _getLocation();
player.playerStateStream.listen((state) async { player.playerStateStream.listen((state) async {
@ -211,8 +213,6 @@ class ChatController extends GetxController {
_amapFlutter.startLocation(); _amapFlutter.startLocation();
} }
@override @override
void onClose() { void onClose() {
Logger.info('ChatController onClose'); Logger.info('ChatController onClose');
@ -295,7 +295,6 @@ class ChatController extends GetxController {
Future<void> sendMessage(String text) async { Future<void> sendMessage(String text) async {
if (text.trim().isEmpty) return; if (text.trim().isEmpty) return;
// 中断当前响应 // 中断当前响应
_interruptCurrentResponse("发送新消息,中断当前响应"); _interruptCurrentResponse("发送新消息,中断当前响应");
@ -558,12 +557,11 @@ class ChatController extends GetxController {
Timer(const Duration(milliseconds: 260), () async { Timer(const Duration(milliseconds: 260), () async {
if (isDebounce) return; if (isDebounce) return;
// 中断当前响应 // 中断当前响应
_interruptCurrentResponse("发送新消息,中断当前响应"); _interruptCurrentResponse("发送新消息,中断当前响应");
try { try {
final recognitionStream = final recognitionStream =
await _asrService.startContinuousRecognition(); await _asrService.startContinuousRecognition(_audioSourceType);
await Future.delayed(const Duration(milliseconds: 260)); await Future.delayed(const Duration(milliseconds: 260));
isDebounce = true; isDebounce = true;
recognizedText.value = ''; recognizedText.value = '';
@ -619,11 +617,11 @@ class ChatController extends GetxController {
Future<void> startVoiceRecognition() async { Future<void> startVoiceRecognition() async {
if (isVoiceRecognitionActive.value) return; if (isVoiceRecognitionActive.value) return;
try { try {
// 启动连续语音识别 // 启动连续语音识别
// 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据 // 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据
final recognitionStream = await _asrService.startContinuousRecognition(); final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
isVoiceRecognitionActive.value = true; isVoiceRecognitionActive.value = true;
isListening.value = true; isListening.value = true;

3
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -111,7 +111,8 @@ class MeetingRecordController extends GetxController
//开始录音 //开始录音
Future<void> _startRecorder() async { Future<void> _startRecorder() async {
final recognitionStream = await _asrService.startContinuousRecognition(); final recognitionStream =
await _asrService.startContinuousRecognition(false);
_recognitionSubscription = recognitionStream.listen( _recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent, _handleRecognitionEvent,
); );

9
lib/modules/ota_upgrade/bindings/ota_upgrade_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/ota_upgrade_controller.dart';
class OtaUpgradeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<OtaUpgradeController>(() => OtaUpgradeController());
}
}

213
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import '../../../data/services/network/api.dart';
import '../ota.dart';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:json_annotation/json_annotation.dart';
class OtaUpgradeController extends GetxController {
final Dio _dio = Dio();
final RxString latestVersion = ''.obs;
final RxString currentVersion = ''.obs;
String updatepackageaddress = '';
final RxDouble downloadProgress = 0.0.obs;
final RxBool isUpdating = false.obs;
final isUpdateAvailable = true.obs;
final RxDouble progress = 0.0.obs;
@override
void onInit() {
super.onInit();
_getCurrentVersion();
}
// 获取当前版本信息
Future<void> _getCurrentVersion() async {
//发送ble获取固件版本
var firmwareVersion = "1.1.0";
currentVersion.value = firmwareVersion;
//发送ble获取产品id
var id = "a2";
//发送api请求
final response = await Api.getequipmentinfo({
"id": id,
});
if (response['info'] != '') {
print('getLoginToken:进来了');
// 根据API设计调整
final resp = OtaConfigResp.fromJson(response);
print('成功解析: ${resp.id}, ${resp.factory}');
latestVersion.value = resp.systemversion!;
updatepackageaddress = resp.updatepackageaddress!;
if (_isNewVersionAvailable(resp.systemversion!)) {
//需要升级
print('需要更新');
isUpdateAvailable.value = false;
} else {
isUpdateAvailable.value = true;
//已经是最新版本更新ui,按钮隐藏
}
} else {
throw const FormatException('API 响应缺少 "info" 字段');
}
}
// 版本比较方法优化
bool _isNewVersionAvailable(String latest) {
try {
List<int> currentParts = currentVersion.value
.split('.')
.map((e) => int.parse(e.trim()))
.toList();
List<int> latestParts =
latest.split('.').map((e) => int.parse(e.trim())).toList();
for (int i = 0; i < latestParts.length; i++) {
int current = (i < currentParts.length) ? currentParts[i] : 0;
if (latestParts[i] > current) return true;
if (latestParts[i] < current) return false;
}
return false;
} catch (e) {
print('版本比较错误: $e');
return false;
}
}
// 版本更新
Future<void> checkUpdate() async {
try {
if (latestVersion.value != "")
_showUpdateDialog(latestVersion.value);
else {
Get.snackbar('错误', '获取最新版本信息失败');
}
} catch (e) {
Get.snackbar('错误', '获取版本信息失败');
}
}
// 显示更新对话框
void _showUpdateDialog(String latest) {
Get.dialog(
AlertDialog(
title: Text('发现新版本 $latest'),
content: Text('是否立即更新?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('取消'),
),
TextButton(
onPressed: () {
Get.back();
startDownload();
},
child: Text('更新'),
),
],
),
);
}
// 开始下载
Future<void> startDownload() async {
isUpdating.value = true;
// 查找最后一个 '/' 的位置
int lastIndex = updatepackageaddress.lastIndexOf('/');
// 截取最后一个 '/' 后面的字符串
String fileName = updatepackageaddress.substring(lastIndex + 1);
String directoryName = "ota_upgrades";
final dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 5),
receiveTimeout: Duration.zero,
sendTimeout: const Duration(seconds: 5),
headers: {'Accept-Encoding': 'gzip'},
));
Get.snackbar('升级命令已发送', '开始下载...');
try {
final Directory? downloadsDir = await getDownloadsDirectory();
if (downloadsDir == null) {
throw FileSystemException('无法访问下载目录');
}
// 创建子目录:Download/APP
final targetDir = Directory('${downloadsDir.path}/$directoryName');
if (!await targetDir.exists()) {
await targetDir.create(recursive: true);
}
final savePath = '${targetDir.path}/$fileName';
print('文件保存路径: $savePath'); // 关键日志
final options = Options(
receiveTimeout: Duration.zero,
followRedirects: true,
maxRedirects: 5,
);
DateTime lastUpdate = DateTime.now();
await dio.download(
updatepackageaddress,
savePath,
options: options,
onReceiveProgress: (received, total) {
final now = DateTime.now();
if (now.difference(lastUpdate).inMilliseconds > 1000) {
lastUpdate = now;
String progressMsg = total != -1
? '${(received / total * 100).toStringAsFixed(1)}%'
: '已下载:${(received / 1024 / 1024).toStringAsFixed(1)}MB';
Get.snackbar(
'升级进度',
progressMsg,
duration: const Duration(seconds: 1),
);
}
},
);
// 下载完成后验证文件存在性
File file = File(savePath);
if (await file.exists()) {
Get.snackbar('下载完成', '文件已保存至: $savePath'); // 明确提示路径
} else {
Get.snackbar('错误', '文件未找到,请检查写入权限');
}
isUpdating.value = false;
//下载完成,开始升级
_installApp(savePath);
} on DioException catch (e) {
Get.snackbar('下载失败', '错误: ${e.message}');
} on FileSystemException catch (e) {
Get.snackbar('存储失败', '无法写入文件: ${e.message}');
} catch (e) {
Get.snackbar('未知错误', e.toString());
}
}
// 开始升级
void _installApp(String path) async {
// final File file = File(path);
// if (await file.exists()) {
// // OpenResult result = await OpenFile.open(path);
// // if (result.type != ResultType.done) {
// // Get.snackbar('错误', '安装失败: ${result.message}');
// // }
// }
}
}

54
lib/modules/ota_upgrade/ota.dart

@ -0,0 +1,54 @@
class OtaConfigResp {
// 单例实例
static OtaConfigResp? _instance;
// 单例访问器(增加空安全保护)
static OtaConfigResp get instance {
if (_instance == null) {
throw Exception("User must be initialized first");
}
return _instance!;
}
// 最终属性(不可变)
final String? id;
final String? factory;
final String? equipmentname;
final String? systemversion;
final String? updatepackageaddress;
// 私有构造函数
OtaConfigResp._({
this.id,
this.factory,
this.equipmentname,
this.systemversion,
this.updatepackageaddress,
});
factory OtaConfigResp.fromJson(Map<String, dynamic> json) {
print("数据获取fronTokenJson${json}");
// 修复1:安全获取嵌套的 user 字段
final infoData = json['info'] as Map<String, dynamic>?; // 允许空值
// 更新单例实例
return _instance = OtaConfigResp._(
id: (infoData?['id'] as String?) ?? '',
factory: (infoData?['factory'] as String?) ?? '',
equipmentname: (infoData?['equipmentname'] as String?) ?? '',
systemversion: (infoData?['systemversion'] as String?) ?? '',
updatepackageaddress:
(infoData?['updatepackageaddress'] as String?) ?? '',
);
}
// 完整的JSON序列化
Map<String, dynamic> toJson() => {
'user': {
'id': instance.id,
'factory': instance.factory,
'equipmentname': instance.equipmentname,
'systemversion': instance.systemversion,
'updatepackageaddress': instance.updatepackageaddress,
}
};
}

109
lib/modules/ota_upgrade/views/ota_upgrade_view.dart

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/ota_upgrade_controller.dart';
class OtaUpgradeView extends GetView<OtaUpgradeController> {
const OtaUpgradeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
// 添加外层Center
child: SingleChildScrollView(
// 添加滚动防止小屏溢出
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, // 水平居中
mainAxisSize: MainAxisSize.min, // 紧凑布局
children: [
// 设备图标
CircleAvatar(
radius: 60,
backgroundColor: Colors.blue[100],
child: Icon(
Icons.phone_iphone,
size: 60,
color: Colors.blue,
),
),
SizedBox(height: 30),
// 版本信息
Column(
children: [
Text(
'当前版本',
style: TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
SizedBox(height: 8),
Text(
'v${controller.currentVersion}',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
SizedBox(height: 40),
// 更新按钮/进度条
Obx(() {
if (controller.isUpdating.value) {
return Column(
children: [
SizedBox(
width: 200, // 限制进度条宽度
child: LinearProgressIndicator(
value: controller.downloadProgress.value,
backgroundColor: Colors.grey[200],
valueColor:
AlwaysStoppedAnimation<Color>(Colors.blue),
minHeight: 12,
borderRadius: BorderRadius.circular(10),
),
),
SizedBox(height: 20),
Text(
'${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%',
style: TextStyle(color: Colors.grey),
)
],
);
} else {
return ElevatedButton(
onPressed: controller.isUpdateAvailable.value
? null
: controller.checkUpdate,
style: ElevatedButton.styleFrom(
backgroundColor: controller.isUpdateAvailable.value
? Colors.grey
: Colors.blue,
foregroundColor: Colors.white,
padding:
EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Text(
controller.isUpdateAvailable.value ? '已是最新版本' : '立即更新',
style: TextStyle(fontSize: 16),
),
);
}
}),
],
),
),
),
),
);
}
}

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

@ -554,11 +554,32 @@ class SettingsView extends GetView<SettingsController> {
title: '语音测试', title: '语音测试',
subtitle: '测试语音识别和合成功能,支持多种服务实现', subtitle: '测试语音识别和合成功能,支持多种服务实现',
icon: Icons.record_voice_over, icon: Icons.record_voice_over,
iconBgColor: isDarkMode ? Colors.orange[900]!.withOpacity(0.3) : Colors.orange[100]!, iconBgColor: isDarkMode
iconColor: isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, ? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!,
iconColor:
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
onTap: () => Get.toNamed(Routes.speechTest), onTap: () => Get.toNamed(Routes.speechTest),
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 测试ble耳机固件版本升级
_buildSimpleNavigationSetting(
title: 'ota升级',
subtitle: '测试ble耳机固件版本升级',
icon: Icons.record_voice_over,
iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!,
iconColor:
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
onTap: () => Get.toNamed(Routes.OTA_UPGRADE),
isDarkMode: isDarkMode,
),
Divider( Divider(
height: 1, height: 1,
color: isDarkMode color: isDarkMode
@ -1190,8 +1211,7 @@ class SettingsView extends GetView<SettingsController> {
Obx(() { Obx(() {
final serviceType = controller.speechServiceType.value; final serviceType = controller.speechServiceType.value;
final serviceName = controller.availableSpeechServices final serviceName = controller.availableSpeechServices
.firstWhere( .firstWhere((service) => service['type'] == serviceType,
(service) => service['type'] == serviceType,
orElse: () => {'name': 'unknown'})['name']; orElse: () => {'name': 'unknown'})['name'];
return Container( return Container(
constraints: BoxConstraints(maxWidth: 120.w), // 限制最大宽度 constraints: BoxConstraints(maxWidth: 120.w), // 限制最大宽度

12
lib/modules/speech_test/controllers/speech_test_controller.dart

@ -26,16 +26,17 @@ class SpeechTestController extends GetxController {
// 识别结果 // 识别结果
final RxString intermediateResult = ''.obs; // 中间结果 final RxString intermediateResult = ''.obs; // 中间结果
final RxString finalResult = ''.obs; // 最终结果 final RxString finalResult = ''.obs; // 最终结果
final RxString recognitionStatus = '准备就绪'.obs; final RxString recognitionStatus = '准备就绪'.obs;
final RxString detectedLanguage = ''.obs; // 检测到的语言 final RxString detectedLanguage = ''.obs; // 检测到的语言
// TTS事件 // TTS事件
final RxString ttsStatus = '准备就绪'.obs; final RxString ttsStatus = '准备就绪'.obs;
// 当前选择的语音服务类型 // 当前选择的语音服务类型
final Rx<SpeechServiceType> selectedServiceType = SpeechServiceType.azure.obs; final Rx<SpeechServiceType> selectedServiceType = SpeechServiceType.azure.obs;
// 音频输入源
bool _audioSourceType = false;
// 连续识别结果流 // 连续识别结果流
StreamSubscription<RecognitionEvent>? _recognitionSubscription; StreamSubscription<RecognitionEvent>? _recognitionSubscription;
@ -267,7 +268,6 @@ class SpeechTestController extends GetxController {
// 刷新队列,确保播放完成 // 刷新队列,确保播放完成
await _ttsService.flushStream(); await _ttsService.flushStream();
} catch (e) { } catch (e) {
ttsStatus.value = '连续播放异常: $e'; ttsStatus.value = '连续播放异常: $e';
Logger.error('连续播放异常: $e'); Logger.error('连续播放异常: $e');
@ -344,11 +344,11 @@ class SpeechTestController extends GetxController {
recognitionStatus.value = '准备连续识别...'; recognitionStatus.value = '准备连续识别...';
// 获取识别事件流 - 这启动了异步识别过程 // 获取识别事件流 - 这启动了异步识别过程
final stream = await _asrService.startContinuousRecognition(); final stream =
await _asrService.startContinuousRecognition(_audioSourceType);
// 设置事件监听 - 异步处理识别结果 // 设置事件监听 - 异步处理识别结果
_setupAsrEventListener(stream); _setupAsrEventListener(stream);
} catch (e) { } catch (e) {
recognitionStatus.value = '启动连续识别异常: $e'; recognitionStatus.value = '启动连续识别异常: $e';
Logger.error('启动连续识别异常: $e'); Logger.error('启动连续识别异常: $e');

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

@ -9,28 +9,9 @@ import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../../data/models/events/voice_interaction_event.dart'; import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/asr_service.dart'; import '../../../data/services/asr_service.dart';
import 'package:jieli/jieli.dart'; import '../../../data/services/ble_manager.dart';
class TranslationController extends GetxController { class TranslationController extends GetxController {
late Jieli jieli;
// 事件订阅
StreamSubscription? _scanSubscription;
StreamSubscription? _connectionSubscription;
StreamSubscription? _wakeupSubscription;
StreamSubscription? _audioSubscription;
StreamSubscription? _commandResultSubscription;
// 状态变量
var isScanning = false.obs;
var deviceInfo = '未获取'.obs;
var connectStatus = '未连接'.obs;
var currentDevice = Rx<BleDevice?>(null);
var scannedDevices = RxList<BleDevice>([]);
var wakeupReceived = false.obs;
var audioDataSize = 0.obs;
var receivedFrames = 0.obs;
var audioDataReceiving = false.obs;
var volumeCommandResult = ''.obs;
// 服务 // 服务
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
final VolcanoTranslationService _translationService = final VolcanoTranslationService _translationService =
@ -38,7 +19,10 @@ class TranslationController extends GetxController {
final TtsService _ttsService = Get.find<TtsService>(); final TtsService _ttsService = Get.find<TtsService>();
final LanguageManager _languageManager = Get.find<LanguageManager>(); final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
// 蓝牙服务
final _bleManager = Get.find<BleManager>();
// 音频输入源
bool _audioSourceType = false;
// 存储相关 // 存储相关
static const String _historyKey = 'translation_history'; static const String _historyKey = 'translation_history';
static const String _sourceLanguageKey = 'translation_source_language'; static const String _sourceLanguageKey = 'translation_source_language';
@ -104,7 +88,7 @@ class TranslationController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
jieli = Jieli();
//_setupEventListeners(); //_setupEventListeners();
// 加载上次保存的语言设置 // 加载上次保存的语言设置
_loadSavedLanguages(); _loadSavedLanguages();
@ -240,19 +224,23 @@ class TranslationController extends GetxController {
// // 添加到历史记录 // // 添加到历史记录
// translationHistory.add(newItem); // translationHistory.add(newItem);
// 开始连续语音识别
if (currentMode.value == "audioVideo") { if (currentMode.value == "audioVideo") {
_bleManager.openDecoder();
// 发送ble音乐或者通话远端声音 // 发送ble音乐或者通话远端声音
_audioSourceType = true;
Logger.info('发送ble音乐或者通话远端声音'); Logger.info('发送ble音乐或者通话远端声音');
} else if (currentMode.value == "call") { } else if (currentMode.value == "call") {
// 发送ble系统mic和dac(音乐或者通话远端)声音 // 发送ble系统mic和dac(音乐或者通话远端)声音
_bleManager.openA2DPDecoder();
_audioSourceType = true;
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
} else {
// 开始连续语音识别
_audioSourceType = false;
} }
final recognitionStream =
// 开始连续语音识别 await _asrService.startContinuousRecognition(_audioSourceType);
final recognitionStream = await _asrService.startContinuousRecognition();
isRecognizing.value = true; isRecognizing.value = true;
// 监听识别结果 // 监听识别结果

14
lib/routes/app_pages.dart

@ -41,6 +41,8 @@ import '../modules/speech_test/views/speech_test_view.dart';
import '../modules/speech_test/bindings/speech_test_binding.dart'; import '../modules/speech_test/bindings/speech_test_binding.dart';
import '../modules/agent/views/agent_view.dart'; import '../modules/agent/views/agent_view.dart';
import '../modules/agent/bindings/agent_binding.dart'; import '../modules/agent/bindings/agent_binding.dart';
import '../modules/ota_upgrade/views/ota_upgrade_view.dart';
import '../modules/ota_upgrade/bindings/ota_upgrade_binding.dart';
abstract class AppPages { abstract class AppPages {
static final pages = [ static final pages = [
@ -79,6 +81,16 @@ abstract class AppPages {
page: () => const ChatView(), page: () => const ChatView(),
binding: ChatBinding(), binding: ChatBinding(),
), ),
GetPage(
name: Routes.recording,
page: () => const ChatView(),
binding: ChatBinding(),
),
GetPage(
name: Routes.OTA_UPGRADE,
page: () => const OtaUpgradeView(),
binding: OtaUpgradeBinding(),
),
GetPage( GetPage(
name: Routes.profile, name: Routes.profile,
page: () => const ProfileView(), page: () => const ProfileView(),
@ -144,13 +156,11 @@ abstract class AppPages {
page: () => const OpusTestView(), page: () => const OpusTestView(),
binding: OpusTestBinding(), binding: OpusTestBinding(),
), ),
GetPage( GetPage(
name: Routes.speechTest, name: Routes.speechTest,
page: () => const SpeechTestView(), page: () => const SpeechTestView(),
binding: SpeechTestBinding(), binding: SpeechTestBinding(),
), ),
GetPage( GetPage(
name: Routes.agent, name: Routes.agent,
page: () => const AgentView(), page: () => const AgentView(),

2
lib/routes/app_routes.dart

@ -6,6 +6,8 @@ abstract class Routes {
static const home = '/home'; static const home = '/home';
static const explore = '/explore'; static const explore = '/explore';
static const chat = '/chat'; static const chat = '/chat';
static const OTA_UPGRADE = '/ota_upgrade';
static const recording = '/recording';
static const profile = '/profile'; static const profile = '/profile';
static const meeting = '/meeting'; static const meeting = '/meeting';
static const meetingRecord = '/meetingRecord'; static const meetingRecord = '/meetingRecord';

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

@ -288,7 +288,7 @@ object AgentService : CoroutineScope {
// 检查录音权限 // 检查录音权限
if (!isExternalActive||!checkRecordAudioPermission()) { if (!isExternalActive&&!checkRecordAudioPermission()) {
FileLogger.e(TAG, "需要录音权限,请在设置中授予权限") FileLogger.e(TAG, "需要录音权限,请在设置中授予权限")
sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
return false return false

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

@ -172,7 +172,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
} }
} }
"startContinuousRecognition" -> { "startContinuousRecognition" -> {
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false val useExternalAudio = call.argument<Boolean>("audioSourceType") ?: false
// 确保事件通道已准备好 // 确保事件通道已准备好
if (asrEventSink == null) { if (asrEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null) result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
@ -187,6 +187,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
} else { } else {
AzureAsrHelper.AudioSourceType.MICROPHONE AzureAsrHelper.AudioSourceType.MICROPHONE
} }
val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) { override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(mapOf( sendAsrEvent(mapOf(

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

@ -999,8 +999,45 @@ object BleService {
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openDecoder(): Boolean { fun openDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...")
val opusStarted = startOpusStreamDecoding(
hasHeader = false, // BLE传输的Opus数据通常无文件头
channel = 1, // 单声道
sampleRate = 16000,// Opus推荐16kHz或更高
packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM)
// packetSize for Opus is typically in ms, e.g. 20, 40, 60.
// The library might expect this in a different unit or derive it.
// Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
)
FileLogger.i(TAG, "打开解码...") FileLogger.i(TAG, "打开解码...")
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte())) return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte()))
}
/**
* 控制编解码 - 打开解码
*/
fun openA2DPDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...")
val opusStarted = startOpusStreamDecoding(
hasHeader = false, // BLE传输的Opus数据通常无文件头
channel = 1, // 单声道
sampleRate = 16000,// Opus推荐16kHz或更高
packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM)
// packetSize for Opus is typically in ms, e.g. 20, 40, 60.
// The library might expect this in a different unit or derive it.
// Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
)
FileLogger.i(TAG, "打开解码...")
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte()))
} }
/** /**

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

@ -151,6 +151,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback {
"openDecoder" -> { "openDecoder" -> {
val success = BleService.openDecoder() val success = BleService.openDecoder()
result.success(success) result.success(success)
}
"openA2DPDecoder" -> {
val success = BleService.openA2DPDecoder()
result.success(success)
} }
"closeCodec" -> { "closeCodec" -> {
val success = BleService.closeCodec() val success = BleService.closeCodec()

41
local_plugins/ble_service/lib/ble_service.dart

@ -20,7 +20,8 @@ class BleService {
factory BleService() => _instance; factory BleService() => _instance;
// 方法通道 // 方法通道
final MethodChannel _methodChannel = const MethodChannel('com.yunqiinnovation.ble_service'); final MethodChannel _methodChannel =
const MethodChannel('com.yunqiinnovation.ble_service');
// 事件通道 - 简化为只使用数据和状态通道 // 事件通道 - 简化为只使用数据和状态通道
late final EventChannel _dataEventChannel; late final EventChannel _dataEventChannel;
@ -38,8 +39,10 @@ class BleService {
// 内部构造函数 // 内部构造函数
BleService._internal() { BleService._internal() {
_dataEventChannel = const EventChannel('com.yunqiinnovation.ble_service/data_events'); _dataEventChannel =
_statusEventChannel = const EventChannel('com.yunqiinnovation.ble_service/status_events'); const EventChannel('com.yunqiinnovation.ble_service/data_events');
_statusEventChannel =
const EventChannel('com.yunqiinnovation.ble_service/status_events');
// 初始化事件流 // 初始化事件流
_dataStream = _dataEventChannel.receiveBroadcastStream(); _dataStream = _dataEventChannel.receiveBroadcastStream();
@ -71,7 +74,8 @@ class BleService {
/// 获取设备版本信息 /// 获取设备版本信息
Future<bool> getDeviceVersion() async { Future<bool> getDeviceVersion() async {
try { try {
final result = await _methodChannel.invokeMethod<bool>('getDeviceVersion'); final result =
await _methodChannel.invokeMethod<bool>('getDeviceVersion');
return result ?? false; return result ?? false;
} catch (e) { } catch (e) {
print('获取设备版本失败: $e'); print('获取设备版本失败: $e');
@ -112,6 +116,17 @@ class BleService {
} }
} }
/// 打开解码器
Future<bool> openA2DPDecoder() async {
try {
final result = await _methodChannel.invokeMethod<bool>('openA2DPDecoder');
return result ?? false;
} catch (e) {
print('打开解码器失败: $e');
return false;
}
}
/// 关闭编解码器 /// 关闭编解码器
Future<bool> closeCodec() async { Future<bool> closeCodec() async {
try { try {
@ -144,7 +159,8 @@ class BleService {
/// - device_name: 设备名称 /// - device_name: 设备名称
Future<bool> startBleService(Map<String, dynamic> config) async { Future<bool> startBleService(Map<String, dynamic> config) async {
try { try {
final result = await _methodChannel.invokeMethod<bool>('startBleService', config); final result =
await _methodChannel.invokeMethod<bool>('startBleService', config);
return result ?? false; return result ?? false;
} catch (e) { } catch (e) {
print('启动BLE后台服务失败: $e'); print('启动BLE后台服务失败: $e');
@ -169,7 +185,8 @@ class BleService {
/// 这是确保设备可以在应用后台时被自动连接的关键功能 /// 这是确保设备可以在应用后台时被自动连接的关键功能
Future<bool> registerBackgroundScan() async { Future<bool> registerBackgroundScan() async {
try { try {
final result = await _methodChannel.invokeMethod<bool>('registerBackgroundScan'); final result =
await _methodChannel.invokeMethod<bool>('registerBackgroundScan');
return result ?? false; return result ?? false;
} catch (e) { } catch (e) {
print('注册后台扫描失败: $e'); print('注册后台扫描失败: $e');
@ -182,7 +199,8 @@ class BleService {
/// 当不再需要后台扫描功能时调用,例如用户主动断开连接时 /// 当不再需要后台扫描功能时调用,例如用户主动断开连接时
Future<bool> unregisterBackgroundScan() async { Future<bool> unregisterBackgroundScan() async {
try { try {
final result = await _methodChannel.invokeMethod<bool>('unregisterBackgroundScan'); final result =
await _methodChannel.invokeMethod<bool>('unregisterBackgroundScan');
return result ?? false; return result ?? false;
} catch (e) { } catch (e) {
print('注销后台扫描失败: $e'); print('注销后台扫描失败: $e');
@ -204,7 +222,8 @@ class BleService {
/// - openaiModel: OpenAI模型名称,默认为'gpt-3.5-turbo' /// - openaiModel: OpenAI模型名称,默认为'gpt-3.5-turbo'
Future<bool> initializeChatAgent(Map<String, dynamic> config) async { Future<bool> initializeChatAgent(Map<String, dynamic> config) async {
try { try {
final result = await _methodChannel.invokeMethod<bool>('initializeChatAgent', config); final result = await _methodChannel.invokeMethod<bool>(
'initializeChatAgent', config);
return result ?? false; return result ?? false;
} catch (e) { } catch (e) {
print('初始化语音交互代理失败: $e'); print('初始化语音交互代理失败: $e');
@ -240,9 +259,11 @@ class BleService {
/// ///
/// [text] 用户输入的文本 /// [text] 用户输入的文本
/// [speakResponse] 是否使用TTS朗读回复,默认为false /// [speakResponse] 是否使用TTS朗读回复,默认为false
Future<bool> processTextInput(String text, {bool speakResponse = false}) async { Future<bool> processTextInput(String text,
{bool speakResponse = false}) async {
try { try {
final result = await _methodChannel.invokeMethod<bool>('processTextInput', { final result =
await _methodChannel.invokeMethod<bool>('processTextInput', {
'text': text, 'text': text,
'speakResponse': speakResponse, 'speakResponse': speakResponse,
}); });

Loading…
Cancel
Save