Browse Source

Merge branch 'ble' of github.com:deepcloud2048/deep_voice into ble

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
53f8f11d3f
  1. 142
      lib/data/services/ble_manager.dart
  2. 2
      lib/modules/agent/controllers/agent_controller.dart
  3. 115
      lib/modules/agent/views/message_bubble.dart
  4. 156
      lib/modules/home/controllers/home_controller.dart
  5. 182
      lib/modules/home/views/home_view.dart
  6. 170
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  7. 0
      lib/modules/ota_upgrade/model/ota_upgrade_model.dart
  8. 200
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  9. 138
      lib/modules/pairing/controllers/pairing_controller.dart
  10. 1
      local_plugins/agent_service/android/build.gradle.kts
  11. 70
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  12. 60
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  13. 10
      local_plugins/ble_service/ios/Classes/BleService.swift
  14. 8
      local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift
  15. 11
      local_plugins/ble_service/lib/ble_service.dart
  16. 1
      local_plugins/music_service/android/build.gradle
  17. 9
      local_plugins/music_service/android/src/main/AndroidManifest.xml
  18. 87
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  19. 25
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

142
lib/data/services/ble_manager.dart

@ -9,13 +9,15 @@ import 'package:deep_voice/data/models/appconfig_model.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle; import 'package:flutter/services.dart' show rootBundle;
import 'package:agent_service/agent_service.dart'; import 'package:agent_service/agent_service.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart';
/// 命令类型常量(与原生层保持一致) /// 命令类型常量(与原生层保持一致)
class BleCommands { class BleCommands {
static const int GET_VERSION = 0x01; static const int GET_VERSION = 0x01;
static const int GET_BATTERY_INFO = 0x02; static const int GET_PRODUCT_ID = 0x02;
static const int CONTROL_CODEC = 0x03; static const int GET_PRODUCT_COLOR = 0x03;
static const int PLAY_A2DP = 0x04; static const int GET_BATTERY_INFO = 0x04;
static const int CONTROL_CODEC = 0x05;
} }
/// 蓝牙设备信息缓存模型 /// 蓝牙设备信息缓存模型
@ -37,6 +39,8 @@ class DeviceInfo {
final bool caseCharging; final bool caseCharging;
final String firmwareVersion; final String firmwareVersion;
final String firmwareProductId;
final String firmwareProductColor;
final DateTime lastUpdated; final DateTime lastUpdated;
DeviceInfo({ DeviceInfo({
@ -50,6 +54,8 @@ class DeviceInfo {
this.rightEarCharging = false, this.rightEarCharging = false,
this.caseCharging = false, this.caseCharging = false,
this.firmwareVersion = '', this.firmwareVersion = '',
this.firmwareProductId = '',
this.firmwareProductColor = '',
DateTime? lastUpdated, DateTime? lastUpdated,
}) : lastUpdated = lastUpdated ?? DateTime.now(); }) : lastUpdated = lastUpdated ?? DateTime.now();
@ -65,6 +71,8 @@ class DeviceInfo {
bool? rightEarCharging, bool? rightEarCharging,
bool? caseCharging, bool? caseCharging,
String? firmwareVersion, String? firmwareVersion,
String? firmwareProductId,
String? firmwareProductColor,
}) { }) {
return DeviceInfo( return DeviceInfo(
address: address ?? this.address, address: address ?? this.address,
@ -77,6 +85,8 @@ class DeviceInfo {
rightEarCharging: rightEarCharging ?? this.rightEarCharging, rightEarCharging: rightEarCharging ?? this.rightEarCharging,
caseCharging: caseCharging ?? this.caseCharging, caseCharging: caseCharging ?? this.caseCharging,
firmwareVersion: firmwareVersion ?? this.firmwareVersion, firmwareVersion: firmwareVersion ?? this.firmwareVersion,
firmwareProductId: firmwareProductId ?? this.firmwareProductId,
firmwareProductColor: firmwareProductColor ?? this.firmwareProductColor,
lastUpdated: DateTime.now(), lastUpdated: DateTime.now(),
); );
} }
@ -96,6 +106,7 @@ class BleManager extends GetxService {
// BLE服务实例 // BLE服务实例
final BleService _bleService = BleService(); final BleService _bleService = BleService();
// 经典蓝牙实例
// BLE连接状态 // BLE连接状态
final _connectionState = BleConnectionState.disconnected.obs; final _connectionState = BleConnectionState.disconnected.obs;
@ -106,8 +117,7 @@ class BleManager extends GetxService {
bool get isConnected => _isConnected.value; bool get isConnected => _isConnected.value;
// 设备信息缓存 // 设备信息缓存
final Rx<DeviceInfo?> _deviceInfo = Rx<DeviceInfo?>(null); final deviceInfo = Rx<DeviceInfo?>(null); // 使用 Rx 包装
DeviceInfo? get deviceInfo => _deviceInfo.value;
// 订阅 // 订阅
StreamSubscription<dynamic>? _statusSubscription; StreamSubscription<dynamic>? _statusSubscription;
@ -145,8 +155,8 @@ class BleManager extends GetxService {
if (event is Map) { if (event is Map) {
// 处理连接状态更新 // 处理连接状态更新
if (event.containsKey('connectionState')) { if (event['type'] == 'connectionStateChanged') {
final state = event['connectionState'] as int; final state = event['state'] as int;
_connectionState.value = state; _connectionState.value = state;
// 连接状态转换 // 连接状态转换
@ -154,8 +164,13 @@ class BleManager extends GetxService {
Logger.info('BLE设备已连接'); Logger.info('BLE设备已连接');
_isConnected.value = true; _isConnected.value = true;
// 在连接成功后获取设备信息 // 在连接成功后延迟 300 毫秒获取设备信息(可根据需要调整时间)
_fetchDeviceInfo(); Future.delayed(const Duration(milliseconds: 300), () {
// 可选:检查是否仍处于连接状态
if (_isConnected.value) {
fetchDeviceInfo();
}
});
} else if (state == BleConnectionState.disconnected || } else if (state == BleConnectionState.disconnected ||
state == BleConnectionState.error) { state == BleConnectionState.error) {
Logger.info('BLE设备已断开或发生错误'); Logger.info('BLE设备已断开或发生错误');
@ -218,12 +233,18 @@ class BleManager extends GetxService {
void _processDeviceInfoByCommandId( void _processDeviceInfoByCommandId(
int commandId, Map<dynamic, dynamic> data) { int commandId, Map<dynamic, dynamic> data) {
switch (commandId) { switch (commandId) {
case BleCommands.GET_BATTERY_INFO:
_updateBatteryInfoFromData(data);
break;
case BleCommands.GET_VERSION: case BleCommands.GET_VERSION:
_updateVersionInfoFromData(data); _updateVersionInfoFromData(data);
break; break;
case BleCommands.GET_PRODUCT_ID:
_updateProductIdFromData(data);
break;
case BleCommands.GET_PRODUCT_COLOR:
break;
case BleCommands.GET_BATTERY_INFO:
_updateBatteryInfoFromData(data);
break;
default: default:
Logger.info('未处理的命令ID: $commandId'); Logger.info('未处理的命令ID: $commandId');
} }
@ -239,6 +260,11 @@ class BleManager extends GetxService {
case 'versionInfo': case 'versionInfo':
_updateVersionInfoFromData(data); _updateVersionInfoFromData(data);
break; break;
case 'productId':
_updateProductIdFromData(data);
break;
case 'productColor':
break;
default: default:
Logger.info('未处理的命令名称: $commandName'); Logger.info('未处理的命令名称: $commandName');
} }
@ -307,6 +333,29 @@ class BleManager extends GetxService {
} }
} }
/// 更新设备id
void _updateProductIdFromData(Map<dynamic, dynamic> data) {
try {
// 确保数据中包含版本信息
if (!data.containsKey('productId')) {
Logger.warning('版本信息数据不完整');
return;
}
// 提取版本信息
final String productId = data['productId'] as String? ?? '';
// 更新设备信息
_updateDeviceInfoIfNeeded(
firmwareProductId: productId,
);
Logger.info('设备设备id已更新: 设备id=$productId');
} catch (e) {
Logger.error('更新设备id失败: $e');
}
}
/// 更新设备信息(如果已存在设备) /// 更新设备信息(如果已存在设备)
void _updateDeviceInfoIfNeeded({ void _updateDeviceInfoIfNeeded({
int? batteryLevel, int? batteryLevel,
@ -317,31 +366,53 @@ class BleManager extends GetxService {
bool? rightEarCharging, bool? rightEarCharging,
bool? caseCharging, bool? caseCharging,
String? firmwareVersion, String? firmwareVersion,
String? firmwareProductId,
String? firmwareProductColor,
}) { }) {
// 如果设备信息已存在,则更新 // 如果设备信息已存在,则更新
if (_deviceInfo.value != null) { if (deviceInfo.value != null) {
_deviceInfo.value = _deviceInfo.value!.copyWith( deviceInfo.value = deviceInfo.value!.copyWith(
batteryLevel: batteryLevel, batteryLevel: batteryLevel ?? deviceInfo.value?.batteryLevel,
leftEarBattery: leftEarBattery, leftEarBattery: leftEarBattery ?? deviceInfo.value?.leftEarBattery,
rightEarBattery: rightEarBattery, rightEarBattery: rightEarBattery ?? deviceInfo.value?.rightEarBattery,
caseBattery: caseBattery, caseBattery: caseBattery ?? deviceInfo.value?.caseBattery,
leftEarCharging: leftEarCharging, leftEarCharging: leftEarCharging ?? deviceInfo.value!.leftEarCharging,
rightEarCharging: rightEarCharging, rightEarCharging:
caseCharging: caseCharging, rightEarCharging ?? deviceInfo.value!.rightEarCharging,
firmwareVersion: firmwareVersion, caseCharging: caseCharging ?? deviceInfo.value!.caseCharging,
firmwareVersion: firmwareVersion ?? deviceInfo.value!.firmwareVersion,
firmwareProductId:
firmwareProductId ?? deviceInfo.value!.firmwareProductId,
firmwareProductColor:
firmwareProductColor ?? deviceInfo.value!.firmwareProductColor,
); );
} }
// 创建设备信息
// 如果设备信息不存在,但有地址和名称,则创建新的设备信息 // 如果设备信息不存在,但有地址和名称,则创建新的设备信息
else if (_deviceInfo.value == null && else if (deviceInfo.value == null &&
_isConnected.value && _isConnected.value &&
(firmwareVersion != null || batteryLevel != null)) { (firmwareVersion != null || batteryLevel != null)) {
// 提示尝试创建设备信息但缺少必要信息 // 提示尝试创建设备信息但缺少必要信息
Logger.warning('尝试更新设备信息,但设备基本信息不存在'); Logger.warning('尝试更新设备信息,但设备基本信息不存在');
deviceInfo.value = DeviceInfo(
address: "", // 应确保从连接信息中获取真实值
name: "",
batteryLevel: batteryLevel ?? 0,
leftEarBattery: leftEarBattery ?? 0,
rightEarBattery: rightEarBattery ?? 0,
caseBattery: caseBattery ?? 0,
leftEarCharging: leftEarCharging ?? false,
rightEarCharging: rightEarCharging ?? false,
caseCharging: caseCharging ?? false,
firmwareVersion: firmwareVersion ?? '',
firmwareProductId: firmwareProductId ?? '',
firmwareProductColor: firmwareProductColor ?? '',
);
} }
} }
/// 连接成功后获取设备信息 /// 连接成功后获取设备信息
Future<void> _fetchDeviceInfo() async { Future<void> fetchDeviceInfo() async {
Logger.info('开始获取BLE设备详细信息'); Logger.info('开始获取BLE设备详细信息');
try { try {
@ -360,6 +431,14 @@ class BleManager extends GetxService {
} else { } else {
Logger.warning('版本信息获取指令发送失败'); Logger.warning('版本信息获取指令发送失败');
} }
// 获取产品id
final productIdResult = await _bleService.getProductId();
if (productIdResult) {
Logger.info('产品id获取指令已发送');
} else {
Logger.warning('产品id获取指令发送失败');
}
} catch (e) { } catch (e) {
Logger.error('获取BLE设备信息失败: $e'); Logger.error('获取BLE设备信息失败: $e');
} }
@ -450,9 +529,9 @@ class BleManager extends GetxService {
_connectionState.value = BleConnectionState.connected; _connectionState.value = BleConnectionState.connected;
// 如果设备信息存在,则获取详细信息 // 如果设备信息存在,则获取详细信息
if (_deviceInfo.value != null) { if (deviceInfo.value != null) {
// 获取详细设备信息 // 获取详细设备信息
_fetchDeviceInfo(); fetchDeviceInfo();
} }
} }
} else { } else {
@ -486,7 +565,6 @@ class BleManager extends GetxService {
} }
} }
/// 获取电池电量 /// 获取电池电量
Future<int> getBatteryLevel() async { Future<int> getBatteryLevel() async {
try { try {
@ -507,6 +585,16 @@ class BleManager extends GetxService {
} }
} }
/// 主动产品id
Future<bool> getProductId() async {
try {
return await _bleService.getProductId();
} catch (e) {
Logger.error('获取设备版本失败: $e');
return false;
}
}
/// 主动获取设备版本信息 /// 主动获取设备版本信息
Future<bool> fetchDeviceVersion() async { Future<bool> fetchDeviceVersion() async {
try { try {

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

@ -622,7 +622,7 @@ class AgentController extends GetxController {
// 播放音乐 // 播放音乐
void musicStatus() async { void musicStatus() async {
var status = await _musiceManager.getStatus(); var status = await _musiceManager.getStatus();
logger.i('获取当前音乐播放状态 $status'); // logger.i('获取当前音乐播放状态 $status');
isPlaying.value = status['isPlaying']; isPlaying.value = status['isPlaying'];
currentUrl.value = status['url']; currentUrl.value = status['url'];
} }

115
lib/modules/agent/views/message_bubble.dart

@ -764,7 +764,7 @@ class MessageBubble extends GetView<AgentController> {
), ),
], ],
), ),
Icon( const Icon(
Icons.wb_sunny, Icons.wb_sunny,
size: 48, size: 48,
color: Colors.white, color: Colors.white,
@ -777,7 +777,7 @@ class MessageBubble extends GetView<AgentController> {
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
), ),
SizedBox(height: 16), SizedBox(height: 16),
Text( const Text(
'未来预报', '未来预报',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
@ -803,11 +803,15 @@ class MessageBubble extends GetView<AgentController> {
(index) { (index) {
String dateStr = days[index]['date']; String dateStr = days[index]['date'];
DateTime? date = DateUtil.getDateTime(dateStr); DateTime? date = DateUtil.getDateTime(dateStr);
String weekday = DateUtil.getWeekday(date); IconData weatherIcon =
getWeatherIcon(days[index]['weatherCondition']);
String weekday = DateUtil.getWeekday(date,
languageCode: "zh", short: true);
String temp = "${days[index]['temperature']}°";
return _buildForecastDay( return _buildForecastDay(
weekday, weekday,
Icons.wb_sunny, weatherIcon,
days[index]['temperature'], temp,
); );
}, },
), ),
@ -915,6 +919,107 @@ class MessageBubble extends GetView<AgentController> {
], ],
); );
} }
/// 获取天气图标
IconData getWeatherIcon(String description) {
description = description.trim();
if (["晴", "少云", "晴间多云"].contains(description)) {
return Icons.wb_sunny;
}
if (["多云", "阴"].contains(description)) {
return Icons.cloud;
}
if (description.contains("雾") ||
["雾", "浓雾", "强浓雾", "轻雾", "大雾", "特强浓雾", "霾", "中度霾", "重度霾", "严重霾"]
.contains(description)) {
return Icons.blur_on;
}
if (description.contains("风") ||
[
"有风",
"平静",
"微风",
"和风",
"清风",
"强风/劲风",
"疾风",
"大风",
"烈风",
"风暴",
"狂爆风",
"飓风",
"热带风暴"
].contains(description)) {
return Icons.air;
}
if (description.contains("雨") ||
[
"阵雨",
"雷阵雨",
"雷阵雨并伴有冰雹",
"小雨",
"中雨",
"大雨",
"暴雨",
"大暴雨",
"特大暴雨",
"强阵雨",
"强雷阵雨",
"极端降雨",
"毛毛雨/细雨",
"雨",
"小雨-中雨",
"中雨-大雨",
"大雨-暴雨",
"暴雨-大暴雨",
"大暴雨-特大暴雨"
].contains(description)) {
return Icons.grain; // 或使用 community icon 包:weather_icons 中的 rain
}
if (description.contains("雪") ||
[
"雨雪天气",
"雨夹雪",
"阵雨夹雪",
"冻雨",
"雪",
"阵雪",
"小雪",
"中雪",
"大雪",
"暴雪",
"小雪-中雪",
"中雪-大雪",
"大雪-暴雪"
].contains(description)) {
return Icons.ac_unit;
}
if (["浮尘", "扬沙", "沙尘暴", "强沙尘暴"].contains(description)) {
return Icons.filter_drama;
}
if (["龙卷风"].contains(description)) {
return Icons.waves;
}
if (["热"].contains(description)) {
return Icons.local_fire_department;
}
if (["冷"].contains(description)) {
return Icons.ac_unit;
}
// 默认或未知天气
return Icons.help_outline;
}
} }
/// 动画点组件 /// 动画点组件

156
lib/modules/home/controllers/home_controller.dart

@ -19,7 +19,7 @@ enum ConnectionStatus {
/// 主页控制器,管理设备连接状态和信息展示 /// 主页控制器,管理设备连接状态和信息展示
class HomeController extends GetxController { class HomeController extends GetxController {
static HomeController get to => Get.find(); static HomeController get to => Get.find();
// 依赖的控制器 // 依赖的控制器
final ExploreController exploreController = Get.find<ExploreController>(); final ExploreController exploreController = Get.find<ExploreController>();
@ -27,31 +27,47 @@ class HomeController extends GetxController {
final _connectionStatus = ConnectionStatus.disconnected.obs; final _connectionStatus = ConnectionStatus.disconnected.obs;
final _connectionErrorMsg = ''.obs; final _connectionErrorMsg = ''.obs;
final _bluetoothState = 'unknown'.obs; final _bluetoothState = 'unknown'.obs;
// Getter方法 // Getter方法
ConnectionStatus get connectionStatus => _connectionStatus.value; ConnectionStatus get connectionStatus => _connectionStatus.value;
String get connectionErrorMsg => _connectionErrorMsg.value; String get connectionErrorMsg => _connectionErrorMsg.value;
String get bluetoothState => _bluetoothState.value; String get bluetoothState => _bluetoothState.value;
BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice; BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice;
DeviceInfo? get deviceInfo => _bleManager.deviceInfo; Rx<DeviceInfo?> get deviceInfo => _bleManager.deviceInfo;
bool get isConnected => currentDevice != null; bool get isConnected => currentDevice != null;
// 设备详细信息的getter,从deviceInfo获取数据 // 设备详细信息的getter,从deviceInfo获取数据
int get leftEarBattery => deviceInfo?.leftEarBattery ?? 0; final leftEarBattery = 0.obs;
int get rightEarBattery => deviceInfo?.rightEarBattery ?? 0; final rightEarBattery = 0.obs;
int get caseBattery => deviceInfo?.caseBattery ?? 0; final caseBattery = 0.obs;
bool get leftEarCharging => deviceInfo?.leftEarCharging ?? false; final leftEarCharging = false.obs;
bool get rightEarCharging => deviceInfo?.rightEarCharging ?? false; final rightEarCharging = false.obs;
bool get caseCharging => deviceInfo?.caseCharging ?? false; final caseCharging = false.obs;
String get firmwareVersion => deviceInfo?.firmwareVersion ?? ''; final firmwareVersion = ''.obs;
// int get leftEarBattery => deviceInfo?.leftEarBattery ?? 0;
// int get rightEarBattery => deviceInfo?.rightEarBattery ?? 0;
// int get caseBattery => deviceInfo?.caseBattery ?? 0;
// bool get leftEarCharging => deviceInfo?.leftEarCharging ?? false;
// bool get rightEarCharging => deviceInfo?.rightEarCharging ?? false;
// bool get caseCharging => deviceInfo?.caseCharging ?? false;
// String get firmwareVersion => deviceInfo?.firmwareVersion ?? '';
/// 当有新的deviceInfo时更新所有Observable变量
void updateDeviceInfo(DeviceInfo? newDeviceInfo) {
leftEarBattery.value = newDeviceInfo?.leftEarBattery ?? 0;
rightEarBattery.value = newDeviceInfo?.rightEarBattery ?? 0;
caseBattery.value = newDeviceInfo?.caseBattery ?? 0;
leftEarCharging.value = newDeviceInfo?.leftEarCharging ?? false;
rightEarCharging.value = newDeviceInfo?.rightEarCharging ?? false;
caseCharging.value = newDeviceInfo?.caseCharging ?? false;
firmwareVersion.value = newDeviceInfo?.firmwareVersion ?? '';
}
// 监听和订阅 // 监听和订阅
Worker? _deviceInfoWorker; Worker? _deviceInfoWorker;
Worker? _bluetoothStateWorker; Worker? _bluetoothStateWorker;
Worker? _deviceWorker; Worker? _deviceWorker;
// 蓝牙服务 // 蓝牙服务
final _bluetoothManager = Get.find<BluetoothManager>(); final _bluetoothManager = Get.find<BluetoothManager>();
final _bleManager = Get.find<BleManager>(); final _bleManager = Get.find<BleManager>();
@ -60,7 +76,7 @@ class HomeController extends GetxController {
super.onInit(); super.onInit();
_initWithCachedInfo(); _initWithCachedInfo();
} }
@override @override
void onClose() { void onClose() {
_deviceInfoWorker?.dispose(); _deviceInfoWorker?.dispose();
@ -73,11 +89,12 @@ class HomeController extends GetxController {
void _initWithCachedInfo() { void _initWithCachedInfo() {
// 使用蓝牙服务中的当前状态 // 使用蓝牙服务中的当前状态
_bluetoothState.value = _bluetoothManager.bluetoothState; _bluetoothState.value = _bluetoothManager.bluetoothState;
// 根据当前设备状态设置连接状态 // 根据当前设备状态设置连接状态
if (currentDevice != null) { if (currentDevice != null) {
_connectionStatus.value = ConnectionStatus.connected; _connectionStatus.value = ConnectionStatus.connected;
_connectionErrorMsg.value = ''; _connectionErrorMsg.value = '';
updateDeviceInfo(_bleManager.deviceInfo.value);
Logger.info('主页加载缓存设备信息: ${currentDevice!.name}'); Logger.info('主页加载缓存设备信息: ${currentDevice!.name}');
} else { } else {
_connectionStatus.value = ConnectionStatus.disconnected; _connectionStatus.value = ConnectionStatus.disconnected;
@ -87,67 +104,73 @@ class HomeController extends GetxController {
_connectionErrorMsg.value = '未连接到设备'; _connectionErrorMsg.value = '未连接到设备';
} }
} }
// 设置监听器监听蓝牙服务状态变化 // 设置监听器监听蓝牙服务状态变化
_setupStateListeners(); _setupStateListeners();
} }
/// 设置状态监听器 /// 设置状态监听器
void _setupStateListeners() { void _setupStateListeners() {
// 设备信息监听 - 设备信息更新时自动触发UI更新 // 设备信息监听 - 设备信息更新时自动触发UI更新
_deviceInfoWorker = ever(_bleManager.deviceInfo.obs, (DeviceInfo? info) { _deviceInfoWorker = ever(_bleManager.deviceInfo, (DeviceInfo? info) {
Logger.info('设备信息监听');
if (info != null) { if (info != null) {
Logger.info('主页更新设备信息: ${info.toString()}'); Logger.info('主页更新设备信息: ${info.toString()}');
updateDeviceInfo(info);
} else { } else {
Logger.info('主页清空设备信息'); Logger.info('主页清空设备信息');
} }
update(); update();
}); });
// 蓝牙状态监听 // 蓝牙状态监听
_bluetoothStateWorker = ever(_bluetoothManager.bluetoothState.obs, (String state) { _bluetoothStateWorker =
ever(_bluetoothManager.bluetoothState.obs, (String state) {
_bluetoothState.value = state; _bluetoothState.value = state;
// 如果蓝牙关闭,更新错误信息 // 如果蓝牙关闭,更新错误信息
if (state == 'off') { if (state == 'off') {
_connectionErrorMsg.value = '蓝牙未开启'; _connectionErrorMsg.value = '蓝牙未开启';
_connectionStatus.value = ConnectionStatus.disconnected; _connectionStatus.value = ConnectionStatus.disconnected;
} }
update(); update();
}); });
// 设备连接状态监听 - 直接监听设备变化 // 设备连接状态监听 - 直接监听设备变化
_deviceWorker = ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) { _deviceWorker =
ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) {
if (device != null) { if (device != null) {
// 设备已连接 // 设备已连接
if (_connectionStatus.value != ConnectionStatus.connected) { if (_connectionStatus.value != ConnectionStatus.connected) {
_connectionStatus.value = ConnectionStatus.connected; _connectionStatus.value = ConnectionStatus.connected;
_connectionErrorMsg.value = ''; _connectionErrorMsg.value = '';
Logger.info('主页检测到设备已连接: ${device.name}'); Logger.info('主页检测到设备已连接: ${device.name}');
} }
} else if (_connectionStatus.value == ConnectionStatus.connected) { } else if (_connectionStatus.value == ConnectionStatus.connected) {
// 设备已断开 // 设备已断开
_connectionStatus.value = ConnectionStatus.disconnected; _connectionStatus.value = ConnectionStatus.disconnected;
_connectionErrorMsg.value = '设备已断开连接'; _connectionErrorMsg.value = '设备已断开连接';
Logger.info('主页检测到设备已断开连接'); Logger.info('主页检测到设备已断开连接');
} }
update(); update();
}); });
} }
/// 刷新设备连接状态 /// 刷新设备连接状态
/// 注意:此方法不会主动刷新设备,只更新UI状态 /// 注意:此方法不会主动刷新设备,只更新UI状态
Future<void> refreshDeviceConnection() async { Future<void> refreshDeviceConnection() async {
// 更新当前状态以反映UI变化 // 更新当前状态以反映UI变化
bool hasDevice = currentDevice != null; bool hasDevice = currentDevice != null;
if (hasDevice) { if (hasDevice) {
_connectionStatus.value = ConnectionStatus.connected; _connectionStatus.value = ConnectionStatus.connected;
_connectionErrorMsg.value = ''; _connectionErrorMsg.value = '';
await _bleManager.fetchDeviceInfo();
Logger.info('使用缓存设备信息: ${currentDevice!.name}'); Logger.info('使用缓存设备信息: ${currentDevice!.name}');
} else { } else {
if (_bluetoothState.value == 'off') { if (_bluetoothState.value == 'off') {
@ -157,13 +180,13 @@ class HomeController extends GetxController {
} }
_connectionStatus.value = ConnectionStatus.disconnected; _connectionStatus.value = ConnectionStatus.disconnected;
} }
// 刷新UI // 刷新UI
update(); update();
} }
// 导航和功能方法 // 导航和功能方法
/// 重新扫描设备 /// 重新扫描设备
void rescanDevices() { void rescanDevices() {
Get.toNamed(Routes.pairing); Get.toNamed(Routes.pairing);
@ -185,7 +208,7 @@ class HomeController extends GetxController {
} }
// UI辅助方法 // UI辅助方法
/// 获取连接状态对应的颜色 /// 获取连接状态对应的颜色
Color getConnectionStatusColor() { Color getConnectionStatusColor() {
switch (_connectionStatus.value) { switch (_connectionStatus.value) {
@ -244,62 +267,71 @@ class HomeController extends GetxController {
/// 获取设备ID /// 获取设备ID
String getDeviceId() { String getDeviceId() {
return deviceInfo?.address ?? currentDevice?.address ?? 'unknown'.tr; return deviceInfo.value?.address ?? currentDevice?.address ?? 'unknown'.tr;
} }
/// 获取设备电量文本 /// 获取设备电量文本
String getBatteryText() { String getBatteryText() {
return deviceInfo?.batteryLevel != null && deviceInfo!.batteryLevel > 0 return deviceInfo.value?.batteryLevel != null &&
? '${deviceInfo!.batteryLevel}%' deviceInfo.value!.batteryLevel > 0
: 'unknown'.tr; ? '${deviceInfo.value?.batteryLevel}%'
: 'unknown'.tr;
} }
/// 获取耳机左右电量文本 /// 获取耳机左右电量文本
String getEarbudsText() { String getEarbudsText() {
if (deviceInfo == null || (deviceInfo!.leftEarBattery <= 0 && deviceInfo!.rightEarBattery <= 0)) { if (deviceInfo == null ||
(deviceInfo.value!.leftEarBattery <= 0 &&
deviceInfo.value!.rightEarBattery <= 0)) {
return 'unknown'.tr; return 'unknown'.tr;
} }
String leftStatus = deviceInfo!.leftEarBattery > 0 ? '${deviceInfo!.leftEarBattery}%' : 'N/A'; String leftStatus = deviceInfo.value!.leftEarBattery > 0
String rightStatus = deviceInfo!.rightEarBattery > 0 ? '${deviceInfo!.rightEarBattery}%' : 'N/A'; ? '${deviceInfo.value!.leftEarBattery}%'
: 'N/A';
leftStatus = deviceInfo!.leftEarCharging ? '$leftStatus ⚡' : leftStatus; String rightStatus = deviceInfo.value!.rightEarBattery > 0
rightStatus = deviceInfo!.rightEarCharging ? '$rightStatus ⚡' : rightStatus; ? '${deviceInfo.value!.rightEarBattery}%'
: 'N/A';
leftStatus =
deviceInfo.value!.leftEarCharging ? '$leftStatus ⚡' : leftStatus;
rightStatus =
deviceInfo.value!.rightEarCharging ? '$rightStatus ⚡' : rightStatus;
return '左耳: $leftStatus / 右耳: $rightStatus'; return '左耳: $leftStatus / 右耳: $rightStatus';
} }
/// 获取充电仓电量文本 /// 获取充电仓电量文本
String getCaseText() { String getCaseText() {
if (deviceInfo == null || deviceInfo!.caseBattery <= 0) { if (deviceInfo.value == null || deviceInfo.value!.caseBattery <= 0) {
return 'unknown'.tr; return 'unknown'.tr;
} }
String caseStatus = '${deviceInfo!.caseBattery}%'; String caseStatus = '${deviceInfo.value!.caseBattery}%';
if (deviceInfo!.caseCharging) { if (deviceInfo.value!.caseCharging) {
caseStatus = '$caseStatus ⚡'; caseStatus = '$caseStatus ⚡';
} }
return '充电仓: $caseStatus'; return '充电仓: $caseStatus';
} }
/// 获取充电状态文本 /// 获取充电状态文本
String getChargingText() { String getChargingText() {
if (deviceInfo == null) { if (deviceInfo == null) {
return 'unknown'.tr; return 'unknown'.tr;
} }
bool isCharging = deviceInfo!.leftEarCharging || bool isCharging = deviceInfo.value!.leftEarCharging ||
deviceInfo!.rightEarCharging || deviceInfo.value!.rightEarCharging ||
deviceInfo!.caseCharging; deviceInfo.value!.caseCharging;
return isCharging ? 'charging'.tr : 'notCharging'.tr; return isCharging ? 'charging'.tr : 'notCharging'.tr;
} }
/// 获取设备版本文本 /// 获取设备版本文本
String getVersionText() { String getVersionText() {
return deviceInfo?.firmwareVersion.isNotEmpty == true ? return deviceInfo.value?.firmwareVersion.isNotEmpty == true
deviceInfo!.firmwareVersion : 'unknown'.tr; ? deviceInfo.value!.firmwareVersion
: 'unknown'.tr;
} }
} }

182
lib/modules/home/views/home_view.dart

@ -160,43 +160,43 @@ class HomeView extends GetView<HomeController> {
child: ClipRRect( child: ClipRRect(
// 添加 ClipRRect 来实现圆角 // 添加 ClipRRect 来实现圆角
borderRadius: BorderRadius.circular(16.r), // 使用与其他部件一致的圆角大小 borderRadius: BorderRadius.circular(16.r), // 使用与其他部件一致的圆角大小
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
height: 200.h, height: 200.h,
child: Image.asset( child: Image.asset(
isDarkMode isDarkMode
? 'assets/images/robot_light.png' ? 'assets/images/robot_light.png'
: 'assets/images/robot_light.png', : 'assets/images/robot_light.png',
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
),
), ),
), ),
), ),
),
Center( Center(
child: ClipRRect( child: ClipRRect(
// 添加 ClipRRect // 添加 ClipRRect
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
child: Image.asset( child: Image.asset(
isDarkMode isDarkMode
? 'assets/images/headphone_dark.png' ? 'assets/images/headphone_dark.png'
: 'assets/images/headphone_light.png', : 'assets/images/headphone_light.png',
height: 180.h, height: 180.h,
fit: BoxFit.contain, fit: BoxFit.contain,
),
), ),
), ),
),
Center( Center(
child: ClipRRect( child: ClipRRect(
// 添加 ClipRRect // 添加 ClipRRect
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
height: 200.h, height: 200.h,
child: Image.asset( child: Image.asset(
isDarkMode isDarkMode
? 'assets/images/glasses_light.png' ? 'assets/images/glasses_light.png'
: 'assets/images/glasses_light.png', : 'assets/images/glasses_light.png',
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
), ),
), ),
), ),
@ -283,50 +283,52 @@ class HomeView extends GetView<HomeController> {
], ],
), ),
Obx(() => Row( Obx(() => Row(
children: [ children: [
Container( Container(
width: 8.w, width: 8.w,
height: 8.w, height: 8.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: controller.getConnectionStatusColor(), color: controller.getConnectionStatusColor(),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: controller.getConnectionStatusColor().withOpacity(0.3), color: controller
blurRadius: 4, .getConnectionStatusColor()
spreadRadius: 1, .withOpacity(0.3),
blurRadius: 4,
spreadRadius: 1,
),
],
), ),
],
),
),
SizedBox(width: 4.w),
Text(
controller.getConnectionStatusText(),
style: TextStyle(
fontSize: 12.sp,
color: controller.getConnectionStatusColor(),
),
),
// 添加刷新按钮
SizedBox(width: 8.w),
InkWell(
onTap: () => controller.refreshDeviceConnection(),
borderRadius: BorderRadius.circular(12.r),
child: Container(
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
shape: BoxShape.circle,
), ),
child: Icon( SizedBox(width: 4.w),
Icons.refresh, Text(
size: 14.sp, controller.getConnectionStatusText(),
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], style: TextStyle(
fontSize: 12.sp,
color: controller.getConnectionStatusColor(),
),
), ),
), // 添加刷新按钮
), SizedBox(width: 8.w),
], InkWell(
)), onTap: () => controller.refreshDeviceConnection(),
borderRadius: BorderRadius.circular(12.r),
child: Container(
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
shape: BoxShape.circle,
),
child: Icon(
Icons.refresh,
size: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
],
)),
], ],
); );
} }
@ -338,9 +340,8 @@ class HomeView extends GetView<HomeController> {
Container( Container(
padding: EdgeInsets.all(12.w), padding: EdgeInsets.all(12.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color:
? Colors.red[900]!.withOpacity(0.2) isDarkMode ? Colors.red[900]!.withOpacity(0.2) : Colors.red[50],
: Colors.red[50],
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
border: Border.all( border: Border.all(
color: isDarkMode ? Colors.red[800]! : Colors.red[200]!), color: isDarkMode ? Colors.red[800]! : Colors.red[200]!),
@ -440,12 +441,12 @@ class HomeView extends GetView<HomeController> {
final leftBattery = controller.leftEarBattery; final leftBattery = controller.leftEarBattery;
final rightBattery = controller.rightEarBattery; final rightBattery = controller.rightEarBattery;
final caseBattery = controller.caseBattery; final caseBattery = controller.caseBattery;
// 获取充电状态 // 获取充电状态
final leftEarCharging = controller.leftEarCharging; final leftEarCharging = controller.leftEarCharging;
final rightEarCharging = controller.rightEarCharging; final rightEarCharging = controller.rightEarCharging;
final caseCharging = controller.caseCharging; final caseCharging = controller.caseCharging;
return GridView.count( return GridView.count(
crossAxisCount: 3, crossAxisCount: 3,
shrinkWrap: true, shrinkWrap: true,
@ -457,33 +458,33 @@ class HomeView extends GetView<HomeController> {
_buildBatteryGridItem( _buildBatteryGridItem(
icon: Icons.headphones, icon: Icons.headphones,
label: '左耳机', label: '左耳机',
batteryLevel: leftBattery, batteryLevel: leftBattery.value,
isCharging: leftEarCharging, isCharging: leftEarCharging.value,
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
// 右耳机 // 右耳机
_buildBatteryGridItem( _buildBatteryGridItem(
icon: Icons.headphones, icon: Icons.headphones,
label: '右耳机', label: '右耳机',
batteryLevel: rightBattery, batteryLevel: rightBattery.value,
isCharging: rightEarCharging, isCharging: rightEarCharging.value,
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
// 充电盒 // 充电盒
_buildBatteryGridItem( _buildBatteryGridItem(
icon: Icons.charging_station, icon: Icons.charging_station,
label: '充电盒', label: '充电盒',
batteryLevel: caseBattery, batteryLevel: caseBattery.value,
isCharging: caseCharging, isCharging: caseCharging.value,
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
], ],
); );
}); });
} }
// 单个电池网格项 // 单个电池网格项
Widget _buildBatteryGridItem({ Widget _buildBatteryGridItem({
required IconData icon, required IconData icon,
@ -495,7 +496,7 @@ class HomeView extends GetView<HomeController> {
// 获取电池图标和颜色 // 获取电池图标和颜色
IconData batteryIcon; IconData batteryIcon;
Color batteryColor; Color batteryColor;
if (batteryLevel > 75) { if (batteryLevel > 75) {
batteryIcon = Icons.battery_full; batteryIcon = Icons.battery_full;
batteryColor = Colors.green[600]!; batteryColor = Colors.green[600]!;
@ -512,16 +513,16 @@ class HomeView extends GetView<HomeController> {
batteryIcon = Icons.battery_alert; batteryIcon = Icons.battery_alert;
batteryColor = Colors.red[600]!; batteryColor = Colors.red[600]!;
} }
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100], color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: isDarkMode ? color: isDarkMode
Colors.black.withOpacity(0.1) : ? Colors.black.withOpacity(0.1)
Colors.grey.withOpacity(0.1), : Colors.grey.withOpacity(0.1),
blurRadius: 4, blurRadius: 4,
spreadRadius: 0, spreadRadius: 0,
offset: Offset(0, 2), offset: Offset(0, 2),
@ -545,7 +546,7 @@ class HomeView extends GetView<HomeController> {
), ),
), ),
SizedBox(height: 6.h), SizedBox(height: 6.h),
// 标签 // 标签
Text( Text(
label, label,
@ -557,7 +558,7 @@ class HomeView extends GetView<HomeController> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: 6.h), SizedBox(height: 6.h),
// 电池指示器 // 电池指示器
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -614,16 +615,15 @@ class HomeView extends GetView<HomeController> {
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
// 固件版本 // 固件版本
_buildInfoRow( _buildInfoRow(
icon: Icons.info_outline, icon: Icons.info_outline,
label: 'firmwareVersion'.tr, label: 'firmwareVersion'.tr,
value: controller.firmwareVersion, value: controller.firmwareVersion.value,
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
// 连接状态标签 // 连接状态标签
SizedBox(height: 10.h), SizedBox(height: 10.h),
Container( Container(
@ -763,7 +763,7 @@ class HomeView extends GetView<HomeController> {
), ),
); );
} }
// 热门AI体验标题 // 热门AI体验标题
Widget _buildSectionTitle(bool isDarkMode) { Widget _buildSectionTitle(bool isDarkMode) {
return Row( return Row(

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

@ -1,19 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path/path.dart';
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import '../../../data/services/network/api.dart'; import '../../../data/services/network/api.dart';
import '../ota.dart'; import '../model/ota_upgrade_model.dart';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:json_annotation/json_annotation.dart'; import '../../../data/services/ble_manager.dart';
class OtaUpgradeController extends GetxController { class OtaUpgradeController extends GetxController {
final RxString errorMessage = ''.obs;
final RxBool isFetchingDeviceInfo = false.obs;
final Dio _dio = Dio(); final Dio _dio = Dio();
final RxString latestVersion = ''.obs; final RxString latestVersion = ''.obs;
final RxString currentVersion = ''.obs; final RxString currentVersion = ''.obs;
@ -24,41 +22,93 @@ class OtaUpgradeController extends GetxController {
final RxDouble progress = 0.0.obs; final RxDouble progress = 0.0.obs;
// 蓝牙服务
final _bleManager = Get.find<BleManager>();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_getCurrentVersion(); _getCurrentVersion();
} }
// 获取当前版本信息 // 添加重试方法
void retryConnection() {
errorMessage.value = '';
_getCurrentVersion();
}
// 添加更新方法
Future<void> startUpdate() async {
try {
isUpdating.value = true;
// 实现更新逻辑...
} catch (e) {
errorMessage.value = e.toString();
Get.snackbar('更新失败', e.toString());
} finally {
isUpdating.value = false;
}
}
Future<void> _getCurrentVersion() async { Future<void> _getCurrentVersion() async {
//发送ble获取固件版本 try {
var firmwareVersion = "1.1.0"; // 添加获取设备信息的重试机制
currentVersion.value = firmwareVersion; DeviceInfo? deviceInfo = _bleManager.deviceInfo.value;
//发送ble获取产品id
var id = "a2"; // 第一次检查设备信息
//发送api请求 if (deviceInfo == null) {
final response = await Api.getequipmentinfo({ // 显示加载提示
"id": id, isFetchingDeviceInfo.value = true;
});
if (response['info'] != '') { // 尝试重新获取设备信息
print('getLoginToken:进来了'); await _bleManager.fetchDeviceInfo();
// 根据API设计调整
final resp = OtaConfigResp.fromJson(response); // 等待后再次获取设备信息
deviceInfo = _bleManager.deviceInfo.value;
isFetchingDeviceInfo.value = false;
// 仍然为空则抛出异常
if (deviceInfo == null) {
throw const FormatException('无法获取设备信息,请检查设备连接');
}
}
// 检查固件信息有效性
if (deviceInfo.firmwareVersion.isEmpty
//||
// deviceInfo.firmwareProductId.isEmpty
) {
throw const FormatException('设备固件信息不完整');
}
// 正常流程
currentVersion.value = deviceInfo.firmwareVersion;
print('成功解析: ${resp.id}, ${resp.factory}'); // API请求
final response = await Api.getequipmentinfo({
"id": "a2",
});
// 处理API响应
if (response['info'] == null) {
throw const FormatException('API 响应格式错误');
}
final resp = OtaConfigResp.fromJson(response);
latestVersion.value = resp.systemversion!; latestVersion.value = resp.systemversion!;
updatepackageaddress = resp.updatepackageaddress!; updatepackageaddress = resp.updatepackageaddress!;
if (_isNewVersionAvailable(resp.systemversion!)) {
//需要升级 // 版本检查
print('需要更新'); isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!);
isUpdateAvailable.value = false; } catch (e) {
} else { // 统一错误处理
isUpdateAvailable.value = true; errorMessage.value = e.toString();
//已经是最新版本更新ui,按钮隐藏 Get.snackbar('错误', e.toString(),
} snackPosition: SnackPosition.BOTTOM,
} else { backgroundColor: Colors.red,
throw const FormatException('API 响应缺少 "info" 字段'); colorText: Colors.white);
// 重要:重新抛出异常以便上层处理(可选)
} finally {
isFetchingDeviceInfo.value = false;
} }
} }
@ -120,13 +170,10 @@ class OtaUpgradeController extends GetxController {
); );
} }
// 开始下载 // 修改后的startDownload方法
Future<void> startDownload() async { Future<void> startDownload() async {
isUpdating.value = true; isUpdating.value = true;
// 查找最后一个 '/' 的位置
int lastIndex = updatepackageaddress.lastIndexOf('/'); int lastIndex = updatepackageaddress.lastIndexOf('/');
// 截取最后一个 '/' 后面的字符串
String fileName = updatepackageaddress.substring(lastIndex + 1); String fileName = updatepackageaddress.substring(lastIndex + 1);
String directoryName = "ota_upgrades"; String directoryName = "ota_upgrades";
@ -145,14 +192,54 @@ class OtaUpgradeController extends GetxController {
throw FileSystemException('无法访问下载目录'); throw FileSystemException('无法访问下载目录');
} }
// 创建子目录:Download/APP
final targetDir = Directory('${downloadsDir.path}/$directoryName'); final targetDir = Directory('${downloadsDir.path}/$directoryName');
if (!await targetDir.exists()) { if (!await targetDir.exists()) {
await targetDir.create(recursive: true); await targetDir.create(recursive: true);
} }
final savePath = '${targetDir.path}/$fileName'; final savePath = '${targetDir.path}/$fileName';
print('文件保存路径: $savePath'); // 关键日志
// 新增:检查同名文件逻辑
bool existingSameFile = false;
try {
var files = await targetDir.list().toList();
for (var entity in files) {
if (await FileSystemEntity.isFile(entity.path)) {
String name = basename(entity.path); // 使用path包中的basename
if (name == fileName) {
existingSameFile = true;
break;
}
}
}
} catch (e) {
Get.snackbar('错误', '检查本地文件失败: $e');
isUpdating.value = false;
return;
}
if (existingSameFile) {
Get.snackbar('文件已存在', '跳过下载,直接更新');
isUpdating.value = false;
_installApp(savePath);
return;
}
// 新增:删除其他文件逻辑
try {
var files = await targetDir.list().toList();
for (var entity in files) {
if (await FileSystemEntity.isFile(entity.path)) {
await entity.delete();
}
}
} catch (e) {
Get.snackbar('错误', '删除旧文件失败: $e');
isUpdating.value = false;
return;
}
print('文件保存路径: $savePath');
final options = Options( final options = Options(
receiveTimeout: Duration.zero, receiveTimeout: Duration.zero,
@ -181,22 +268,23 @@ class OtaUpgradeController extends GetxController {
}, },
); );
// 下载完成后验证文件存在性
File file = File(savePath); File file = File(savePath);
if (await file.exists()) { if (await file.exists()) {
Get.snackbar('下载完成', '文件已保存至: $savePath'); // 明确提示路径 Get.snackbar('下载完成', '文件已保存至: $savePath');
} else { } else {
Get.snackbar('错误', '文件未找到,请检查写入权限'); Get.snackbar('错误', '文件未找到,请检查写入权限');
} }
isUpdating.value = false; isUpdating.value = false;
//下载完成,开始升级
_installApp(savePath); _installApp(savePath);
} on DioException catch (e) { } on DioException catch (e) {
Get.snackbar('下载失败', '错误: ${e.message}'); Get.snackbar('下载失败', '错误: ${e.message}');
isUpdating.value = false;
} on FileSystemException catch (e) { } on FileSystemException catch (e) {
Get.snackbar('存储失败', '无法写入文件: ${e.message}'); Get.snackbar('存储失败', '无法写入文件: ${e.message}');
isUpdating.value = false;
} catch (e) { } catch (e) {
Get.snackbar('未知错误', e.toString()); Get.snackbar('未知错误', e.toString());
isUpdating.value = false;
} }
} }

0
lib/modules/ota_upgrade/ota.dart → lib/modules/ota_upgrade/model/ota_upgrade_model.dart

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

@ -4,106 +4,126 @@ import '../controllers/ota_upgrade_controller.dart';
class OtaUpgradeView extends GetView<OtaUpgradeController> { class OtaUpgradeView extends GetView<OtaUpgradeController> {
const OtaUpgradeView({Key? key}) : super(key: key); const OtaUpgradeView({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Center( body: Center(
// 添加外层Center child: ConstrainedBox(
child: SingleChildScrollView( constraints: BoxConstraints(maxWidth: 600), // 限制最大宽度
// 添加滚动防止小屏溢出 child: SingleChildScrollView(
child: Padding( child: Padding(
padding: EdgeInsets.all(20), padding: EdgeInsets.all(20),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, // 水平居中 children: [
mainAxisSize: MainAxisSize.min, // 紧凑布局 // 设备图标
children: [ _buildDeviceIcon(),
// 设备图标 SizedBox(height: 30),
CircleAvatar(
radius: 60,
backgroundColor: Colors.blue[100],
child: Icon(
Icons.phone_iphone,
size: 60,
color: Colors.blue,
),
),
SizedBox(height: 30),
// 版本信息 // 版本信息
Column( _buildVersionInfo(),
children: [ SizedBox(height: 40),
Text(
'当前版本',
style: TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
SizedBox(height: 8),
Text(
'v${controller.currentVersion}',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
SizedBox(height: 40),
// 更新按钮/进度条 // 状态显示区域
Obx(() { Obx(() {
if (controller.isUpdating.value) { if (controller.isFetchingDeviceInfo.value) {
return Column( return _buildLoadingIndicator();
children: [ }
SizedBox( if (controller.errorMessage.value.isNotEmpty) {
width: 200, // 限制进度条宽度 return _buildErrorInfo();
child: LinearProgressIndicator( }
value: controller.downloadProgress.value, return _buildUpdateButton();
backgroundColor: Colors.grey[200], }),
valueColor: ],
AlwaysStoppedAnimation<Color>(Colors.blue), ),
minHeight: 12,
borderRadius: BorderRadius.circular(10),
),
),
SizedBox(height: 20),
Text(
'${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%',
style: TextStyle(color: Colors.grey),
)
],
);
} else {
return ElevatedButton(
onPressed: controller.isUpdateAvailable.value
? null
: controller.checkUpdate,
style: ElevatedButton.styleFrom(
backgroundColor: controller.isUpdateAvailable.value
? Colors.grey
: Colors.blue,
foregroundColor: Colors.white,
padding:
EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Text(
controller.isUpdateAvailable.value ? '已是最新版本' : '立即更新',
style: TextStyle(fontSize: 16),
),
);
}
}),
],
), ),
), ),
), ),
), ),
); );
} }
// 组件化构建方法
Widget _buildDeviceIcon() => CircleAvatar(
radius: 60,
backgroundColor: Colors.blue[100],
child: Icon(Icons.phone_iphone, size: 60, color: Colors.blue),
);
Widget _buildVersionInfo() => Column(
children: [
Text('当前版本', style: TextStyle(fontSize: 18, color: Colors.grey)),
SizedBox(height: 8),
Text('v${controller.currentVersion}',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue)),
],
);
Widget _buildLoadingIndicator() => Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 20),
Text('正在连接设备...', style: TextStyle(color: Colors.grey))
],
);
Widget _buildErrorInfo() => Column(
children: [
Icon(Icons.error_outline, color: Colors.red, size: 40),
SizedBox(height: 10),
Text(controller.errorMessage.value,
style: TextStyle(color: Colors.red)),
SizedBox(height: 20),
ElevatedButton(
onPressed: controller.retryConnection,
child: Text('重试连接'),
)
],
);
Widget _buildUpdateButton() {
return controller.isUpdating.value
? _buildProgressIndicator()
: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: controller.isUpdateAvailable.value
? Colors.blue
: Colors.grey,
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
onPressed: controller.isUpdateAvailable.value
? controller.startUpdate
: null,
child: Text(
controller.isUpdateAvailable.value ? '立即更新' : '已是最新版本',
style: TextStyle(fontSize: 16, color: Colors.white),
),
);
}
Widget _buildProgressIndicator() => Column(
children: [
SizedBox(
width: 200,
child: LinearProgressIndicator(
value: controller.downloadProgress.value,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
minHeight: 12,
borderRadius: BorderRadius.circular(10),
),
),
SizedBox(height: 20),
Text(
'${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%',
style: TextStyle(color: Colors.grey))
],
);
} }

