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. 207
      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. 150
      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. 103
      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();

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 {
try {

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

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

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

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

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

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

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

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

207
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';
enum VoiceInteractionState {
idle, // 等待唤醒
active, // 活跃状态 - 可以同时识别和播放
idle, // 等待唤醒
active, // 活跃状态 - 可以同时识别和播放
}
class IosVoiceInteractionService extends GetxService {
@ -18,45 +18,47 @@ class IosVoiceInteractionService extends GetxService {
final TtsService _ttsService = Get.find<TtsService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final ChatHistoryService _chatHistoryService = Get.find<ChatHistoryService>();
// 当前状态
final Rx<VoiceInteractionState> state = VoiceInteractionState.idle.obs;
// 是否识别到用户语音
final RxBool isSpeechDetected = false.obs;
// 是否正在处理AI响应
final RxBool isProcessingAI = false.obs;
// 是否正在播放TTS
final RxBool isSpeaking = false.obs;
// 对话历史记录
final List<Map<String, String>> _messageHistory = [];
// 最大历史记录数
static const int _maxHistorySize = 10;
// 超时计时器
Timer? _inactivityTimer;
// 流订阅
StreamSubscription? _recognitionSubscription;
// 当前识别的文本
String _currentRecognizedText = '';
// 音频输入源
bool _audioSourceType = false;
// 事件流控制器
final StreamController<VoiceInteractionEvent> _eventStreamController =
final StreamController<VoiceInteractionEvent> _eventStreamController =
StreamController<VoiceInteractionEvent>.broadcast();
// 获取事件流
@override
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream;
Stream<VoiceInteractionEvent> get eventStream =>
_eventStreamController.stream;
// 标识符,用于存储聊天历史
final String _agentId = 'ios_assistant';
/// 初始化服务
@override
Future<IosVoiceInteractionService> initialize() async {
@ -66,10 +68,10 @@ class IosVoiceInteractionService extends GetxService {
} catch (e) {
Logger.error('iOS语音交互服务初始化失败: $e');
}
return this;
}
// 处理唤醒事件
void onWakeup() {
_ttsService.speakOnce('我在呢!');
@ -79,63 +81,61 @@ class IosVoiceInteractionService extends GetxService {
_activateInteraction();
}
}
// 激活交互
void _activateInteraction() {
// 设置状态为活跃
state.value = VoiceInteractionState.active;
// 开始连续监听
_startContinuousListening();
// 启动不活动计时器
_startInactivityTimer();
// 发送识别开始事件
_sendRecognitionStartedEvent();
}
// 开始连续监听
Future<void> _startContinuousListening() async {
// 停止之前的监听
_recognitionSubscription?.cancel();
try {
// 开始连续识别
final recognitionStream = await _asrService.startContinuousRecognition();
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
// 监听识别结果
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
onError: (error) {
Logger.error('语音识别错误: $error');
_startContinuousListening(); // 尝试重新启动
}
);
_recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent, onError: (error) {
Logger.error('语音识别错误: $error');
_startContinuousListening(); // 尝试重新启动
});
Logger.info('开始连续语音识别');
} catch (e) {
Logger.error('启动语音识别失败: $e');
}
}
// 处理识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
// 更新活动时间
_updateActivityTime();
switch (event.type) {
case RecognitionEventType.intermediateResult:
// 检测用户是否开始说话
if (event.text.isNotEmpty && !isSpeechDetected.value) {
isSpeechDetected.value = true;
// 用户开始讲话时,立即中断当前响应
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
}
break;
case RecognitionEventType.finalResult:
if (event.text.isNotEmpty) {
// 最终结果,处理用户输入
@ -144,16 +144,16 @@ class IosVoiceInteractionService extends GetxService {
// 重置语音检测状态
isSpeechDetected.value = false;
break;
case RecognitionEventType.sessionStarted:
Logger.info('语音识别会话开始');
break;
case RecognitionEventType.sessionStopped:
Logger.info('语音识别会话结束');
break;
case RecognitionEventType.error:
case RecognitionEventType.canceled:
Logger.error('语音识别错误: ${event.error}');
@ -161,11 +161,11 @@ class IosVoiceInteractionService extends GetxService {
break;
}
}
// 中断当前响应
void _interruptCurrentResponse(String reason) {
Logger.info(reason);
// 如果正在活跃状态,需要中断当前操作
if (state.value == VoiceInteractionState.active) {
// 停止TTS播放
@ -174,40 +174,40 @@ class IosVoiceInteractionService extends GetxService {
isProcessingAI.value = false;
}
}
// 处理用户输入
Future<void> _processUserInput(String text) async {
Logger.info('处理用户输入: $text');
// 设置为处理状态
isProcessingAI.value = true;
// 添加用户消息到历史记录
final userMessage = {'role': 'user', 'content': text};
_addToHistory(userMessage);
// 创建用户消息对象
final userMsg = Message(
role: 'user',
content: text,
timestamp: DateTime.now(),
);
// AI响应处理标志
bool isProcessingCancelled = false;
try {
// 构建消息历史
final messageHistory = _buildMessageHistory();
// 调用AI服务获取响应
final responseStream = _aiService.sendMessageStream(
messages: messageHistory,
systemPrompt: "你是一个智能助手,请简明扼要地回答问题。",
);
String aiResponse = '';
// 等待AI响应
await for (final chunk in responseStream) {
// 检查是否被用户打断
@ -217,49 +217,49 @@ class IosVoiceInteractionService extends GetxService {
Logger.info('AI响应生成过程中被用户打断');
break;
}
// 如果状态已改变(可能由其他原因导致),停止处理
if (state.value != VoiceInteractionState.active) {
isProcessingCancelled = true;
break;
}
aiResponse += chunk;
// 立即播放当前文本块,实现边生成边播放
_speakStreamResponse(chunk);
}
// 如果处理被取消,不继续后续操作
if (isProcessingCancelled) {
isProcessingAI.value = false;
return;
}
// 确保完整响应被处理
if (aiResponse.isNotEmpty) {
// 将AI响应添加到历史记录
final assistantMessage = {'role': 'assistant', 'content': aiResponse};
_addToHistory(assistantMessage);
// 创建助手消息对象
final assistantMsg = Message(
role: 'assistant',
content: aiResponse,
timestamp: DateTime.now(),
);
// 保存聊天历史
await _saveChatHistory(userMsg, assistantMsg);
// 发送聊天历史事件
_sendChatHistoryEvent(userMsg.content, assistantMsg.content);
// 刷新TTS流,确保所有文本都被播放
await _ttsService.flushStream();
isSpeaking.value = false;
}
// 处理完成
isProcessingAI.value = false;
} catch (e) {
@ -268,59 +268,59 @@ class IosVoiceInteractionService extends GetxService {
isProcessingAI.value = false;
}
}
// 构建用于AI服务的消息历史
List<Map<String, String>> _buildMessageHistory() {
return List<Map<String, String>>.from(_messageHistory);
}
// 添加消息到历史记录
void _addToHistory(Map<String, String> message) {
_messageHistory.add(message);
// 限制历史记录大小
while (_messageHistory.length > _maxHistorySize) {
_messageHistory.removeAt(0);
}
}
// 播放流式响应
void _speakStreamResponse(String chunk) {
if (chunk.isEmpty) return;
// 更新播放状态
isSpeaking.value = true;
// 使用流式TTS播放
_ttsService.speakStream(chunk);
// 更新活动时间
_updateActivityTime();
}
// 停止TTS
void _stopTts() {
_ttsService.stop();
isSpeaking.value = false;
}
// 更新最后活动时间
DateTime _lastActivityTime = DateTime.now();
void _updateActivityTime() {
_lastActivityTime = DateTime.now();
}
// 启动不活动计时器(长时间无交互会切换到空闲状态)
void _startInactivityTimer() {
_cancelInactivityTimer();
_inactivityTimer = Timer.periodic(Duration(seconds: 5), (timer) {
// 计算空闲时间
final idleTime = DateTime.now().difference(_lastActivityTime).inSeconds;
// 如果空闲超过30秒,且不在播放或检测到语音,切换到空闲
if (idleTime > 8 &&
!isSpeaking.value &&
if (idleTime > 8 &&
!isSpeaking.value &&
!isProcessingAI.value &&
!isSpeechDetected.value) {
_resetToIdle();
@ -328,37 +328,37 @@ class IosVoiceInteractionService extends GetxService {
}
});
}
// 取消不活动计时器
void _cancelInactivityTimer() {
_inactivityTimer?.cancel();
_inactivityTimer = null;
}
// 重置到空闲状态
void _resetToIdle() {
_ttsService.speakOnce('没有听到声音,暂停对话,双击耳机唤醒!');
_cancelInactivityTimer();
// 停止TTS
_stopTts();
// 停止语音识别
_recognitionSubscription?.cancel();
_asrService.stopContinuousRecognition();
// 重置状态
state.value = VoiceInteractionState.idle;
isSpeechDetected.value = false;
isProcessingAI.value = false;
isSpeaking.value = false;
// 清空历史记录
_messageHistory.clear();
Logger.info('语音交互已重置为空闲状态');
}
// 暂停语音交互
@override
void pauseVoiceInteraction() {
@ -366,53 +366,53 @@ class IosVoiceInteractionService extends GetxService {
_stopTts();
_recognitionSubscription?.cancel();
_asrService.stopContinuousRecognition();
// 重置所有状态
state.value = VoiceInteractionState.idle;
isSpeechDetected.value = false;
isProcessingAI.value = false;
isSpeaking.value = false;
// 清空历史记录
_messageHistory.clear();
}
}
// 恢复语音交互
void resumeVoiceInteraction() {
if (state.value == VoiceInteractionState.idle) {
_activateInteraction();
}
}
// 手动停止交互
void stopInteraction() {
_resetToIdle();
}
// 获取当前状态
VoiceInteractionState getCurrentState() {
return state.value;
}
// 是否处于活跃状态
@override
bool isActive() {
return state.value != VoiceInteractionState.idle;
}
// 获取当前对话历史
@override
List<Map<String, String>> getMessageHistory() {
return List<Map<String, String>>.from(_messageHistory);
}
// 获取当前服务ID
@override
String getServiceId() {
return _agentId;
}
// 释放资源
@override
void onClose() {
@ -422,25 +422,25 @@ class IosVoiceInteractionService extends GetxService {
_eventStreamController.close();
super.onClose();
}
// 保存聊天历史
Future<void> _saveChatHistory(Message userMsg, Message assistantMsg) async {
try {
// 加载现有历史记录
final existingMessages = await _chatHistoryService.loadHistory(_agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
await _chatHistoryService.saveHistory(_agentId, existingMessages);
Logger.info('保存聊天历史记录成功');
} catch (e) {
Logger.error('保存聊天历史记录失败: $e');
}
}
// 发送聊天历史事件
void _sendChatHistoryEvent(String userMessage, String assistantMessage) {
final event = ChatHistoryEvent(
@ -449,19 +449,18 @@ class IosVoiceInteractionService extends GetxService {
assistantMessage: assistantMessage,
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(event);
Logger.info('已发送聊天历史事件');
}
// 发送识别开始事件
void _sendRecognitionStartedEvent() {
final event = RecognitionStartedEvent(
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(event);
Logger.info('已发送识别开始事件');
}
}

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

@ -78,6 +78,9 @@ class ChatController extends GetxController {
// 取消标志
bool _shouldCancelAiResponse = false;
// 音频输入源
bool _audioSourceType = false;
// 自动停止语音识别计时器
Timer? _autoStopTimer;
@ -184,7 +187,6 @@ class ChatController extends GetxController {
// 自动停止计时器会在startVoiceRecognition中启动
}
_getLocation();
player.playerStateStream.listen((state) async {
@ -211,8 +213,6 @@ class ChatController extends GetxController {
_amapFlutter.startLocation();
}
@override
void onClose() {
Logger.info('ChatController onClose');
@ -295,7 +295,6 @@ class ChatController extends GetxController {
Future<void> sendMessage(String text) async {
if (text.trim().isEmpty) return;
// 中断当前响应
_interruptCurrentResponse("发送新消息,中断当前响应");
@ -558,12 +557,11 @@ class ChatController extends GetxController {
Timer(const Duration(milliseconds: 260), () async {
if (isDebounce) return;
// 中断当前响应
_interruptCurrentResponse("发送新消息,中断当前响应");
try {
final recognitionStream =
await _asrService.startContinuousRecognition();
await _asrService.startContinuousRecognition(_audioSourceType);
await Future.delayed(const Duration(milliseconds: 260));
isDebounce = true;
recognizedText.value = '';
@ -619,11 +617,11 @@ class ChatController extends GetxController {
Future<void> startVoiceRecognition() async {
if (isVoiceRecognitionActive.value) return;
try {
// 启动连续语音识别
// 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据
final recognitionStream = await _asrService.startContinuousRecognition();
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
isVoiceRecognitionActive.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 {
final recognitionStream = await _asrService.startContinuousRecognition();
final recognitionStream =
await _asrService.startContinuousRecognition(false);
_recognitionSubscription = recognitionStream.listen(
_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: '语音测试',
subtitle: '测试语音识别和合成功能,支持多种服务实现',
icon: Icons.record_voice_over,
iconBgColor: isDarkMode ? Colors.orange[900]!.withOpacity(0.3) : Colors.orange[100]!,
iconColor: isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!,
iconColor:
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
onTap: () => Get.toNamed(Routes.speechTest),
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(
height: 1,
color: isDarkMode
@ -1190,8 +1211,7 @@ class SettingsView extends GetView<SettingsController> {
Obx(() {
final serviceType = controller.speechServiceType.value;
final serviceName = controller.availableSpeechServices
.firstWhere(
(service) => service['type'] == serviceType,
.firstWhere((service) => service['type'] == serviceType,
orElse: () => {'name': 'unknown'})['name'];
return Container(
constraints: BoxConstraints(maxWidth: 120.w), // 限制最大宽度

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

@ -11,47 +11,48 @@ import '../../../core/utils/logger.dart';
class SpeechTestController extends GetxController {
// 语音服务工厂
final SpeechFactory _speechFactory = Get.find<SpeechFactory>();
// 当前使用的TTS服务
late TtsService _ttsService;
// 当前使用的ASR服务
late AsrService _asrService;
// 状态变量
final RxBool isInitialized = false.obs;
final RxBool isProcessing = false.obs;
final RxBool isListening = false.obs;
final RxBool isSpeaking = false.obs;
// 识别结果
final RxString intermediateResult = ''.obs; // 中间结果
final RxString finalResult = ''.obs; // 最终结果
final RxString finalResult = ''.obs; // 最终结果
final RxString recognitionStatus = '准备就绪'.obs;
final RxString detectedLanguage = ''.obs; // 检测到的语言
final RxString detectedLanguage = ''.obs; // 检测到的语言
// TTS事件
final RxString ttsStatus = '准备就绪'.obs;
// 当前选择的语音服务类型
final Rx<SpeechServiceType> selectedServiceType = SpeechServiceType.azure.obs;
// 音频输入源
bool _audioSourceType = false;
// 连续识别结果流
StreamSubscription<RecognitionEvent>? _recognitionSubscription;
// TTS事件流
StreamSubscription<TtsEvent>? _ttsEventSubscription;
// 文本控制器
final TextEditingController textController = TextEditingController();
// 连续播放的测试句子
final List<String> testSentences = [
'这是第一句测试语音。',
'这是第二句测试语音,语音合成支持连续播放。',
'这是第三句测试语音,将在连续模式下播放完毕。'
];
@override
void onInit() {
super.onInit();
@ -59,23 +60,23 @@ class SpeechTestController extends GetxController {
textController.text = '欢迎使用语音测试,这是一段默认的测试文本。您可以修改此文本,然后点击播放按钮测试语音合成效果。';
_initServices();
}
/// 初始化服务
Future<void> _initServices() async {
try {
isProcessing.value = true;
// 获取服务实例
_ttsService = _speechFactory.getTtsService();
_asrService = _speechFactory.getAsrService();
// 初始化服务
await _ttsService.initialize();
await _asrService.initialize();
// 监听TTS事件
_setupTtsEventListener();
isInitialized.value = true;
ttsStatus.value = '准备就绪';
recognitionStatus.value = '准备就绪';
@ -87,12 +88,12 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 设置TTS事件监听
void _setupTtsEventListener() {
// 取消旧的订阅
_ttsEventSubscription?.cancel();
// 设置新的订阅
_ttsEventSubscription = _ttsService.onEvent.listen((event) {
switch (event.type) {
@ -115,18 +116,18 @@ class SpeechTestController extends GetxController {
}
});
}
/// 设置ASR事件监听
void _setupAsrEventListener(Stream<RecognitionEvent> stream) {
// 取消旧的订阅
_recognitionSubscription?.cancel();
// 重置识别结果
intermediateResult.value = '';
finalResult.value = '';
detectedLanguage.value = '';
recognitionStatus.value = '正在等待语音输入...';
// 设置新的订阅
_recognitionSubscription = stream.listen((event) {
switch (event.type) {
@ -134,14 +135,14 @@ class SpeechTestController extends GetxController {
isListening.value = true;
recognitionStatus.value = '开始聆听...';
break;
case RecognitionEventType.sessionStopped:
isListening.value = false;
recognitionStatus.value = '聆听已结束';
// 清空中间结果,保留最终结果
intermediateResult.value = '';
break;
case RecognitionEventType.intermediateResult:
// 更新中间结果,显示为斜体或不同颜色
intermediateResult.value = event.text;
@ -150,7 +151,7 @@ class SpeechTestController extends GetxController {
}
recognitionStatus.value = '正在识别...';
break;
case RecognitionEventType.finalResult:
// 更新最终结果,显示为正常文本
finalResult.value = event.text;
@ -160,12 +161,12 @@ class SpeechTestController extends GetxController {
}
recognitionStatus.value = '识别完成';
break;
case RecognitionEventType.error:
isListening.value = false;
recognitionStatus.value = '识别错误: ${event.error}';
break;
case RecognitionEventType.canceled:
isListening.value = false;
recognitionStatus.value = '识别已取消';
@ -179,21 +180,21 @@ class SpeechTestController extends GetxController {
intermediateResult.value = '';
});
}
/// 切换语音服务类型
void switchServiceType(SpeechServiceType type) async {
if (selectedServiceType.value == type) return;
try {
isProcessing.value = true;
// 停止当前的所有服务状态
await stopAll();
// 切换服务类型
_speechFactory.setServiceType(type);
selectedServiceType.value = type;
// 重新初始化服务
await _initServices();
} catch (e) {
@ -202,40 +203,40 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 停止所有活动
Future<void> stopAll() async {
// 停止ASR
if (isListening.value) {
await stopContinuousRecognition();
}
// 停止TTS
if (isSpeaking.value) {
await stopSpeaking();
}
// 取消所有订阅
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
}
/// 单次播放文本
Future<void> speakOnce() async {
if (!isInitialized.value || isProcessing.value) return;
final text = textController.text.trim();
if (text.isEmpty) {
ttsStatus.value = '请输入要播放的文本';
return;
}
try {
isProcessing.value = true;
ttsStatus.value = '准备播放...';
final result = await _ttsService.speakOnce(text);
if (!result) {
ttsStatus.value = '播放失败';
}
@ -246,15 +247,15 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 连续播放测试
Future<void> speakContinuously() async {
if (!isInitialized.value || isProcessing.value) return;
try {
isProcessing.value = true;
ttsStatus.value = '准备连续播放...';
// 为避免阻塞,使用一个句子接一个句子的方式添加到队列
for (final sentence in testSentences) {
if (!await _ttsService.speakStream(sentence)) {
@ -264,10 +265,9 @@ class SpeechTestController extends GetxController {
// 给前一个句子一点处理时间
await Future.delayed(const Duration(milliseconds: 50));
}
// 刷新队列,确保播放完成
await _ttsService.flushStream();
} catch (e) {
ttsStatus.value = '连续播放异常: $e';
Logger.error('连续播放异常: $e');
@ -275,11 +275,11 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 停止播放
Future<void> stopSpeaking() async {
if (!isInitialized.value) return;
try {
await _ttsService.stop();
} catch (e) {
@ -287,39 +287,39 @@ class SpeechTestController extends GetxController {
Logger.error('停止播放异常: $e');
}
}
/// 一次性识别
Future<void> recognizeOnce() async {
if (!isInitialized.value || isProcessing.value || isListening.value) return;
try {
isProcessing.value = true;
recognitionStatus.value = '开始识别...';
// 清空之前的结果
intermediateResult.value = '';
finalResult.value = '';
detectedLanguage.value = '';
// 显示正在进行的状态
recognitionStatus.value = '正在聆听...';
// 执行一次性识别 - 这是同步等待结果的操作
final result = await _asrService.recognizeOnce();
if (result.isError) {
recognitionStatus.value = '识别错误: ${result.error}';
Logger.error('一次性识别错误: ${result.error}');
} else {
// 更新最终结果
finalResult.value = result.text;
// 更新语言信息
if (result.detectedLanguage.isNotEmpty) {
detectedLanguage.value = result.detectedLanguage;
Logger.debug('检测到语言: ${result.detectedLanguage}');
}
recognitionStatus.value = '识别完成';
}
} catch (e) {
@ -329,26 +329,26 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 开始连续识别
Future<void> startContinuousRecognition() async {
if (!isInitialized.value || isProcessing.value || isListening.value) return;
try {
isProcessing.value = true;
// 清空之前的结果
intermediateResult.value = '';
finalResult.value = '';
detectedLanguage.value = '';
recognitionStatus.value = '准备连续识别...';
// 获取识别事件流 - 这启动了异步识别过程
final stream = await _asrService.startContinuousRecognition();
final stream =
await _asrService.startContinuousRecognition(_audioSourceType);
// 设置事件监听 - 异步处理识别结果
_setupAsrEventListener(stream);
} catch (e) {
recognitionStatus.value = '启动连续识别异常: $e';
Logger.error('启动连续识别异常: $e');
@ -359,19 +359,19 @@ class SpeechTestController extends GetxController {
isProcessing.value = false;
}
}
/// 停止连续识别
Future<void> stopContinuousRecognition() async {
if (!isInitialized.value || !isListening.value) return;
try {
// 首先更新状态,防止重复调用
isListening.value = false;
recognitionStatus.value = '正在停止识别...';
// 停止识别过程
await _asrService.stopContinuousRecognition();
// 更新UI状态
recognitionStatus.value = '连续识别已停止';
intermediateResult.value = '';
@ -380,16 +380,16 @@ class SpeechTestController extends GetxController {
Logger.error('停止连续识别异常: $e');
}
}
// 获取识别结果(用于UI显示)
String getDisplayResult() {
// 如果有最终结果,优先显示
String displayText = '';
if (finalResult.value.isNotEmpty) {
displayText = finalResult.value;
}
// 如果有中间结果,添加到最终结果后(可在UI中使用不同样式显示)
if (intermediateResult.value.isNotEmpty) {
if (displayText.isNotEmpty) {
@ -398,19 +398,19 @@ class SpeechTestController extends GetxController {
displayText = '正在识别: ${intermediateResult.value}';
}
}
return displayText;
}
@override
void onClose() {
// 取消所有订阅
_recognitionSubscription?.cancel();
_ttsEventSubscription?.cancel();
// 释放控制器
textController.dispose();
super.onClose();
}
}
}

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

@ -9,28 +9,9 @@ import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart';
import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/asr_service.dart';
import 'package:jieli/jieli.dart';
import '../../../data/services/ble_manager.dart';
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 VolcanoTranslationService _translationService =
@ -38,7 +19,10 @@ class TranslationController extends GetxController {
final TtsService _ttsService = Get.find<TtsService>();
final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage();
// 蓝牙服务
final _bleManager = Get.find<BleManager>();
// 音频输入源
bool _audioSourceType = false;
// 存储相关
static const String _historyKey = 'translation_history';
static const String _sourceLanguageKey = 'translation_source_language';
@ -104,7 +88,7 @@ class TranslationController extends GetxController {
@override
void onInit() {
super.onInit();
jieli = Jieli();
//_setupEventListeners();
// 加载上次保存的语言设置
_loadSavedLanguages();
@ -240,19 +224,23 @@ class TranslationController extends GetxController {
// // 添加到历史记录
// translationHistory.add(newItem);
// 开始连续语音识别
if (currentMode.value == "audioVideo") {
_bleManager.openDecoder();
// 发送ble音乐或者通话远端声音
_audioSourceType = true;
Logger.info('发送ble音乐或者通话远端声音');
} else if (currentMode.value == "call") {
// 发送ble系统mic和dac(音乐或者通话远端)声音
_bleManager.openA2DPDecoder();
_audioSourceType = true;
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
} else {
// 开始连续语音识别
_audioSourceType = false;
}
// 开始连续语音识别
final recognitionStream = await _asrService.startContinuousRecognition();
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
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/agent/views/agent_view.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 {
static final pages = [
@ -79,6 +81,16 @@ abstract class AppPages {
page: () => const ChatView(),
binding: ChatBinding(),
),
GetPage(
name: Routes.recording,
page: () => const ChatView(),
binding: ChatBinding(),
),
GetPage(
name: Routes.OTA_UPGRADE,
page: () => const OtaUpgradeView(),
binding: OtaUpgradeBinding(),
),
GetPage(
name: Routes.profile,
page: () => const ProfileView(),
@ -144,13 +156,11 @@ abstract class AppPages {
page: () => const OpusTestView(),
binding: OpusTestBinding(),
),
GetPage(
name: Routes.speechTest,
page: () => const SpeechTestView(),
binding: SpeechTestBinding(),
),
GetPage(
name: Routes.agent,
page: () => const AgentView(),

2
lib/routes/app_routes.dart

@ -6,6 +6,8 @@ abstract class Routes {
static const home = '/home';
static const explore = '/explore';
static const chat = '/chat';
static const OTA_UPGRADE = '/ota_upgrade';
static const recording = '/recording';
static const profile = '/profile';
static const meeting = '/meeting';
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, "需要录音权限,请在设置中授予权限")
sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
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" -> {
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
val useExternalAudio = call.argument<Boolean>("audioSourceType") ?: false
// 确保事件通道已准备好
if (asrEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
@ -187,6 +187,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope {
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
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 {
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_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" -> {
val success = BleService.openDecoder()
result.success(success)
}
"openA2DPDecoder" -> {
val success = BleService.openA2DPDecoder()
result.success(success)
}
"closeCodec" -> {
val success = BleService.closeCodec()

103
local_plugins/ble_service/lib/ble_service.dart

@ -15,32 +15,35 @@ class BleConnectionState {
/// BLE服务插件
class BleService {
static final BleService _instance = BleService._internal();
/// 获取单例
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 _statusEventChannel;
// 事件流
late final Stream<dynamic> _dataStream;
late final Stream<dynamic> _statusStream;
// 获取事件流
Stream<dynamic> get onDataReceived => _dataStream;
// 状态流直接传递原始数据
Stream<dynamic> get onStatusUpdated => _statusStream;
// 内部构造函数
BleService._internal() {
_dataEventChannel = const EventChannel('com.yunqiinnovation.ble_service/data_events');
_statusEventChannel = const EventChannel('com.yunqiinnovation.ble_service/status_events');
_dataEventChannel =
const EventChannel('com.yunqiinnovation.ble_service/data_events');
_statusEventChannel =
const EventChannel('com.yunqiinnovation.ble_service/status_events');
// 初始化事件流
_dataStream = _dataEventChannel.receiveBroadcastStream();
_statusStream = _statusEventChannel.receiveBroadcastStream();
@ -56,7 +59,7 @@ class BleService {
return false;
}
}
/// 获取电池电量
Future<int> getBatteryLevel() async {
try {
@ -67,18 +70,19 @@ class BleService {
return 0;
}
}
/// 获取设备版本信息
Future<bool> getDeviceVersion() async {
try {
final result = await _methodChannel.invokeMethod<bool>('getDeviceVersion');
final result =
await _methodChannel.invokeMethod<bool>('getDeviceVersion');
return result ?? false;
} catch (e) {
print('获取设备版本失败: $e');
return false;
}
}
/// 获取电池信息
Future<bool> getBatteryInfo() async {
try {
@ -89,7 +93,7 @@ class BleService {
return false;
}
}
/// 打开编码器
Future<bool> openEncoder() async {
try {
@ -100,7 +104,7 @@ class BleService {
return false;
}
}
/// 打开解码器
Future<bool> openDecoder() async {
try {
@ -111,7 +115,18 @@ class BleService {
return false;
}
}
/// 打开解码器
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 {
try {
@ -122,7 +137,7 @@ class BleService {
return false;
}
}
/// 播放A2DP
Future<bool> playA2DP() async {
try {
@ -133,7 +148,7 @@ class BleService {
return false;
}
}
/// 启动BLE后台服务
/// [config] 配置参数,可包含:
/// - scan_interval: 扫描间隔时间(毫秒)
@ -144,14 +159,15 @@ class BleService {
/// - device_name: 设备名称
Future<bool> startBleService(Map<String, dynamic> config) async {
try {
final result = await _methodChannel.invokeMethod<bool>('startBleService', config);
final result =
await _methodChannel.invokeMethod<bool>('startBleService', config);
return result ?? false;
} catch (e) {
print('启动BLE后台服务失败: $e');
return false;
}
}
/// 停止BLE后台服务
Future<bool> stopBleService() async {
try {
@ -162,40 +178,42 @@ class BleService {
return false;
}
}
/// 注册BLE后台扫描
///
///
/// 注册后,即使应用被杀死,系统也会在检测到设备广播时唤醒应用
/// 这是确保设备可以在应用后台时被自动连接的关键功能
Future<bool> registerBackgroundScan() async {
try {
final result = await _methodChannel.invokeMethod<bool>('registerBackgroundScan');
final result =
await _methodChannel.invokeMethod<bool>('registerBackgroundScan');
return result ?? false;
} catch (e) {
print('注册后台扫描失败: $e');
return false;
}
}
/// 注销BLE后台扫描
///
///
/// 当不再需要后台扫描功能时调用,例如用户主动断开连接时
Future<bool> unregisterBackgroundScan() async {
try {
final result = await _methodChannel.invokeMethod<bool>('unregisterBackgroundScan');
final result =
await _methodChannel.invokeMethod<bool>('unregisterBackgroundScan');
return result ?? false;
} catch (e) {
print('注销后台扫描失败: $e');
return false;
}
}
//========================================================================
// 以下是ChatAgent相关功能
//========================================================================
/// 初始化语音交互代理
///
///
/// [config] 配置参数,必须包含:
/// - azureSpeechKey: Azure语音服务API密钥
/// - azureSpeechRegion: Azure语音服务区域
@ -204,16 +222,17 @@ class BleService {
/// - openaiModel: OpenAI模型名称,默认为'gpt-3.5-turbo'
Future<bool> initializeChatAgent(Map<String, dynamic> config) async {
try {
final result = await _methodChannel.invokeMethod<bool>('initializeChatAgent', config);
final result = await _methodChannel.invokeMethod<bool>(
'initializeChatAgent', config);
return result ?? false;
} catch (e) {
print('初始化语音交互代理失败: $e');
return false;
}
}
/// 启动语音识别
///
///
/// 用于手动启动语音识别,通常不需要直接调用,因为在唤醒时会自动启动
Future<bool> startListening() async {
try {
@ -224,7 +243,7 @@ class BleService {
return false;
}
}
/// 停止语音识别
Future<bool> stopListening() async {
try {
@ -235,14 +254,16 @@ class BleService {
return false;
}
}
/// 处理文本输入
///
///
/// [text] 用户输入的文本
/// [speakResponse] 是否使用TTS朗读回复,默认为false
Future<bool> processTextInput(String text, {bool speakResponse = false}) async {
Future<bool> processTextInput(String text,
{bool speakResponse = false}) async {
try {
final result = await _methodChannel.invokeMethod<bool>('processTextInput', {
final result =
await _methodChannel.invokeMethod<bool>('processTextInput', {
'text': text,
'speakResponse': speakResponse,
});
@ -252,7 +273,7 @@ class BleService {
return false;
}
}
/// 停止TTS播放
Future<bool> stopTts() async {
try {
@ -263,4 +284,4 @@ class BleService {
return false;
}
}
}
}

Loading…
Cancel
Save