Browse Source

feat(agent): 添加蓝牙耳机唤醒和通话模式支持

- 新增蓝牙服务订阅和唤醒处理逻辑
- 实现输入模式状态保存和恢复机制
- 增加耳机唤醒通话专用入口
- 添加语音输入状态验证和安全管理
newdev_shunjiawei
lxm 1 year ago
parent
commit
25207e3918
  1. 211
      lib/modules/agent/controllers/agent_controller.dart

211
lib/modules/agent/controllers/agent_controller.dart

@ -9,6 +9,7 @@ import 'package:deep_voice/data/services/location_manager.dart';
import 'package:deep_voice/data/services/music_manager.dart';
import 'package:deep_voice/data/services/navigation_manager.dart';
import 'package:deep_voice/data/services/network/api.dart';
import 'package:ble_service/ble_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart';
@ -22,6 +23,13 @@ import 'package:photo_view/photo_view.dart';
import '../../../core/utils/permission_util.dart';
import '../../../data/models/message_model.dart';
// 语音输入模式枚举
enum VoiceInputMode {
text, // 文字输入模式
pushToTalk, // 按住说话模式
phoneCall, // 打电话模式
}
class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式
@ -87,6 +95,9 @@ class AgentController extends GetxController {
// 按住说话模式状态 - 用于区分是否处于按住说话模式(即使当前没有按住)
final isPushToTalkMode = false.obs; // 是否处于按住说话模式
// 蓝牙唤醒状态
final isBleWakeupActive = false.obs; // 是否正在进行蓝牙唤醒对话
// 键盘状态
final isKeyboardVisible = false.obs; // 键盘是否可见
@ -96,6 +107,9 @@ class AgentController extends GetxController {
// 导航流事件订阅
StreamSubscription<AmapServiceEvent>? _amapeventSubscription;
// BLE事件订阅
StreamSubscription<dynamic>? _bleEventSubscription;
// 播放进度更新定时器
Timer? _progressTimer;
@ -127,6 +141,7 @@ class AgentController extends GetxController {
_loadTtsPreference(); //加载TTS偏好设置
_loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件
_subscribeToBleEvents(); //订阅BLE事件
_initMusicStatus(); //初始化音乐状态
_setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器
@ -167,6 +182,7 @@ class AgentController extends GetxController {
scrollController.dispose();
_eventSubscription?.cancel();
_amapeventSubscription?.cancel();
_bleEventSubscription?.cancel();
_progressTimer?.cancel();
super.onClose();
}
@ -346,12 +362,21 @@ class AgentController extends GetxController {
break;
case AgentServiceEventType.recognitionStopped:
isListening.value = false;
// 处理蓝牙唤醒结束
if (isBleWakeupActive.value) {
isBleWakeupActive.value = false;
// 蓝牙唤醒结束后,恢复之前的输入模式
_restorePreviousInputMode();
Logger.i(TAG, '蓝牙唤醒对话结束,恢复之前的输入模式');
} else {
// 只有在非按住说话模式且非打电话模式下才自动切换到文字模式
// 按住说话模式下应该保持在语音模式,等待下次按住
if (!isPushToTalkMode.value && !isPhoneCallMode.value) {
isTextInputMode.value = true;
}
// isTextInputMode.refresh();
}
Logger.i(TAG, '识别停止');
break;
@ -651,6 +676,66 @@ class AgentController extends GetxController {
});
}
// 订阅BLE事件
void _subscribeToBleEvents() {
try {
_bleEventSubscription = BleService().onStatusUpdated.listen((event) {
if (event is Map && event['type'] == 'wakeup') {
_handleBleWakeup();
}
});
Logger.i(TAG, 'BLE事件监听已设置');
} catch (e) {
Logger.e(TAG, 'BLE事件监听设置失败: $e');
}
}
// 处理BLE唤醒信号
void _handleBleWakeup() {
Logger.i(TAG, '🎧 收到BLE唤醒信号');
// 检查是否可以启动蓝牙唤醒
if (!_canStartBleWakeup()) {
Logger.w(TAG, '当前状态不允许BLE唤醒');
return;
}
// 保存当前输入模式
_saveCurrentInputMode();
// 设置蓝牙唤醒状态
isBleWakeupActive.value = true;
// 注意:这里不需要调用startVoiceInput(),因为原生层已经启动了语音识别
// 我们只需要同步Flutter层的状态
isTextInputMode.value = false;
Logger.i(TAG, 'BLE唤醒状态已设置,等待原生层语音识别结果');
}
// 检查是否可以启动BLE唤醒
bool _canStartBleWakeup() {
// 如果已经在BLE唤醒状态,不允许重复启动
if (isBleWakeupActive.value) {
Logger.w(TAG, '已在BLE唤醒状态,忽略重复唤醒');
return false;
}
// 如果正在打电话模式,不允许BLE唤醒
if (isPhoneCallMode.value) {
Logger.w(TAG, '正在打电话模式,忽略BLE唤醒');
return false;
}
// 如果正在处理TTS,暂时不允许启动
if (isSpeaking.value) {
Logger.w(TAG, '正在播放TTS,延迟BLE唤醒');
return false;
}
return true;
}
// 发送消息(包括所有支持的消息类型)
//场景1:用户在文本输入框中输入文本,点击发送按钮,调用sendMessage()方法。
//场景2:当用户选择图片后在文本框输入内容并发送时,调用sendMessage()转发给sendImageMessage()。
@ -1039,9 +1124,46 @@ class AgentController extends GetxController {
useImagePrompt('请翻译这张图片中的文字');
}
// 开始打电话模式
void startPhoneCall() async {
Logger.i(TAG, '🔥 开始打电话模式');
// 模式历史记录
VoiceInputMode? _previousInputMode;
// 保存当前输入模式
void _saveCurrentInputMode() {
if (isPushToTalkMode.value) {
_previousInputMode = VoiceInputMode.pushToTalk;
} else if (isTextInputMode.value) {
_previousInputMode = VoiceInputMode.text;
} else {
_previousInputMode = null;
}
}
// 恢复之前的输入模式
void _restorePreviousInputMode() {
switch (_previousInputMode) {
case VoiceInputMode.pushToTalk:
isTextInputMode.value = false;
isPushToTalkMode.value = true;
break;
case VoiceInputMode.text:
isTextInputMode.value = true;
isPushToTalkMode.value = false;
break;
default:
// 默认恢复到文字模式
isTextInputMode.value = true;
isPushToTalkMode.value = false;
break;
}
_previousInputMode = null; // 清除历史记录
}
// 开始打电话模式(支持耳机唤醒)
Future<void> startPhoneCall({bool isHeadsetTriggered = false}) async {
Logger.i(TAG, '🔥 开始打电话模式 (耳机触发: $isHeadsetTriggered)');
// 保存当前输入模式,以便通话结束后恢复
_saveCurrentInputMode();
// 收缩"+"选项
collapsePlusOptions();
@ -1062,8 +1184,9 @@ class AgentController extends GetxController {
);
if (!hasPermission) {
// 权限未获取,退出打电话模式
// 权限未获取,退出打电话模式并恢复之前状态
isPhoneCallMode.value = false;
_previousInputMode = null; // 清除历史记录
return;
}
@ -1085,9 +1208,85 @@ class AgentController extends GetxController {
// 停止语音输入
await stopVoiceInput();
// 切换回文本模式,并禁用按住说话模式
// 恢复之前的输入模式,而不是强制切换到文字模式
_restorePreviousInputMode();
}
// 耳机唤醒通话模式(专用入口)
Future<void> startHeadsetCall() async {
Logger.i(TAG, '🎧 耳机唤醒通话模式');
if (canStartHeadsetCall()) {
await startPhoneCall(isHeadsetTriggered: true);
}
}
// 检查当前是否可以启动耳机通话
bool canStartHeadsetCall() {
// 如果已经在打电话模式,不允许重复启动
if (isPhoneCallMode.value) {
Logger.w(TAG, '已在打电话模式,忽略耳机唤醒');
return false;
}
// 如果正在处理语音或TTS,暂时不允许启动
if (isListening.value || isSpeaking.value || isProcessing.value) {
Logger.w(TAG, '当前正在处理语音,延迟耳机唤醒');
return false;
}
return true;
}
// 验证语音输入状态一致性
void _validateVoiceInputStates() {
// 检查状态冲突
if (isPhoneCallMode.value && isPushToTalkMode.value) {
Logger.w(TAG, '状态冲突:同时启用打电话和按住说话模式,优先保持打电话模式');
isPushToTalkMode.value = false;
}
// 检查文字模式与语音模式的一致性
if (isTextInputMode.value && (isPushToTalkMode.value || isPhoneCallMode.value)) {
Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式');
isTextInputMode.value = false;
}
// 检查按住说话状态的合理性
if (isPushToTalkActive.value && !isPushToTalkMode.value) {
Logger.w(TAG, '状态冲突:按住说话激活但未启用按住说话模式');
isPushToTalkActive.value = false;
}
}
// 安全的状态切换方法
void _safeStateTransition(VoiceInputMode targetMode) {
Logger.i(TAG, '安全状态切换到: $targetMode');
switch (targetMode) {
case VoiceInputMode.text:
isTextInputMode.value = true;
isPushToTalkMode.value = false;
isPhoneCallMode.value = false;
isPushToTalkActive.value = false;
break;
case VoiceInputMode.pushToTalk:
isTextInputMode.value = false;
isPushToTalkMode.value = true;
isPhoneCallMode.value = false;
isPushToTalkActive.value = false;
break;
case VoiceInputMode.phoneCall:
isTextInputMode.value = false;
isPushToTalkMode.value = false;
isPhoneCallMode.value = true;
isPushToTalkActive.value = false;
break;
}
// 验证状态一致性
_validateVoiceInputStates();
}
// 开始按住说话

Loading…
Cancel
Save