138
lib/modules/pairing/controllers/pairing_controller.dart

@ -10,13 +10,16 @@ import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
import 'package:agent_service/agent_service.dart'; import 'package:agent_service/agent_service.dart';
import 'package:ble_service/ble_service.dart'; import 'package:ble_service/ble_service.dart';
/// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程 /// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程
class PairingController extends GetxController { class PairingController extends GetxController {
static PairingController get to => Get.find(); static PairingController get to => Get.find();
//是否需要连接耳机
final isneedearphone = false;
// 状态标志 // 状态标志
final isLoading = false.obs; final isLoading = false.obs;
// 权限是否已授予 // 权限是否已授予
final _hasPermissions = false.obs; final _hasPermissions = false.obs;
bool get hasPermissions => _hasPermissions.value; bool get hasPermissions => _hasPermissions.value;
@ -25,22 +28,22 @@ class PairingController extends GetxController {
final _bluetoothState = 'unknown'.obs; final _bluetoothState = 'unknown'.obs;
String get bluetoothState => _bluetoothState.value; String get bluetoothState => _bluetoothState.value;
bool get isBluetoothOn => _bluetoothState.value == 'on'; bool get isBluetoothOn => _bluetoothState.value == 'on';
// 状态描述(用于UI显示) // 状态描述(用于UI显示)
final statusDescription = 'initializing'.tr.obs; final statusDescription = 'initializing'.tr.obs;
// 是否正在跳转到主页 // 是否正在跳转到主页
bool _isNavigatingToHome = false; bool _isNavigatingToHome = false;
// 是否正在执行Companion关联流程 // 是否正在执行Companion关联流程
bool _isAssociating = false; bool _isAssociating = false;
// 当前连接设备信息 // 当前连接设备信息
BluetoothDevice? get currentDevice => _bluetoothService.currentDevice; BluetoothDevice? get currentDevice => _bluetoothService.currentDevice;
// 是否已连接设备 // 是否已连接设备
bool get isDeviceConnected => currentDevice != null; bool get isDeviceConnected => currentDevice != null;
// 是否已完成Companion关联 // 是否已完成Companion关联
final _companionAssociated = false.obs; final _companionAssociated = false.obs;
bool get isCompanionAssociated => _companionAssociated.value; bool get isCompanionAssociated => _companionAssociated.value;
@ -48,10 +51,10 @@ class PairingController extends GetxController {
// 服务状态监听 // 服务状态监听
Worker? _bluetoothStateWorker; Worker? _bluetoothStateWorker;
Worker? _deviceWorker; Worker? _deviceWorker;
// 蓝牙服务 // 蓝牙服务
final _bluetoothService = Get.find<BluetoothManager>(); final _bluetoothService = Get.find<BluetoothManager>();
// BLE服务 // BLE服务
final _bleService = BleService(); final _bleService = BleService();
@ -96,7 +99,7 @@ class PairingController extends GetxController {
// 刷新已连接设备 // 刷新已连接设备
statusDescription.value = 'searchingDevice'.tr; statusDescription.value = 'searchingDevice'.tr;
await refreshConnectedDevices(); await refreshConnectedDevices();
// 检查是否已存在Companion设备关联,但不要重复调用关联流程 // 检查是否已存在Companion设备关联,但不要重复调用关联流程
await _checkExistingCompanionAssociation(); await _checkExistingCompanionAssociation();
} catch (e) { } catch (e) {
@ -107,16 +110,16 @@ class PairingController extends GetxController {
_updateStatusDescription(); _updateStatusDescription();
} }
} }
/// 检查是否已存在Companion设备关联 /// 检查是否已存在Companion设备关联
Future<void> _checkExistingCompanionAssociation() async { Future<void> _checkExistingCompanionAssociation() async {
// 只在Android平台执行 // 只在Android平台执行
if (!Platform.isAndroid) return; if (!Platform.isAndroid) return;
try { try {
final isSupported = await _bleService.isCompanionSupported(); final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) return; if (!isSupported) return;
final pairedMac = await _bleService.getPairedMacAddress(); final pairedMac = await _bleService.getPairedMacAddress();
if (pairedMac != null) { if (pairedMac != null) {
Logger.info('发现已关联的Companion设备: $pairedMac'); Logger.info('发现已关联的Companion设备: $pairedMac');
@ -165,18 +168,18 @@ class PairingController extends GetxController {
Logger.warning('权限 ${permission.toString()} 未被授予: $status'); Logger.warning('权限 ${permission.toString()} 未被授予: $status');
} }
}); });
_hasPermissions.value = allGranted; _hasPermissions.value = allGranted;
return allGranted; return allGranted;
} }
// 辅助方法获取Android SDK版本 // 辅助方法获取Android SDK版本
Future<int> _getAndroidSdkVersion() async { Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
// 这里需要原生代码支持来获取确切的SDK版本, // 这里需要原生代码支持来获取确切的SDK版本,
// 或者使用 device_info_plus 插件 // 或者使用 device_info_plus 插件
// 暂时返回一个较高值以触发新权限请求 // 暂时返回一个较高值以触发新权限请求
return 31; // 假设运行在需要新权限的设备上 return 31; // 假设运行在需要新权限的设备上
} }
return 0; return 0;
} }
@ -184,38 +187,40 @@ class PairingController extends GetxController {
/// 设置服务状态监听 /// 设置服务状态监听
void _setupServiceStateListeners() { void _setupServiceStateListeners() {
// 监听蓝牙服务的蓝牙状态变化 // 监听蓝牙服务的蓝牙状态变化
_bluetoothStateWorker = ever(_bluetoothService.bluetoothState.obs, (String state) { _bluetoothStateWorker =
ever(_bluetoothService.bluetoothState.obs, (String state) {
_bluetoothState.value = state; _bluetoothState.value = state;
_updateStatusDescription(); _updateStatusDescription();
// 如果蓝牙关闭,可能需要更新UI或提示用户 // 如果蓝牙关闭,可能需要更新UI或提示用户
if (state == 'off') { if (state == 'off') {
Logger.info('蓝牙已关闭'); Logger.info('蓝牙已关闭');
} }
}); });
// 监听当前设备变化 - 使用公开的currentDeviceRx字段 // 监听当前设备变化 - 使用公开的currentDeviceRx字段
_deviceWorker = ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) { _deviceWorker =
ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) {
if (device != null && !_isNavigatingToHome) { if (device != null && !_isNavigatingToHome) {
Logger.info('配对页面发现设备已连接: ${device.name}'); Logger.info('配对页面发现设备已连接: ${device.name}');
// 当经典蓝牙设备连接成功,尝试进行Companion关联 // 当经典蓝牙设备连接成功,尝试进行Companion关联
_handleDeviceConnected(); _handleDeviceConnected();
} }
_updateStatusDescription(); _updateStatusDescription();
}); });
if (!isneedearphone) {
//不需要耳机
_handleDeviceConnected();
}
} }
/// 处理设备连接成功事件 /// 处理设备连接成功事件
Future<void> _handleDeviceConnected() async { Future<void> _handleDeviceConnected() async {
if (!isDeviceConnected || _isNavigatingToHome) return; if ((!isDeviceConnected && isneedearphone) || _isNavigatingToHome) return;
// 如果已经完成关联,直接跳转 // 如果已经完成关联,直接跳转
if (_companionAssociated.value) { if (isneedearphone && _companionAssociated.value) {
_autoNavigateToHome(); _autoNavigateToHome();
return; return;
} }
// 启动关联流程 // 启动关联流程
_startCompanionAssociation(); _startCompanionAssociation();
} }
@ -226,7 +231,7 @@ class PairingController extends GetxController {
statusDescription.value = 'permissionDenied'.tr; statusDescription.value = 'permissionDenied'.tr;
return; return;
} }
if (bluetoothState == 'off') { if (bluetoothState == 'off') {
statusDescription.value = 'bluetoothTurnedOff'.tr; statusDescription.value = 'bluetoothTurnedOff'.tr;
return; return;
@ -234,16 +239,12 @@ class PairingController extends GetxController {
if (isDeviceConnected) { if (isDeviceConnected) {
if (Platform.isAndroid && !isCompanionAssociated && _isAssociating) { if (Platform.isAndroid && !isCompanionAssociated && _isAssociating) {
statusDescription.value = '${'associatingDevice'.tr} ${currentDevice!.name}'; statusDescription.value =
'${'associatingDevice'.tr} ${currentDevice!.name}';
} else if (isCompanionAssociated) { } else if (isCompanionAssociated) {
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}'; statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
} else { } else {
// 检查是否为DeepSound耳机 statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
if (!currentDevice!.name.contains("DeapSound")) {
statusDescription.value = 'pleaseConnectDeepSoundDevice'.tr;
} else {
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
}
} }
return; return;
} }
@ -262,18 +263,18 @@ class PairingController extends GetxController {
statusDescription.value = 'permissionDenied'.tr; statusDescription.value = 'permissionDenied'.tr;
return; return;
} }
try { try {
isLoading.value = true; isLoading.value = true;
statusDescription.value = 'searchingDevice'.tr; statusDescription.value = 'searchingDevice'.tr;
// 使用蓝牙服务刷新连接 // 使用蓝牙服务刷新连接
await _bluetoothService.refreshDeviceConnection(); await _bluetoothService.refreshDeviceConnection();
// 检查连接状态 // 检查连接状态
if (isDeviceConnected) { if (isDeviceConnected) {
Logger.info('刷新发现设备已连接: ${currentDevice!.name}'); Logger.info('刷新发现设备已连接: ${currentDevice!.name}');
// 当发现设备已连接,统一由_handleDeviceConnected处理 // 当发现设备已连接,统一由_handleDeviceConnected处理
_handleDeviceConnected(); _handleDeviceConnected();
} else { } else {
@ -287,18 +288,20 @@ class PairingController extends GetxController {
_updateStatusDescription(); _updateStatusDescription();
} }
} }
/// 启动Companion设备关联流程 /// 启动Companion设备关联流程
Future<void> _startCompanionAssociation() async { Future<void> _startCompanionAssociation() async {
// 如果不是Android平台,已经跳转到首页,已经完成关联或正在进行关联,直接返回 // 如果不是Android平台,已经跳转到首页,已经完成关联或正在进行关联,直接返回
if (!Platform.isAndroid || _isNavigatingToHome || _companionAssociated.value || _isAssociating) { if (!Platform.isAndroid ||
_isNavigatingToHome ||
_companionAssociated.value ||
_isAssociating) {
return; return;
} }
// 设置关联状态标志 // 设置关联状态标志
_isAssociating = true; _isAssociating = true;
_updateStatusDescription(); _updateStatusDescription();
try { try {
// 检查是否支持Companion Device功能 // 检查是否支持Companion Device功能
final isSupported = await _bleService.isCompanionSupported(); final isSupported = await _bleService.isCompanionSupported();
@ -314,7 +317,7 @@ class PairingController extends GetxController {
// 不清除关联,因为MAC地址匹配 // 不清除关联,因为MAC地址匹配
return; return;
} }
// 检查是否已有配对的MAC地址 // 检查是否已有配对的MAC地址
final pairedMac = await _bleService.getPairedMacAddress(); final pairedMac = await _bleService.getPairedMacAddress();
if (pairedMac != null) { if (pairedMac != null) {
@ -326,8 +329,8 @@ class PairingController extends GetxController {
Logger.info('已关联的Companion设备与当前连接的设备一致'); Logger.info('已关联的Companion设备与当前连接的设备一致');
_companionAssociated.value = true; _companionAssociated.value = true;
await _bleService.registerBackgroundScan(); await _bleService.registerBackgroundScan();
_autoNavigateToHome(); // _autoNavigateToHome();
return; // return;
} else { } else {
// MAC地址不匹配 // MAC地址不匹配
Logger.warning('未找到已关联的DeepSound耳机(MAC地址不匹配)'); Logger.warning('未找到已关联的DeepSound耳机(MAC地址不匹配)');
@ -337,26 +340,24 @@ class PairingController extends GetxController {
return; return;
} }
} }
// 开始关联流程 // 开始关联流程
if (currentDevice != null) { if (currentDevice != null) {
Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}'); Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}');
final success = await _bleService.startDeviceAssociation( var success = await _bleService.startDeviceAssociation(
deviceNamePattern: currentDevice!.name, deviceNamePattern: currentDevice!.name,
macAddress: currentDevice!.address, macAddress: currentDevice!.address,
); );
if (success) { if (success) {
Logger.info('Companion设备关联成功'); Logger.info('Companion设备关联成功');
_companionAssociated.value = true; _companionAssociated.value = true;
// 注册后台扫描,确保应用被杀死后仍能被唤醒 // 注册后台扫描,确保应用被杀死后仍能被唤醒
await _bleService.registerBackgroundScan(macAddress: currentDevice!.address); await _bleService.registerBackgroundScan(
macAddress: currentDevice!.address);
// 启动BLE后台服务(可选) // 启动BLE后台服务(可选)
await _startAgentService(); await _startAgentService();
// 关联和后台扫描都成功后,再跳转 // 关联和后台扫描都成功后,再跳转
_autoNavigateToHome(); _autoNavigateToHome();
} else { } else {
@ -364,6 +365,12 @@ class PairingController extends GetxController {
// 关联失败,但仍然可以跳转到主页(用户体验考虑) // 关联失败,但仍然可以跳转到主页(用户体验考虑)
_autoNavigateToHome(); _autoNavigateToHome();
} }
} else if (!isneedearphone) {
// 启动BLE后台服务(可选)
await _startAgentService();
// 关联和后台扫描都成功后,再跳转
_autoNavigateToHome();
} }
} catch (e) { } catch (e) {
Logger.error('Companion设备关联过程发生错误: $e'); Logger.error('Companion设备关联过程发生错误: $e');
@ -379,10 +386,10 @@ class PairingController extends GetxController {
/// 自动跳转到主页 /// 自动跳转到主页
Future<void> _autoNavigateToHome() async { Future<void> _autoNavigateToHome() async {
if (_isNavigatingToHome) return; if (_isNavigatingToHome) return;
// 如果没有连接,不进行跳转 // 如果没有连接,不进行跳转
if (!isDeviceConnected) return; if (!isDeviceConnected) return;
// 在Android平台上,如果支持Companion且仍在关联中,不跳转 // 在Android平台上,如果支持Companion且仍在关联中,不跳转
if (Platform.isAndroid) { if (Platform.isAndroid) {
final isSupported = await _bleService.isCompanionSupported(); final isSupported = await _bleService.isCompanionSupported();
@ -391,25 +398,24 @@ class PairingController extends GetxController {
return; return;
} }
} }
_isNavigatingToHome = true; _isNavigatingToHome = true;
Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页'); Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页');
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
Get.offAllNamed(Routes.home); Get.offAllNamed(Routes.home);
}); });
} }
/// 启动BLE后台服务 /// 启动BLE后台服务
Future<void> _startAgentService() async { Future<void> _startAgentService() async {
try { try {
if (!isDeviceConnected) return; if (isneedearphone && !isDeviceConnected) return;
// 启动BLE服务,无需传递配置参数,将从.env读取 // 启动BLE服务,无需传递配置参数,将从.env读取
final result = await BleManager.to.startAgentService(); final result = await BleManager.to.startAgentService();
if (result) { if (result) {
Logger.info('BLE后台服务启动成功'); Logger.info('BLE后台服务启动成功');
} else { } else {
@ -419,12 +425,12 @@ class PairingController extends GetxController {
Logger.error('启动BLE后台服务发生错误: $e'); Logger.error('启动BLE后台服务发生错误: $e');
} }
} }
/// 停止BLE后台服务 /// 停止BLE后台服务
Future<void> _stopAgentService() async { Future<void> _stopAgentService() async {
try { try {
final result = await BleManager.to.stopAgentService(); final result = await BleManager.to.stopAgentService();
if (result) { if (result) {
Logger.info('BLE后台服务停止成功'); Logger.info('BLE后台服务停止成功');
} else { } else {
@ -451,7 +457,7 @@ class PairingController extends GetxController {
Logger.error('打开设置失败: $e'); Logger.error('打开设置失败: $e');
} }
} }
/// 打开应用设置 /// 打开应用设置
Future<void> openAppSettings() async { Future<void> openAppSettings() async {
await AppSettings.openAppSettings(); await AppSettings.openAppSettings();

1
local_plugins/agent_service/android/build.gradle.kts

@ -48,4 +48,5 @@ dependencies {
add("compileOnly", project(":deepvoice_utils")) add("compileOnly", project(":deepvoice_utils"))
add("compileOnly", project(":chat_storage")) add("compileOnly", project(":chat_storage"))
add("compileOnly", project(":ble_service")) add("compileOnly", project(":ble_service"))
add("compileOnly", project(":music_service"))
} }

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

@ -683,6 +683,42 @@ object AgentService : CoroutineScope {
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
)) ))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult") FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) {
metadata = metaStr;
val meta = JSONObject(metaStr)
FileLogger.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic")
val musicUrl = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "")
val sgener = cardMusic.optString("sgener", "")
processMusicPlay(musicUrl, name, sgener)
}else if (meta.has("card_musiclist")) { //音乐列表
val cardMusiclist = meta.getJSONObject("card_musiclist")
val musics = cardMusiclist.optJSONArray("musics")
if (musics != null) {
val playlist = mutableListOf<Map<String, String>>()
for (i in 0 until musics.length()) {
val item = musics.optJSONObject(i) ?: continue
val url = item.optString("url", "")
val name = item.optString("name", "")
val sgener = item.optString("sgener", "")
if (url.isNotEmpty()) {
playlist.add(mapOf("url" to url, "name" to name, "sgener" to sgener))
}
}
if (playlist.isNotEmpty()) {
processMusicPlayList(playlist)
} else {
FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目")
}
} else {
FileLogger.w(TAG, "card_musiclist.musics 不是一个数组")
}
}
}
} }
} }
) )
@ -1025,4 +1061,36 @@ object AgentService : CoroutineScope {
} }
} }
} }
}
/**
* 处理音乐播放
* 将ai返回的音乐连接 在后台启动播放
*
* @param musicUrl 图片文件路径
* @param name 歌曲名称,默认为空
* @param singer 歌手,默认为false
* @return 是否成功开始处理
*/
fun processMusicPlay(musicUrl:String,name: String = "",singer:String = ""){
// 在其他 Service、BroadcastReceiver 或 Application 中调用
FileLogger.i(TAG, "启动音乐服务 播放音乐 $musicUrl")
MusicServiceStarter.startServiceWithCommand(context, "play", url = musicUrl)
}
/**
* 处理音乐播放列表
* 将ai返回的音乐连接 在后台启动播放
*
* @param musicUrl 图片文件路径
* @param name 歌曲名称,默认为空
* @param singer 歌手,默认为false
* @return 是否成功开始处理
*/
fun processMusicPlayList(playlist:List<Map<String,String>>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用
val urls = playlist.mapNotNull { it["url"] }.distinct()
FileLogger.i(TAG, "启动音乐服务 播放音乐列表 $urls")
MusicServiceStarter.startServiceWithPlaylist(context, "playlist", urls = urls)
}
}

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

