21 changed files with 1454 additions and 2518 deletions
@ -1,310 +0,0 @@ |
|||
import 'dart:async'; |
|||
import 'dart:io' show Platform; |
|||
import 'package:flutter/services.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../../core/utils/logger.dart'; |
|||
import 'voice_interaction_service.dart'; |
|||
/// 蓝牙媒体按钮事件类型 |
|||
enum MediaButtonType { |
|||
/// 播放/暂停按钮 |
|||
playPause, |
|||
|
|||
/// 未知按钮 |
|||
unknown |
|||
} |
|||
|
|||
/// 蓝牙媒体按钮事件 |
|||
class MediaButtonEvent { |
|||
/// 按钮类型 |
|||
final MediaButtonType buttonType; |
|||
|
|||
/// 事件发生时间戳 |
|||
final int timestamp; |
|||
|
|||
MediaButtonEvent({ |
|||
required this.buttonType, |
|||
required this.timestamp, |
|||
}); |
|||
|
|||
factory MediaButtonEvent.fromMap(Map<dynamic, dynamic> map) { |
|||
MediaButtonType buttonType; |
|||
|
|||
switch (map['buttonType']) { |
|||
case 'playPause': |
|||
buttonType = MediaButtonType.playPause; |
|||
break; |
|||
default: |
|||
// 将所有其他事件类型映射为 playPause |
|||
if (map['buttonType'] == 'play' || |
|||
map['buttonType'] == 'pause' || |
|||
map['buttonType'] == 'togglePlayPause') { |
|||
buttonType = MediaButtonType.playPause; |
|||
} else { |
|||
// 忽略其他类型(next, previous, seekForward, seekBackward) |
|||
buttonType = MediaButtonType.unknown; |
|||
} |
|||
} |
|||
|
|||
return MediaButtonEvent( |
|||
buttonType: buttonType, |
|||
timestamp: map['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch, |
|||
); |
|||
} |
|||
|
|||
@override |
|||
String toString() { |
|||
switch (buttonType) { |
|||
case MediaButtonType.playPause: |
|||
return '蓝牙媒体按钮: 播放/暂停'; |
|||
case MediaButtonType.unknown: |
|||
return '蓝牙媒体按钮: 未知'; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 蓝牙媒体按钮服务 |
|||
class BluetoothMediaButtonService extends GetxService { |
|||
static BluetoothMediaButtonService get to => Get.find<BluetoothMediaButtonService>(); |
|||
|
|||
// 方法通道名称 |
|||
final MethodChannel _channel = const MethodChannel('com.deep_voice.bluetooth_media_button'); |
|||
|
|||
// 事件通道名称 |
|||
final EventChannel _eventChannel = const EventChannel('com.deep_voice.bluetooth_media_button_events'); |
|||
|
|||
/// 按钮事件流控制器 |
|||
final _buttonEventController = StreamController<MediaButtonEvent>.broadcast(); |
|||
|
|||
/// 按钮事件流 |
|||
Stream<MediaButtonEvent> get buttonEvents => _buttonEventController.stream; |
|||
|
|||
/// 是否已启用监听 |
|||
final _isListening = false.obs; |
|||
bool get isListening => _isListening.value; |
|||
|
|||
/// 是否正在加载 |
|||
final isLoading = false.obs; |
|||
|
|||
/// 错误信息 |
|||
final errorMessage = ''.obs; |
|||
|
|||
/// 按钮事件流订阅 |
|||
StreamSubscription? _buttonEventSubscription; |
|||
|
|||
/// 服务是否已初始化 |
|||
bool _isInitialized = false; |
|||
|
|||
/// 是否支持媒体按钮服务(iOS 13.0及以上) |
|||
bool get isSupported { |
|||
if (!Platform.isIOS) return true; // 非iOS平台默认支持 |
|||
|
|||
// 获取平台版本信息 |
|||
final String version = Platform.operatingSystemVersion; |
|||
|
|||
// 尝试提取iOS版本号 (格式如: "Version 14.5 (Build 18E182)") |
|||
try { |
|||
// 提取版本号 |
|||
final RegExp versionRegex = RegExp(r'Version\s+(\d+)\.(\d+)'); |
|||
final match = versionRegex.firstMatch(version); |
|||
|
|||
if (match != null) { |
|||
final int majorVersion = int.parse(match.group(1) ?? '0'); |
|||
return majorVersion >= 13; // iOS 13及以上支持 |
|||
} |
|||
} catch (e) { |
|||
Logger.error('解析iOS版本失败: $e'); |
|||
} |
|||
|
|||
// 无法确定版本时,假设不支持 |
|||
return false; |
|||
} |
|||
|
|||
/// 初始化服务 |
|||
BluetoothMediaButtonService() { |
|||
Logger.info('创建 BluetoothMediaButtonService 实例'); |
|||
} |
|||
|
|||
/// 初始化媒体按钮服务 |
|||
Future<bool> initialize() async { |
|||
if (_isInitialized) { |
|||
Logger.info('BluetoothMediaButtonService 已经初始化'); |
|||
return true; |
|||
} |
|||
|
|||
// 检查系统版本是否支持 |
|||
if (!isSupported) { |
|||
Logger.info('当前iOS版本不支持蓝牙媒体按钮功能(需要iOS 13.0+)'); |
|||
return false; |
|||
} |
|||
|
|||
try { |
|||
Logger.info('正在初始化 BluetoothMediaButtonService...'); |
|||
|
|||
// 设置事件监听器 |
|||
_setupButtonEventListener(); |
|||
|
|||
_isInitialized = true; |
|||
Logger.info('BluetoothMediaButtonService 初始化成功'); |
|||
return true; |
|||
} catch (e) { |
|||
Logger.error('初始化 BluetoothMediaButtonService 失败: $e'); |
|||
errorMessage.value = '初始化蓝牙媒体按钮服务失败'; |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 设置按钮事件监听器 |
|||
void _setupButtonEventListener() { |
|||
_buttonEventSubscription?.cancel(); |
|||
|
|||
// 设置事件通道监听器 |
|||
_buttonEventSubscription = _eventChannel |
|||
.receiveBroadcastStream() |
|||
.listen(_handleButtonEvent, onError: _handleEventStreamError); |
|||
|
|||
Logger.info('已设置蓝牙媒体按钮事件监听器'); |
|||
} |
|||
|
|||
/// 处理按钮事件 |
|||
void _handleButtonEvent(dynamic event) { |
|||
Logger.info('BluetoothMediaButtonService 收到原始事件: $event'); |
|||
|
|||
if (event is! Map) { |
|||
Logger.error('收到无效的蓝牙媒体按钮事件 (非Map类型): $event'); |
|||
return; |
|||
} |
|||
|
|||
final Map<dynamic, dynamic> eventMap = event; |
|||
|
|||
try { |
|||
if (eventMap['type'] == 'mediaButtonEvent') { |
|||
// 获取按钮类型 |
|||
final String buttonType = eventMap['buttonType'] as String? ?? 'unknown'; |
|||
|
|||
// 判断是否为playPause相关事件 |
|||
if (buttonType == 'playPause' || |
|||
buttonType == 'play' || |
|||
buttonType == 'pause' || |
|||
buttonType == 'togglePlayPause') { |
|||
|
|||
// 创建playPause事件并广播 |
|||
final buttonEvent = MediaButtonEvent( |
|||
buttonType: MediaButtonType.playPause, |
|||
timestamp: eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch, |
|||
); |
|||
|
|||
|
|||
|
|||
// 广播事件 |
|||
_buttonEventController.add(buttonEvent); |
|||
|
|||
// 广播事件 |
|||
_buttonEventController.add(buttonEvent); |
|||
Logger.info('发送playPause按钮事件: $buttonEvent'); |
|||
} else { |
|||
// 忽略其他类型的按钮事件 |
|||
Logger.info('忽略非playPause按钮事件: $buttonType'); |
|||
} |
|||
} else { |
|||
Logger.warning('收到未知类型的事件: ${eventMap['type']}'); |
|||
} |
|||
} catch (e) { |
|||
Logger.error('处理蓝牙媒体按钮事件时出错: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 处理事件流错误 |
|||
void _handleEventStreamError(dynamic error) { |
|||
Logger.error('蓝牙媒体按钮事件流错误: $error'); |
|||
errorMessage.value = '蓝牙媒体按钮监听出错'; |
|||
} |
|||
|
|||
/// 开始监听蓝牙媒体按钮事件 |
|||
Future<bool> startButtonListening() async { |
|||
// 检查系统版本是否支持 |
|||
if (!isSupported) { |
|||
Logger.info('当前iOS版本不支持蓝牙媒体按钮功能(需要iOS 13.0+)'); |
|||
return false; |
|||
} |
|||
|
|||
if (!_isInitialized && !(await initialize())) { |
|||
return false; |
|||
} |
|||
|
|||
if (_isListening.value) { |
|||
Logger.info('已经在监听蓝牙媒体按钮事件'); |
|||
return true; |
|||
} |
|||
|
|||
isLoading.value = true; |
|||
|
|||
try { |
|||
final result = await _channel.invokeMethod<bool>('startButtonListening') ?? false; |
|||
|
|||
if (result) { |
|||
_isListening.value = true; |
|||
Logger.info('开始监听蓝牙媒体按钮事件成功'); |
|||
} else { |
|||
errorMessage.value = '开始监听蓝牙媒体按钮事件失败'; |
|||
Logger.error('开始监听蓝牙媒体按钮事件失败'); |
|||
} |
|||
|
|||
isLoading.value = false; |
|||
return result; |
|||
} catch (e) { |
|||
isLoading.value = false; |
|||
errorMessage.value = '开始监听蓝牙媒体按钮事件出错: $e'; |
|||
Logger.error('开始监听蓝牙媒体按钮事件出错: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 停止监听蓝牙媒体按钮事件 |
|||
Future<bool> stopButtonListening() async { |
|||
if (!_isInitialized) { |
|||
return false; |
|||
} |
|||
|
|||
if (!_isListening.value) { |
|||
return true; |
|||
} |
|||
|
|||
isLoading.value = true; |
|||
|
|||
try { |
|||
final result = await _channel.invokeMethod<bool>('stopButtonListening') ?? false; |
|||
|
|||
if (result) { |
|||
_isListening.value = false; |
|||
Logger.info('停止监听蓝牙媒体按钮事件成功'); |
|||
} else { |
|||
errorMessage.value = '停止监听蓝牙媒体按钮事件失败'; |
|||
Logger.error('停止监听蓝牙媒体按钮事件失败'); |
|||
} |
|||
|
|||
isLoading.value = false; |
|||
return result; |
|||
} catch (e) { |
|||
isLoading.value = false; |
|||
errorMessage.value = '停止监听蓝牙媒体按钮事件出错: $e'; |
|||
Logger.error('停止监听蓝牙媒体按钮事件出错: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 释放资源 |
|||
void dispose() { |
|||
_buttonEventSubscription?.cancel(); |
|||
_buttonEventSubscription = null; |
|||
_buttonEventController.close(); |
|||
_isInitialized = false; |
|||
_isListening.value = false; |
|||
Logger.info('BluetoothMediaButtonService 已释放'); |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
dispose(); |
|||
super.onClose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,249 @@ |
|||
import 'dart:async'; |
|||
import 'dart:math' as math; |
|||
import 'package:get/get.dart'; |
|||
import 'package:classic_bluetooth/classic_bluetooth.dart'; |
|||
import 'package:deep_voice/core/utils/logger.dart'; |
|||
import 'package:flutter/services.dart'; |
|||
|
|||
/// 蓝牙设备信息缓存模型 |
|||
class DeviceInfo { |
|||
final String address; |
|||
final String name; |
|||
|
|||
// 整体电池电量 |
|||
final int batteryLevel; |
|||
|
|||
// 详细电池信息 |
|||
final int leftEarBattery; |
|||
final int rightEarBattery; |
|||
final int caseBattery; |
|||
|
|||
// 充电状态 |
|||
final bool leftEarCharging; |
|||
final bool rightEarCharging; |
|||
final bool caseCharging; |
|||
|
|||
final String firmwareVersion; |
|||
final String productId; |
|||
final String colorId; |
|||
final DateTime lastUpdated; |
|||
|
|||
DeviceInfo({ |
|||
required this.address, |
|||
required this.name, |
|||
this.batteryLevel = 0, |
|||
this.leftEarBattery = 0, |
|||
this.rightEarBattery = 0, |
|||
this.caseBattery = 0, |
|||
this.leftEarCharging = false, |
|||
this.rightEarCharging = false, |
|||
this.caseCharging = false, |
|||
this.firmwareVersion = '', |
|||
this.productId = '', |
|||
this.colorId = '', |
|||
DateTime? lastUpdated, |
|||
}) : lastUpdated = lastUpdated ?? DateTime.now(); |
|||
|
|||
/// 创建更新后的设备信息副本 |
|||
DeviceInfo copyWith({ |
|||
String? address, |
|||
String? name, |
|||
int? batteryLevel, |
|||
int? leftEarBattery, |
|||
int? rightEarBattery, |
|||
int? caseBattery, |
|||
bool? leftEarCharging, |
|||
bool? rightEarCharging, |
|||
bool? caseCharging, |
|||
String? firmwareVersion, |
|||
String? productId, |
|||
String? colorId, |
|||
}) { |
|||
return DeviceInfo( |
|||
address: address ?? this.address, |
|||
name: name ?? this.name, |
|||
batteryLevel: batteryLevel ?? this.batteryLevel, |
|||
leftEarBattery: leftEarBattery ?? this.leftEarBattery, |
|||
rightEarBattery: rightEarBattery ?? this.rightEarBattery, |
|||
caseBattery: caseBattery ?? this.caseBattery, |
|||
leftEarCharging: leftEarCharging ?? this.leftEarCharging, |
|||
rightEarCharging: rightEarCharging ?? this.rightEarCharging, |
|||
caseCharging: caseCharging ?? this.caseCharging, |
|||
firmwareVersion: firmwareVersion ?? this.firmwareVersion, |
|||
productId: productId ?? this.productId, |
|||
colorId: colorId ?? this.colorId, |
|||
lastUpdated: DateTime.now(), |
|||
); |
|||
} |
|||
|
|||
@override |
|||
String toString() { |
|||
return 'DeviceInfo(address: $address, name: $name, ' |
|||
'battery: $batteryLevel%, L: $leftEarBattery%, R: $rightEarBattery%, Case: $caseBattery%, ' |
|||
'charging: L:$leftEarCharging, R:$rightEarCharging, C:$caseCharging, ' |
|||
'version: $firmwareVersion, productId: $productId, colorId: $colorId)'; |
|||
} |
|||
} |
|||
|
|||
/// 蓝牙服务 - 管理经典蓝牙服务,提供设备信息缓存 |
|||
class BluetoothService extends GetxService { |
|||
static BluetoothService get to => Get.find<BluetoothService>(); |
|||
|
|||
// 蓝牙状态 |
|||
final _bluetoothState = 'unknown'.obs; |
|||
String get bluetoothState => _bluetoothState.value; |
|||
|
|||
// 当前经典蓝牙设备 |
|||
final Rx<BluetoothDevice?> currentDeviceRx = Rx<BluetoothDevice?>(null); |
|||
BluetoothDevice? get currentDevice => currentDeviceRx.value; |
|||
|
|||
// 设备信息缓存 |
|||
final Rx<DeviceInfo?> _deviceInfo = Rx<DeviceInfo?>(null); |
|||
DeviceInfo? get deviceInfo => _deviceInfo.value; |
|||
|
|||
// 表明当前连接类型,本服务只处理经典蓝牙 |
|||
bool get isBleConnection => false; |
|||
|
|||
// 订阅 |
|||
StreamSubscription<BluetoothEvent>? _bluetoothSubscription; |
|||
|
|||
/// 初始化 |
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
Logger.info('蓝牙服务初始化'); |
|||
|
|||
// 设置蓝牙事件监听 |
|||
_setupBluetoothListener(); |
|||
} |
|||
|
|||
/// 释放资源 |
|||
@override |
|||
void onClose() { |
|||
_bluetoothSubscription?.cancel(); |
|||
super.onClose(); |
|||
} |
|||
|
|||
/// 设置经典蓝牙事件监听 |
|||
void _setupBluetoothListener() { |
|||
_bluetoothSubscription?.cancel(); |
|||
|
|||
try { |
|||
_bluetoothSubscription = ClassicBluetooth.events.listen( |
|||
(event) { |
|||
if (event.type == 'stateChanged' && event.state != null) { |
|||
_bluetoothState.value = event.state!; |
|||
Logger.info('蓝牙状态变更: ${event.state}'); |
|||
|
|||
// 如果蓝牙关闭,清除设备 |
|||
if (event.state == 'off') { |
|||
currentDeviceRx.value = null; |
|||
_deviceInfo.value = null; |
|||
} |
|||
} else if (event.type == 'deviceConnected' && event.device != null) { |
|||
// 直接从事件中获取设备信息 |
|||
final newDevice = event.device!; |
|||
|
|||
// 更新当前设备 |
|||
currentDeviceRx.value = newDevice; |
|||
|
|||
Logger.info('经典蓝牙设备已连接: ${newDevice.name} (${newDevice.address})'); |
|||
|
|||
// 创建基本设备信息 |
|||
if (_deviceInfo.value == null || _deviceInfo.value!.address != newDevice.address) { |
|||
_deviceInfo.value = DeviceInfo( |
|||
address: newDevice.address, |
|||
name: newDevice.name, |
|||
); |
|||
Logger.info('创建设备信息缓存: ${_deviceInfo.value}'); |
|||
} |
|||
} else if (event.type == 'deviceDisconnected' && event.device != null) { |
|||
// 直接从事件中获取设备信息 |
|||
final disconnectedDevice = event.device!; |
|||
|
|||
Logger.info('经典蓝牙设备已断开: ${disconnectedDevice.name} (${disconnectedDevice.address})'); |
|||
|
|||
// 如果断开的是当前设备,清除当前设备 |
|||
if (currentDeviceRx.value?.address == disconnectedDevice.address) { |
|||
currentDeviceRx.value = null; |
|||
// 不清除设备信息缓存,保留最后状态 |
|||
Logger.info('已清除当前设备引用,保留最后设备信息缓存'); |
|||
} |
|||
} |
|||
}, |
|||
onError: (error) { |
|||
Logger.error('蓝牙事件流错误: $error'); |
|||
}, |
|||
); |
|||
} catch (e) { |
|||
Logger.error('设置蓝牙事件监听器失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 刷新设备连接状态 |
|||
Future<void> refreshDeviceConnection() async { |
|||
try { |
|||
// 检查经典蓝牙连接 |
|||
final devices = await ClassicBluetooth.getConnectedDevices(); |
|||
|
|||
if (devices.isNotEmpty) { |
|||
final newDevice = devices.first; |
|||
|
|||
// 更新当前设备 |
|||
currentDeviceRx.value = newDevice; |
|||
|
|||
Logger.info('已连接经典蓝牙设备: ${newDevice.name} (${newDevice.address})'); |
|||
|
|||
// 创建基本设备信息 |
|||
if (_deviceInfo.value == null || _deviceInfo.value!.address != newDevice.address) { |
|||
_deviceInfo.value = DeviceInfo( |
|||
address: newDevice.address, |
|||
name: newDevice.name, |
|||
); |
|||
Logger.info('创建设备信息缓存: ${_deviceInfo.value}'); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
// 没有任何连接 |
|||
currentDeviceRx.value = null; |
|||
Logger.info('未找到已连接的蓝牙设备'); |
|||
|
|||
} catch (e) { |
|||
Logger.error('刷新设备连接失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 检查蓝牙是否已启用 |
|||
Future<bool> isBluetoothEnabled() async { |
|||
try { |
|||
final isEnabled = await ClassicBluetooth.isBluetoothEnabled(); |
|||
_bluetoothState.value = isEnabled ? 'on' : 'off'; |
|||
return isEnabled; |
|||
} catch (e) { |
|||
Logger.error('检查蓝牙状态失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 启动BLE服务(不适用于此服务,仅为接口兼容) |
|||
Future<bool> startBleService(Map<String, dynamic> config) async { |
|||
Logger.warning('BluetoothService不支持BLE服务,请使用BleService'); |
|||
return false; |
|||
} |
|||
|
|||
/// 停止BLE服务(不适用于此服务,仅为接口兼容) |
|||
Future<bool> stopBleService() async { |
|||
Logger.warning('BluetoothService不支持BLE服务,请使用BleService'); |
|||
return false; |
|||
} |
|||
|
|||
/// 注册服务 |
|||
static Future<BluetoothService> init() async { |
|||
if (!Get.isRegistered<BluetoothService>()) { |
|||
Get.put(BluetoothService(), permanent: true); |
|||
} |
|||
return Future.value(Get.find<BluetoothService>()); |
|||
} |
|||
} |
|||
@ -1,350 +0,0 @@ |
|||
import 'dart:async'; |
|||
import 'dart:convert'; |
|||
import 'package:deep_voice/data/models/appconfig_model.dart'; |
|||
import 'package:flutter/services.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter_dotenv/flutter_dotenv.dart'; |
|||
import 'package:get_storage/get_storage.dart'; |
|||
import '../models/events/voice_interaction_event.dart'; |
|||
import '../../core/utils/logger.dart'; |
|||
import '../../modules/chat/models/message_model.dart'; |
|||
import '../../routes/app_routes.dart'; |
|||
import 'chat_history_service.dart'; |
|||
|
|||
/// 语音交互服务接口 |
|||
/// |
|||
/// 管理与平台原生语音交互服务的通信,提供统一的接口供应用使用 |
|||
class VoiceInteractionService extends GetxService { |
|||
static VoiceInteractionService get to => Get.find<VoiceInteractionService>(); |
|||
final GetStorage _storage = GetStorage(); |
|||
// 方法通道 |
|||
static const MethodChannel _channel = |
|||
MethodChannel('com.deep_voice.voice_interaction'); |
|||
|
|||
// 事件通道 |
|||
static const EventChannel _eventChannel = |
|||
EventChannel('com.deep_voice.voice_interaction_events'); |
|||
|
|||
// 服务状态 |
|||
final _isServiceRunning = false.obs; |
|||
bool get isServiceRunning => _isServiceRunning.value; |
|||
|
|||
// 流控制器 |
|||
final _eventStreamController = |
|||
StreamController<VoiceInteractionEvent>.broadcast(); |
|||
|
|||
// 事件流 |
|||
Stream<VoiceInteractionEvent> get eventStream => |
|||
_eventStreamController.stream; |
|||
|
|||
// 事件通道订阅 |
|||
StreamSubscription? _eventSubscription; |
|||
|
|||
// 标记是否初始化 |
|||
bool _isInitialized = false; |
|||
|
|||
// 配置信息 |
|||
late String _azureSpeechKey; |
|||
late String _azureSpeechRegion; |
|||
late String _openaiApiKey; |
|||
late String _openaiBaseUrl; |
|||
late String _openaiModel; |
|||
late String _volcanoSpeechAppId; |
|||
late String _volcanoSpeechAppToken; |
|||
// MCP配置信息 |
|||
String? _mcpConfig; |
|||
|
|||
// 聊天历史服务 |
|||
late final ChatHistoryService _chatHistoryService; |
|||
|
|||
/// 设置事件通道 |
|||
void _setupEventChannel() { |
|||
_eventSubscription = _eventChannel |
|||
.receiveBroadcastStream() |
|||
.listen(_handleVoiceInteractionEvent, onError: (error) { |
|||
Logger.error('语音交互事件通道错误: $error'); |
|||
}); |
|||
} |
|||
|
|||
/// 从环境变量加载配置 |
|||
void _loadConfig() { |
|||
// _azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; |
|||
// _azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; |
|||
// _openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; |
|||
// _openaiBaseUrl = dotenv.env['OPENAI_BASE_URL'] ?? ''; |
|||
// _openaiModel = dotenv.env['OPENAI_MODEL'] ?? ''; |
|||
// _volcanoSpeechAppId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? ''; |
|||
// _volcanoSpeechAppToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? ''; |
|||
final _env = _storage.read("ENV") as Map<String, String>; |
|||
_azureSpeechKey = _env['AZURE_SPEECH_KEY'] ?? ''; |
|||
_azureSpeechRegion = _env['AZURE_SPEECH_REGION'] ?? ''; |
|||
_openaiApiKey = _env['OPENAI_API_KEY'] ?? ''; |
|||
_openaiBaseUrl = _env['OPENAI_BASE_URL'] ?? ''; |
|||
_openaiModel = _env['OPENAI_MODEL'] ?? ''; |
|||
_volcanoSpeechAppId = _env['VOLCANO_SPEECH_APP_ID'] ?? ''; |
|||
_volcanoSpeechAppToken = _env['VOLCANO_SPEECH_APP_TOKEN'] ?? ''; |
|||
|
|||
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) { |
|||
Logger.warning( |
|||
'未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); |
|||
} |
|||
|
|||
// 加载MCP配置 |
|||
_loadMcpConfig(); |
|||
} |
|||
|
|||
/// 加载MCP配置 |
|||
void _loadMcpConfig() { |
|||
try { |
|||
final mcps = _storage.read("MCPS") as Map<String, DBMCPServer>; |
|||
String config = jsonEncode(mcps); |
|||
_mcpConfig = config; |
|||
Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig'); |
|||
// rootBundle.loadString('.mcp.json').then((config) { |
|||
// _mcpConfig = config; |
|||
// Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig'); |
|||
// }); |
|||
} catch (e) { |
|||
Logger.warning('加载MCP配置失败: $e'); |
|||
_mcpConfig = null; |
|||
} |
|||
} |
|||
|
|||
/// 处理来自原生层的事件 |
|||
void _handleVoiceInteractionEvent(dynamic event) { |
|||
if (event is! Map) return; |
|||
|
|||
final eventMap = event as Map<dynamic, dynamic>; |
|||
final String eventType = eventMap['type'] as String? ?? ''; |
|||
// final int timestamp = eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch; |
|||
|
|||
Logger.info('收到语音交互事件: $eventType'); |
|||
|
|||
switch (eventType) { |
|||
case 'recognition_started': |
|||
// 语音识别开始事件 |
|||
final recognitionEvent = RecognitionStartedEvent( |
|||
timestamp: DateTime.now().millisecondsSinceEpoch, |
|||
); |
|||
_eventStreamController.add(recognitionEvent); |
|||
break; |
|||
|
|||
case 'chat_history_updated': |
|||
// 聊天历史更新事件 |
|||
final String agentId = eventMap['agentId'] as String? ?? ''; |
|||
final String userMessage = eventMap['userMessage'] as String? ?? ''; |
|||
final String assistantMessage = |
|||
eventMap['assistantMessage'] as String? ?? ''; |
|||
|
|||
final chatHistoryEvent = ChatHistoryEvent( |
|||
agentId: agentId, |
|||
userMessage: userMessage, |
|||
assistantMessage: assistantMessage, |
|||
timestamp: DateTime.now().millisecondsSinceEpoch, |
|||
); |
|||
_eventStreamController.add(chatHistoryEvent); |
|||
|
|||
// 保存聊天历史到ChatHistoryService |
|||
_saveChatHistory(agentId, userMessage, assistantMessage); |
|||
break; |
|||
|
|||
case 'enter_translation_mode': |
|||
// 进入翻译模式事件 |
|||
Logger.info('收到进入翻译模式事件,正在导航到翻译界面'); |
|||
_navigateToTranslation(); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
/// 导航到翻译界面 |
|||
void _navigateToTranslation() { |
|||
try { |
|||
// 使用GetX导航到翻译页面 |
|||
Get.offNamedUntil( |
|||
Routes.translation, (route) => route.settings.name == Routes.home); |
|||
Logger.info('已导航到翻译界面'); |
|||
} catch (e) { |
|||
Logger.error('导航到翻译界面失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 保存聊天历史 |
|||
Future<void> _saveChatHistory( |
|||
String agentId, String userMessage, String assistantMessage) async { |
|||
try { |
|||
final timestamp = DateTime.now().millisecondsSinceEpoch; |
|||
|
|||
// 创建用户消息和助手消息 |
|||
final userMsg = Message( |
|||
role: 'user', |
|||
content: userMessage, |
|||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), |
|||
); |
|||
|
|||
final assistantMsg = Message( |
|||
role: 'assistant', |
|||
content: assistantMessage, |
|||
timestamp: DateTime.fromMillisecondsSinceEpoch( |
|||
timestamp + 1), // 确保助手消息时间戳晚于用户消息 |
|||
); |
|||
|
|||
// 加载现有历史记录 |
|||
final existingMessages = await _chatHistoryService.loadHistory(agentId); |
|||
|
|||
// 添加新消息 |
|||
existingMessages.addAll([userMsg, assistantMsg]); |
|||
|
|||
// 保存更新后的历史记录 |
|||
await _chatHistoryService.saveHistory(agentId, existingMessages); |
|||
|
|||
Logger.info('已保存聊天历史: agentId=$agentId'); |
|||
} catch (e) { |
|||
Logger.error('保存聊天历史失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 初始化服务 |
|||
Future<bool> initialize() async { |
|||
if (_isInitialized) return true; |
|||
|
|||
try { |
|||
Logger.info('正在初始化语音交互服务...'); |
|||
|
|||
// 获取聊天历史服务 |
|||
try { |
|||
_chatHistoryService = Get.find<ChatHistoryService>(); |
|||
} catch (e) { |
|||
Logger.warning('获取ChatHistoryService失败,将创建新实例'); |
|||
_chatHistoryService = Get.put(ChatHistoryService()); |
|||
} |
|||
|
|||
// 设置事件通道 |
|||
_setupEventChannel(); |
|||
|
|||
// 加载配置 |
|||
_loadConfig(); |
|||
|
|||
// 检查服务是否已在运行 |
|||
final bool running = await checkServiceStatus(); |
|||
|
|||
// 如果服务未运行,启动服务 |
|||
if (!running) { |
|||
// 启动语音交互服务 |
|||
final success = await startService(); |
|||
if (!success) { |
|||
Logger.error('语音交互服务启动失败'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
_isInitialized = true; |
|||
Logger.info('语音交互服务初始化完成'); |
|||
return true; |
|||
} catch (e) { |
|||
Logger.error('语音交互服务初始化失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 启动语音交互服务 |
|||
Future<bool> startService() async { |
|||
try { |
|||
Logger.info('启动语音交互服务...'); |
|||
|
|||
// 构建参数 |
|||
final Map<String, dynamic> params = { |
|||
'azure_speech_key': _azureSpeechKey, |
|||
'azure_speech_region': _azureSpeechRegion, |
|||
'openai_api_key': _openaiApiKey, |
|||
'openai_base_url': _openaiBaseUrl, |
|||
'openai_model': _openaiModel, |
|||
'volcano_speech_app_id': _volcanoSpeechAppId, |
|||
'volcano_speech_app_token': _volcanoSpeechAppToken, |
|||
}; |
|||
|
|||
// 如果有MCP配置,添加到参数中 |
|||
if (_mcpConfig != null) { |
|||
params['mcp_config'] = _mcpConfig; |
|||
} |
|||
|
|||
final result = |
|||
await _channel.invokeMethod<bool>('startService', params) ?? false; |
|||
|
|||
if (result) { |
|||
_isServiceRunning.value = true; |
|||
Logger.info('语音交互服务已启动'); |
|||
} else { |
|||
Logger.error('启动语音交互服务失败'); |
|||
} |
|||
|
|||
return result; |
|||
} catch (e) { |
|||
Logger.error('启动语音交互服务时发生错误: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 停止语音交互服务 |
|||
Future<bool> stopService() async { |
|||
try { |
|||
Logger.info('停止语音交互服务...'); |
|||
|
|||
final result = await _channel.invokeMethod<bool>('stopService') ?? false; |
|||
|
|||
if (result) { |
|||
_isServiceRunning.value = false; |
|||
Logger.info('语音交互服务已停止'); |
|||
} else { |
|||
Logger.error('停止语音交互服务失败'); |
|||
} |
|||
|
|||
return result; |
|||
} catch (e) { |
|||
Logger.error('停止语音交互服务时发生错误: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 检查服务是否运行 |
|||
Future<bool> checkServiceStatus() async { |
|||
try { |
|||
final bool result = |
|||
await _channel.invokeMethod<bool>('isServiceRunning') ?? false; |
|||
_isServiceRunning.value = result; |
|||
return result; |
|||
} catch (e) { |
|||
Logger.error('检查服务状态时发生错误: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 暂停语音交互 |
|||
Future<bool> pauseVoiceInteraction() async { |
|||
try { |
|||
Logger.info('暂停后台语音交互...'); |
|||
|
|||
final result = |
|||
await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false; |
|||
|
|||
if (result) { |
|||
Logger.info('语音交互已暂停'); |
|||
} else { |
|||
Logger.error('暂停语音交互失败'); |
|||
} |
|||
|
|||
return result; |
|||
} catch (e) { |
|||
Logger.error('暂停语音交互时发生错误: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 资源释放 |
|||
@override |
|||
void onClose() { |
|||
_eventSubscription?.cancel(); |
|||
_eventStreamController.close(); |
|||
super.onClose(); |
|||
} |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/ble_test_controller.dart'; |
|||
|
|||
class BleTestBinding extends Bindings { |
|||
@override |
|||
void dependencies() { |
|||
Get.lazyPut<BleTestController>( |
|||
() => BleTestController(), |
|||
); |
|||
} |
|||
} |
|||
@ -1,433 +0,0 @@ |
|||
import 'dart:async'; |
|||
import 'dart:typed_data'; |
|||
|
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:ble_service/ble_service.dart'; |
|||
|
|||
/// BLE测试控制器 |
|||
/// 用于测试BLE服务插件的功能 |
|||
class BleTestController extends GetxController { |
|||
// 设备列表 |
|||
final deviceList = <BleDevice>[].obs; |
|||
|
|||
// 扫描状态 |
|||
final isScanning = false.obs; |
|||
|
|||
// 连接状态 |
|||
final isConnected = false.obs; |
|||
final connectionState = 0.obs; |
|||
|
|||
// 设备信息 |
|||
final connectedDeviceAddress = ''.obs; |
|||
final batteryLevel = 0.obs; |
|||
final firmwareVersion = ''.obs; |
|||
|
|||
// 状态消息 |
|||
final statusMessage = ''.obs; |
|||
|
|||
// BLE服务 |
|||
final _bleService = BleService(); |
|||
|
|||
// 订阅 |
|||
late StreamSubscription _scanSubscription; |
|||
late StreamSubscription _connectionSubscription; |
|||
late StreamSubscription _dataSubscription; |
|||
late StreamSubscription _statusSubscription; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_initBleService(); |
|||
} |
|||
|
|||
/// 初始化BLE服务 |
|||
Future<void> _initBleService() async { |
|||
try { |
|||
// 初始化BLE服务 |
|||
final result = await _bleService.initialize(); |
|||
if (result) { |
|||
statusMessage.value = 'BLE服务初始化成功'; |
|||
|
|||
// 注册回调 |
|||
await _bleService.registerCallback(); |
|||
|
|||
// 订阅扫描结果 |
|||
_scanSubscription = _bleService.onScanResults.listen((devices) { |
|||
if (devices.isEmpty) { |
|||
deviceList.clear(); |
|||
return; |
|||
} |
|||
|
|||
bool hasChanges = false; |
|||
|
|||
for (final newDevice in devices) { |
|||
int existingIndex = deviceList.indexWhere((d) => d.id == newDevice.id); |
|||
if (existingIndex >= 0) { |
|||
BleDevice existing = deviceList[existingIndex]; |
|||
if (existing.rssi != newDevice.rssi || existing.name != newDevice.name) { |
|||
deviceList[existingIndex] = newDevice; |
|||
hasChanges = true; |
|||
} |
|||
} else { |
|||
deviceList.add(newDevice); |
|||
hasChanges = true; |
|||
} |
|||
} |
|||
|
|||
if (hasChanges) { |
|||
deviceList.refresh(); |
|||
} |
|||
}); |
|||
|
|||
// 订阅连接状态 |
|||
_connectionSubscription = _bleService.onConnectionChanged.listen((state) { |
|||
if (state is int) { |
|||
connectionState.value = state; |
|||
isConnected.value = state == BleConnectionState.connected; |
|||
statusMessage.value = '连接状态变更: ${_getStateString(state)}'; |
|||
} |
|||
}); |
|||
|
|||
// 订阅数据 |
|||
_dataSubscription = _bleService.onDataReceived.listen((data) { |
|||
if (data is Map) { |
|||
final type = data['type']; |
|||
if (type == 'wakeup') { |
|||
statusMessage.value = '收到唤醒信号'; |
|||
} else if (type == 'audioData') { |
|||
statusMessage.value = '收到音频数据: ${(data['data'] as Uint8List).length} 字节'; |
|||
} else if (type == 'rawData') { |
|||
statusMessage.value = '收到原始数据: ${(data['data'] as Uint8List).length} 字节'; |
|||
} |
|||
} |
|||
}); |
|||
|
|||
// 订阅状态更新 |
|||
_statusSubscription = _bleService.onStatusUpdated.listen((status) { |
|||
if (status is Map) { |
|||
final type = status['type']; |
|||
if (type == 'statusUpdate') { |
|||
// 更新状态 |
|||
connectionState.value = status['connectionState'] as int? ?? 0; |
|||
isConnected.value = connectionState.value == BleConnectionState.connected; |
|||
batteryLevel.value = status['batteryLevel'] as int? ?? 0; |
|||
firmwareVersion.value = status['firmwareVersion'] as String? ?? ''; |
|||
connectedDeviceAddress.value = status['connectedDevice'] as String? ?? ''; |
|||
|
|||
statusMessage.value = '状态更新: ${_getStateString(connectionState.value)}'; |
|||
} else if (type == 'deviceInfo') { |
|||
// 设备信息 |
|||
final infoType = status['infoType'] as int; |
|||
final infoData = status['infoData'] as String; |
|||
|
|||
statusMessage.value = '设备信息: $infoData'; |
|||
|
|||
// 处理不同类型的设备信息 |
|||
if (infoType == 0x80) { // 版本信息 |
|||
firmwareVersion.value = infoData; |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
|
|||
// 获取连接状态 |
|||
_updateConnectionState(); |
|||
} else { |
|||
statusMessage.value = 'BLE服务初始化失败'; |
|||
} |
|||
} catch (e) { |
|||
statusMessage.value = '初始化异常: $e'; |
|||
Get.snackbar('初始化失败', '无法初始化BLE服务: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 获取状态字符串 |
|||
String _getStateString(int state) { |
|||
switch (state) { |
|||
case BleConnectionState.disconnected: |
|||
return '已断开'; |
|||
case BleConnectionState.connecting: |
|||
return '连接中'; |
|||
case BleConnectionState.connected: |
|||
return '已连接'; |
|||
case BleConnectionState.disconnecting: |
|||
return '断开中'; |
|||
case BleConnectionState.error: |
|||
return '连接错误'; |
|||
default: |
|||
return '未知状态($state)'; |
|||
} |
|||
} |
|||
|
|||
/// 更新连接状态 |
|||
Future<void> _updateConnectionState() async { |
|||
connectionState.value = await _bleService.getConnectionState(); |
|||
isConnected.value = connectionState.value == BleConnectionState.connected; |
|||
|
|||
if (isConnected.value) { |
|||
connectedDeviceAddress.value = await _bleService.getConnectedDeviceAddress() ?? ''; |
|||
batteryLevel.value = await _bleService.getBatteryLevel(); |
|||
firmwareVersion.value = await _bleService.getFirmwareVersion() ?? ''; |
|||
} |
|||
} |
|||
|
|||
/// 开始扫描设备 |
|||
Future<void> startScan() async { |
|||
try { |
|||
// 清空设备列表 |
|||
deviceList.clear(); |
|||
|
|||
// 设置扫描状态 |
|||
isScanning.value = true; |
|||
statusMessage.value = '开始扫描...'; |
|||
|
|||
// 开始扫描 |
|||
final result = await _bleService.startScan(timeout: 15000); |
|||
|
|||
if (!result) { |
|||
statusMessage.value = '扫描失败'; |
|||
Get.snackbar('扫描失败', '无法启动BLE扫描', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} catch (e) { |
|||
statusMessage.value = '扫描异常: $e'; |
|||
Get.snackbar('扫描错误', '扫描过程中出错: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} finally { |
|||
// 5秒后自动停止扫描 |
|||
Future.delayed(const Duration(seconds: 15), () { |
|||
if (isScanning.value) { |
|||
stopScan(); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/// 停止扫描设备 |
|||
Future<void> stopScan() async { |
|||
try { |
|||
await _bleService.stopScan(); |
|||
isScanning.value = false; |
|||
statusMessage.value = '扫描已停止'; |
|||
} catch (e) { |
|||
statusMessage.value = '停止扫描异常: $e'; |
|||
Get.snackbar('停止扫描失败', '无法停止BLE扫描: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 连接到设备 |
|||
Future<void> connectToDevice(String address) async { |
|||
try { |
|||
statusMessage.value = '正在连接到 $address...'; |
|||
|
|||
final result = await _bleService.connect(address); |
|||
|
|||
if (!result) { |
|||
statusMessage.value = '连接失败'; |
|||
Get.snackbar('连接失败', '无法连接到设备', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} else { |
|||
connectedDeviceAddress.value = address; |
|||
statusMessage.value = '连接成功,正在获取设备信息...'; |
|||
|
|||
// 获取设备信息 |
|||
await _bleService.getDeviceVersion(); |
|||
await _bleService.getBatteryInfo(); |
|||
} |
|||
} catch (e) { |
|||
statusMessage.value = '连接异常: $e'; |
|||
Get.snackbar('连接错误', '连接过程中出错: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 断开设备连接 |
|||
Future<void> disconnect() async { |
|||
try { |
|||
statusMessage.value = '正在断开连接...'; |
|||
|
|||
await _bleService.disconnect(); |
|||
|
|||
connectedDeviceAddress.value = ''; |
|||
isConnected.value = false; |
|||
|
|||
statusMessage.value = '已断开连接'; |
|||
} catch (e) { |
|||
statusMessage.value = '断开连接异常: $e'; |
|||
Get.snackbar('断开连接失败', '无法断开设备连接: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 获取设备版本信息 |
|||
Future<void> getDeviceVersion() async { |
|||
try { |
|||
statusMessage.value = '正在获取设备版本...'; |
|||
|
|||
await _bleService.getDeviceVersion(); |
|||
|
|||
statusMessage.value = '版本获取命令已发送'; |
|||
} catch (e) { |
|||
statusMessage.value = '获取版本异常: $e'; |
|||
Get.snackbar('获取版本失败', '无法获取设备版本: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 获取电池信息 |
|||
Future<void> getBatteryInfo() async { |
|||
try { |
|||
statusMessage.value = '正在获取电池信息...'; |
|||
|
|||
await _bleService.getBatteryInfo(); |
|||
|
|||
statusMessage.value = '电池信息获取命令已发送'; |
|||
} catch (e) { |
|||
statusMessage.value = '获取电池信息异常: $e'; |
|||
Get.snackbar('获取电池信息失败', '无法获取电池信息: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 打开编码器 |
|||
Future<void> openEncoder() async { |
|||
try { |
|||
statusMessage.value = '正在打开编码器...'; |
|||
|
|||
await _bleService.openEncoder(); |
|||
|
|||
statusMessage.value = '编码器打开命令已发送'; |
|||
} catch (e) { |
|||
statusMessage.value = '打开编码器异常: $e'; |
|||
Get.snackbar('操作失败', '无法打开编码器: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 打开解码器 |
|||
Future<void> openDecoder() async { |
|||
try { |
|||
statusMessage.value = '正在打开解码器...'; |
|||
|
|||
await _bleService.openDecoder(); |
|||
|
|||
statusMessage.value = '解码器打开命令已发送'; |
|||
} catch (e) { |
|||
statusMessage.value = '打开解码器异常: $e'; |
|||
Get.snackbar('操作失败', '无法打开解码器: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 关闭编解码器 |
|||
Future<void> closeCodec() async { |
|||
try { |
|||
statusMessage.value = '正在关闭编解码器...'; |
|||
|
|||
await _bleService.closeCodec(); |
|||
|
|||
statusMessage.value = '编解码器关闭命令已发送'; |
|||
} catch (e) { |
|||
statusMessage.value = '关闭编解码器异常: $e'; |
|||
Get.snackbar('操作失败', '无法关闭编解码器: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 播放A2DP |
|||
Future<void> playA2DP() async { |
|||
try { |
|||
statusMessage.value = '正在播放A2DP...'; |
|||
|
|||
await _bleService.playA2DP(); |
|||
|
|||
statusMessage.value = 'A2DP播放命令已发送'; |
|||
} catch (e) { |
|||
statusMessage.value = '播放A2DP异常: $e'; |
|||
Get.snackbar('操作失败', '无法播放A2DP: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 音量增加 |
|||
Future<void> volumeUp() async { |
|||
try { |
|||
statusMessage.value = '正在增加音量...'; |
|||
|
|||
await _bleService.volumeUp(); |
|||
|
|||
statusMessage.value = '音量增加命令已发送'; |
|||
} catch (e) { |
|||
statusMessage.value = '增加音量异常: $e'; |
|||
Get.snackbar('操作失败', '无法增加音量: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 音量减小 |
|||
Future<void> volumeDown() async { |
|||
try { |
|||
statusMessage.value = '正在减小音量...'; |
|||
|
|||
await _bleService.volumeDown(); |
|||
|
|||
statusMessage.value = '音量减小命令已发送'; |
|||
} catch (e) { |
|||
statusMessage.value = '减小音量异常: $e'; |
|||
Get.snackbar('操作失败', '无法减小音量: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 发送音频数据 |
|||
Future<void> sendAudioData(Uint8List data) async { |
|||
try { |
|||
statusMessage.value = '正在发送音频数据...'; |
|||
|
|||
await _bleService.sendAudioData(data); |
|||
|
|||
statusMessage.value = '音频数据发送成功: ${data.length} 字节'; |
|||
} catch (e) { |
|||
statusMessage.value = '发送音频数据异常: $e'; |
|||
Get.snackbar('发送失败', '无法发送音频数据: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
/// 清理资源 |
|||
Future<void> cleanup() async { |
|||
try { |
|||
await _bleService.cleanup(); |
|||
} catch (e) { |
|||
Get.snackbar('清理失败', '无法清理BLE资源: $e', |
|||
snackPosition: SnackPosition.BOTTOM); |
|||
} |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
// 取消所有订阅 |
|||
_scanSubscription.cancel(); |
|||
_connectionSubscription.cancel(); |
|||
_dataSubscription.cancel(); |
|||
_statusSubscription.cancel(); |
|||
|
|||
// 停止扫描 |
|||
if (isScanning.value) { |
|||
stopScan(); |
|||
} |
|||
|
|||
// 断开连接 |
|||
if (isConnected.value) { |
|||
disconnect(); |
|||
} |
|||
|
|||
// 清理资源 |
|||
cleanup(); |
|||
|
|||
super.onClose(); |
|||
} |
|||
} |
|||
@ -1,279 +0,0 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/ble_test_controller.dart'; |
|||
|
|||
class BleTestView extends GetView<BleTestController> { |
|||
const BleTestView({Key? key}) : super(key: key); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: AppBar( |
|||
title: const Text('BLE测试'), |
|||
actions: [ |
|||
Obx(() => IconButton( |
|||
icon: const Icon(Icons.refresh), |
|||
onPressed: controller.isScanning.value ? null : controller.startScan, |
|||
)), |
|||
], |
|||
), |
|||
body: Column( |
|||
children: [ |
|||
// 状态消息 - 使用AnimatedContainer让过渡更平滑 |
|||
Obx(() => AnimatedContainer( |
|||
duration: const Duration(milliseconds: 300), |
|||
padding: const EdgeInsets.all(8), |
|||
width: double.infinity, |
|||
color: Colors.grey[200], |
|||
child: Text( |
|||
controller.statusMessage.value, |
|||
style: const TextStyle(fontSize: 14), |
|||
), |
|||
)), |
|||
|
|||
// 根据连接状态显示不同的内容 |
|||
Expanded( |
|||
child: Obx(() { |
|||
// 保存连接状态到局部变量,避免多次访问反应式变量 |
|||
final isConnected = controller.isConnected.value; |
|||
return isConnected ? _buildConnectedView() : _buildScanView(); |
|||
}), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 扫描视图 |
|||
Widget _buildScanView() { |
|||
return Column( |
|||
children: [ |
|||
// 扫描控制 |
|||
Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|||
children: [ |
|||
Obx(() { |
|||
final isScanning = controller.isScanning.value; |
|||
return ElevatedButton( |
|||
onPressed: isScanning ? null : controller.startScan, |
|||
child: Text(isScanning ? '扫描中...' : '开始扫描'), |
|||
); |
|||
}), |
|||
Obx(() => ElevatedButton( |
|||
onPressed: controller.isScanning.value |
|||
? controller.stopScan |
|||
: null, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.red, |
|||
), |
|||
child: const Text('停止扫描'), |
|||
)), |
|||
], |
|||
), |
|||
), |
|||
|
|||
// 设备列表 - 使用RepaintBoundary提高性能 |
|||
Expanded( |
|||
child: RepaintBoundary( |
|||
child: Obx(() { |
|||
if (controller.deviceList.isEmpty) { |
|||
return const Center( |
|||
child: Text('没有找到设备,请开始扫描'), |
|||
); |
|||
} |
|||
|
|||
// 仅在需要时创建列表视图 |
|||
return ListView.builder( |
|||
// 使用常量key避免整体重建 |
|||
key: const ValueKey('device_list'), |
|||
itemCount: controller.deviceList.length, |
|||
itemBuilder: (context, index) { |
|||
final device = controller.deviceList[index]; |
|||
return Card( |
|||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), |
|||
child: ListTile( |
|||
title: Text(device.name), |
|||
subtitle: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text('ID: ${device.id}'), |
|||
if (device.uuids.isNotEmpty) |
|||
Text( |
|||
'UUID: ${device.uuids.length > 0 ? device.uuids.first : "无"}', |
|||
style: const TextStyle(fontSize: 12), |
|||
overflow: TextOverflow.ellipsis, |
|||
maxLines: 1, |
|||
), |
|||
if (device.uuids.length > 1) |
|||
Text( |
|||
'其他UUID: ${device.uuids.length - 1}个', |
|||
style: const TextStyle(fontSize: 12), |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
], |
|||
), |
|||
trailing: Column( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Text('RSSI: ${device.rssi}'), |
|||
const SizedBox(height: 4), |
|||
const Icon(Icons.bluetooth, size: 16), |
|||
], |
|||
), |
|||
onTap: () => controller.connectToDevice(device.id), |
|||
), |
|||
); |
|||
}, |
|||
); |
|||
}), |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
// 已连接设备视图 |
|||
Widget _buildConnectedView() { |
|||
return SingleChildScrollView( |
|||
padding: const EdgeInsets.all(16), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
// 设备信息卡片 |
|||
Card( |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(16), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Obx(() => Text( |
|||
'设备地址: ${controller.connectedDeviceAddress.value}', |
|||
style: const TextStyle(fontWeight: FontWeight.bold), |
|||
)), |
|||
const SizedBox(height: 8), |
|||
Obx(() => Text('电池电量: ${controller.batteryLevel.value}%')), |
|||
const SizedBox(height: 4), |
|||
Obx(() => Text('固件版本: ${controller.firmwareVersion.value}')), |
|||
const SizedBox(height: 16), |
|||
Wrap( |
|||
spacing: 8, // 水平间距 |
|||
runSpacing: 8, // 垂直间距 |
|||
alignment: WrapAlignment.spaceBetween, |
|||
children: [ |
|||
SizedBox( |
|||
width: 110, |
|||
child: ElevatedButton( |
|||
onPressed: controller.getDeviceVersion, |
|||
child: const Text('获取版本'), |
|||
), |
|||
), |
|||
SizedBox( |
|||
width: 110, |
|||
child: ElevatedButton( |
|||
onPressed: controller.getBatteryInfo, |
|||
child: const Text('获取电量'), |
|||
), |
|||
), |
|||
SizedBox( |
|||
width: 110, |
|||
child: ElevatedButton( |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.red, |
|||
), |
|||
onPressed: controller.disconnect, |
|||
child: const Text('断开连接'), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
|
|||
const SizedBox(height: 24), |
|||
|
|||
// 音频控制 |
|||
const Text('音频控制', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), |
|||
const SizedBox(height: 16), |
|||
|
|||
Wrap( |
|||
spacing: 8, |
|||
runSpacing: 8, |
|||
alignment: WrapAlignment.start, |
|||
children: [ |
|||
SizedBox( |
|||
width: 140, |
|||
child: ElevatedButton( |
|||
onPressed: controller.openEncoder, |
|||
child: const Text('打开编码器'), |
|||
), |
|||
), |
|||
SizedBox( |
|||
width: 140, |
|||
child: ElevatedButton( |
|||
onPressed: controller.openDecoder, |
|||
child: const Text('打开解码器'), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
Wrap( |
|||
spacing: 8, |
|||
runSpacing: 8, |
|||
alignment: WrapAlignment.start, |
|||
children: [ |
|||
SizedBox( |
|||
width: 140, |
|||
child: ElevatedButton( |
|||
onPressed: controller.closeCodec, |
|||
child: const Text('关闭编解码器'), |
|||
), |
|||
), |
|||
SizedBox( |
|||
width: 140, |
|||
child: ElevatedButton( |
|||
onPressed: controller.playA2DP, |
|||
child: const Text('播放A2DP'), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
|
|||
const SizedBox(height: 24), |
|||
|
|||
// 音量控制 |
|||
const Text('音量控制', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), |
|||
const SizedBox(height: 16), |
|||
|
|||
Wrap( |
|||
spacing: 8, |
|||
runSpacing: 8, |
|||
alignment: WrapAlignment.start, |
|||
children: [ |
|||
SizedBox( |
|||
width: 140, |
|||
child: ElevatedButton( |
|||
onPressed: controller.volumeUp, |
|||
child: const Text('音量+'), |
|||
), |
|||
), |
|||
SizedBox( |
|||
width: 140, |
|||
child: ElevatedButton( |
|||
onPressed: controller.volumeDown, |
|||
child: const Text('音量-'), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue