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