@ -473,7 +473,7 @@ object BleService {
} }
override fun onCharacteristicChanged(g: BluetoothGatt, c: BluetoothGattCharacteristic) { override fun onCharacteristicChanged(g: BluetoothGatt, c: BluetoothGattCharacteristic) {
val data = c.value ?: return val data = c.value ?: return
// 根据特征UUID区分处理 // 根据特征UUID区分处理
@ -1017,9 +1017,11 @@ object BleService {
stopOpusStreamDecoding() // 停止Opus解码流 stopOpusStreamDecoding() // 停止Opus解码流
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), BleConst.CMD_CONTROL_CODEC.toByte(),
byteArrayOf(BleConst.CODEC_CONTROL_CLOSE.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_CLOSE.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte()) // 或其他默认声道
BleConst.AUDIO_CHANNEL_RIGHT.toByte()
) // 或其他默认声道
) )
} }
@ -1027,45 +1029,54 @@ object BleService {
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openDecoder(): Boolean { fun openDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") FileLogger.i(TAG, "打开编码 (设备侧)...")
val opusStarted = startOpusStreamDecoding( val opusStarted = startOpusStreamDecoding(
hasHeader = false, // BLE传输的Opus数据通常无文件头 hasHeader = false, // BLE传输的Opus数据通常无文件头
channel = 1, // 单声道 channel = 1, // 单声道
sampleRate = 16000,// Opus推荐16kHz或更高 sampleRate = 16000,// Opus推荐16kHz或更高
packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM) packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM)
// packetSize for Opus is typically in ms, e.g. 20, 40, 60. // packetSize for Opus is typically in ms, e.g. 20, 40, 60.
// The library might expect this in a different unit or derive it. // The library might expect this in a different unit or derive it.
// Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms. // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
) )
FileLogger.i(TAG, "打开解码...") FileLogger.i(TAG, "打开解码...")
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte(), return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.AUDIO_CHANNEL_RIGHT.toByte())) BleConst.CODEC_CONTROL_DECODE_ON.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte()
)
)
} }
/** /**
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openA2DPDecoder(): Boolean { fun openA2DPDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") FileLogger.i(TAG, "打开编码 (设备侧)...")
val opusStarted = startOpusStreamDecoding( val opusStarted = startOpusStreamDecoding(
hasHeader = false, // BLE传输的Opus数据通常无文件头 hasHeader = false, // BLE传输的Opus数据通常无文件头
channel = 1, // 单声道 channel = 1, // 单声道
sampleRate = 16000,// Opus推荐16kHz或更高 sampleRate = 16000,// Opus推荐16kHz或更高
packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM) packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM)
// packetSize for Opus is typically in ms, e.g. 20, 40, 60. // packetSize for Opus is typically in ms, e.g. 20, 40, 60.
// The library might expect this in a different unit or derive it. // The library might expect this in a different unit or derive it.
// Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms. // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
) )
FileLogger.i(TAG, "打开解码...") FileLogger.i(TAG, "打开解码...")
return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.AUDIO_CHANNEL_RIGHT.toByte())) BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(),
BleConst.AUDIO_CHANNEL_RIGHT.toByte()
)
)
} }
/** /**
@ -1087,11 +1098,11 @@ object BleService {
channel = 1, // 单声道 channel = 1, // 单声道
sampleRate = 16000,// Opus推荐16kHz或更高 sampleRate = 16000,// Opus推荐16kHz或更高
packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM) packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM)
// packetSize for Opus is typically in ms, e.g. 20, 40, 60. // packetSize for Opus is typically in ms, e.g. 20, 40, 60.
// The library might expect this in a different unit or derive it. // The library might expect this in a different unit or derive it.
// Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms. // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
) )
// 注意:此方法通常是控制设备端开始编码并发送数据。 // 注意:此方法通常是控制设备端开始编码并发送数据。
// App侧通常是打开解码器(openDecoder)来接收和处理这些数据。 // App侧通常是打开解码器(openDecoder)来接收和处理这些数据。
@ -1178,7 +1189,6 @@ object BleService {
return bluetoothAdapter return bluetoothAdapter
} }
// ====================================================================================================== // ======================================================================================================
// 资源清理 // 资源清理

10
local_plugins/ble_service/ios/Classes/BleService.swift

@ -353,6 +353,11 @@ actor BleService {
func getDeviceVersion() -> Bool { func getDeviceVersion() -> Bool {
return sendCommand(BleConst.CMD_GET_VERSION) return sendCommand(BleConst.CMD_GET_VERSION)
} }
///获取产品ID
/// - Returns: 是否成功发送请求
func getProductId() -> Bool {
return sendCommand(BleConst.CMD_GET_PRODUCT_ID)
}
/// 获取电池信息 /// 获取电池信息
/// - Returns: 是否成功发送请求 /// - Returns: 是否成功发送请求
@ -371,6 +376,11 @@ actor BleService {
func openDecoder() -> Bool { func openDecoder() -> Bool {
return sendCodecControl(BleConst.CODEC_CONTROL_DECODE_ON) return sendCodecControl(BleConst.CODEC_CONTROL_DECODE_ON)
} }
/// 打开解码器
/// - Returns: 是否成功发送请求
func openA2DPDecoder() -> Bool {
return sendCodecControl(BleConst.CODEC_CONTROL_A2DP_PLAY)
}
/// 关闭编解码器 /// 关闭编解码器
/// - Returns: 是否成功发送请求 /// - Returns: 是否成功发送请求

8
local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift

@ -73,12 +73,18 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate {
case "getDeviceVersion": case "getDeviceVersion":
result(bleService.getDeviceVersion()) result(bleService.getDeviceVersion())
case "getProductId":
result(bleService.getProductId())
case "openEncoder": case "openEncoder":
result(bleService.openEncoder()) result(bleService.openEncoder())
case "openDecoder": case "openDecoder":
result(bleService.openDecoder()) result(bleService.openDecoder())
case "openA2DPDecoder":
result(bleService.openA2DPDecoder())
case "closeCodec": case "closeCodec":
result(bleService.closeCodec()) result(bleService.closeCodec())

11
local_plugins/ble_service/lib/ble_service.dart

@ -83,6 +83,17 @@ class BleService {
} }
} }
/// 获取产品ID
Future<bool> getProductId() async {
try {
final result = await _methodChannel.invokeMethod<bool>('getProductId');
return result ?? false;
} catch (e) {
print('获取设备版本失败: $e');
return false;
}
}
/// 获取电池信息 /// 获取电池信息
Future<bool> getBatteryInfo() async { Future<bool> getBatteryInfo() async {
try { try {

1
local_plugins/music_service/android/build.gradle

@ -67,4 +67,5 @@ android {
dependencies { dependencies {
implementation project(':deepvoice_utils') implementation project(':deepvoice_utils')
implementation("androidx.media:media:1.6.0")
} }

9
local_plugins/music_service/android/src/main/AndroidManifest.xml

@ -3,7 +3,6 @@
<!-- 基础前台服务权限 --> <!-- 基础前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 14 及以上需要声明具体的前台服务类型权限 --> <!-- Android 14 及以上需要声明具体的前台服务类型权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
@ -13,5 +12,13 @@
android:exported="false" android:exported="false"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="mediaPlayback" /> android:foregroundServiceType="mediaPlayback" />
<!-- 用于接收媒体按钮(耳机按钮、蓝牙播放控制) -->
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

87
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -9,6 +9,8 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import com.yunqiinnovation.deepvoice_utils.FileLogger import com.yunqiinnovation.deepvoice_utils.FileLogger
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -28,7 +30,7 @@ class MusicService : Service() {
fun isPlaying(): Boolean = currentStatus fun isPlaying(): Boolean = currentStatus
fun getCurrentUrl(): String? = currentUrlInternal fun getCurrentUrl(): String? = currentUrlInternal
} }
private lateinit var mediaSession: MediaSessionCompat
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private val playlist = mutableListOf<String>() // 可选:初始化为预置歌曲 private val playlist = mutableListOf<String>() // 可选:初始化为预置歌曲
private var currentIndex = 0 private var currentIndex = 0
@ -38,9 +40,38 @@ class MusicService : Service() {
FileLogger.init(this) FileLogger.init(this)
FileLogger.i(TAG, "MusicService 正在创建...") FileLogger.i(TAG, "MusicService 正在创建...")
isRunning.set(true) isRunning.set(true)
Log.i(TAG, "MusicService Created") mediaSession = MediaSessionCompat(this, TAG).apply {
setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() {
resume()
}
override fun onPause() {
pause()
}
override fun onSkipToNext() {
if (currentIndex + 1 < playlist.size) {
currentIndex++
play(playlist[currentIndex])
}
}
override fun onSkipToPrevious() {
if (currentIndex - 1 >= 0) {
currentIndex--
play(playlist[currentIndex])
}
}
override fun onStop() {
stopSelf()
}
})
isActive = true
}
startForeground(NOTIFICATION_ID, createNotification("等待播放...")) startForeground(NOTIFICATION_ID, createNotification("等待播放..."))
isRunning.set(true) Log.i(TAG, "MusicService Created")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -173,15 +204,14 @@ class MusicService : Service() {
manager.createNotificationChannel(channel) manager.createNotificationChannel(channel)
} }
val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply { // 修复 pendingIntent:打开 App 主 Activity
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP val activityIntent = packageManager.getLaunchIntentForPackage(packageName)
} val pendingIntent = PendingIntent.getActivity(
val pendingIntent = intent?.let { this,
PendingIntent.getActivity( 0,
this, 0, it, activityIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
}
return NotificationCompat.Builder(this, CHANNEL_ID) return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound") .setContentTitle("DeepSound")
@ -189,6 +219,39 @@ class MusicService : Service() {
.setSmallIcon(android.R.drawable.ic_media_play) .setSmallIcon(android.R.drawable.ic_media_play)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setOngoing(true) .setOngoing(true)
.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
.addAction(
NotificationCompat.Action(
android.R.drawable.ic_media_previous, "上一曲",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
)
)
.addAction(
NotificationCompat.Action(
if (currentStatus) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play,
if (currentStatus) "暂停" else "播放",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_PLAY_PAUSE
)
)
)
.addAction(
NotificationCompat.Action(
android.R.drawable.ic_media_next, "下一曲",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(
this,
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
)
)
)
.build() .build()
} }

25
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.yunqiinnovation.deepvoice_utils.FileLogger import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.music_service.MusicServiceStarter.startServiceWithCommand
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -24,22 +25,33 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
FileLogger.d(TAG, "Method call: ${call.method}") FileLogger.d(TAG, "Method call: ${call.method}")
val intent = Intent(context, MusicService::class.java)
when (call.method) { when (call.method) {
"play" -> { "play" -> {
val url = call.argument<String>("url") val url = call.argument<String>("url")
MusicServiceStarter.startServiceWithCommand(context, "play", url) startServiceWithCommand(context, "play", url)
result.success(null)
} }
"playlist" -> { "playlist" -> {
val urls = call.argument<List<String>>("urls") val urls = call.argument<List<String>>("urls")
MusicServiceStarter.startServiceWithPlaylist(context, "playlist", urls) MusicServiceStarter.startServiceWithPlaylist(context, "playlist", urls)
result.success(null)
}
"pause" -> {
startServiceWithCommand(context, "pause")
result.success(null)
}
"resume" -> {
startServiceWithCommand(context, "resume")
result.success(null)
}
"stop" -> {
startServiceWithCommand(context, "stop")
result.success(null)
} }
"pause" -> MusicServiceStarter.startServiceWithCommand(context, "pause")
"resume" -> MusicServiceStarter.startServiceWithCommand(context, "resume")
"stop" -> MusicServiceStarter.startServiceWithCommand(context, "stop")
"setVolume" -> { "setVolume" -> {
val volume = call.argument<Double>("volume")?.toFloat() val volume = call.argument<Double>("volume")?.toFloat()
MusicServiceStarter.startServiceWithCommand(context, "setVolume", volume = volume) startServiceWithCommand(context, "setVolume", volume = volume)
result.success(null)
} }
"getStatus" -> { "getStatus" -> {
val isPlaying = MusicService.isPlaying() val isPlaying = MusicService.isPlaying()
@ -52,7 +64,6 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
} }
else -> result.notImplemented() else -> result.notImplemented()
} }
result.success(null)
} }
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {} override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {}

Loading…
Cancel
Save