You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
655 lines
22 KiB
655 lines
22 KiB
import 'dart:async';
|
|
import '/core/utils/id_verify_guard.dart';
|
|
import '/core/utils/device_connection.dart';
|
|
|
|
import 'package:device_jieli/device_jieli.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:get_storage/get_storage.dart';
|
|
import 'package:agent_service/agent_service.dart';
|
|
import '../../../data/services/novice_guide_service.dart';
|
|
import '../../../routes/app_routes.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
import 'package:flutter/material.dart';
|
|
import '../../../data/services/bluetooth_manager.dart';
|
|
import '../../../data/services/ble_manager.dart';
|
|
import '../../../data/services/ota_check_service.dart';
|
|
import '../../../data/services/id_verify_service.dart';
|
|
import '../../../data/models/appconfig.dart';
|
|
import '../../../data/models/appconfig_model.dart';
|
|
|
|
/// 连接状态枚举
|
|
enum ConnectionStatus {
|
|
disconnected, // 未连接
|
|
connecting, // 连接中
|
|
connected, // 已连接
|
|
failed // 连接失败
|
|
}
|
|
|
|
/// 主页控制器,管理设备连接状态和信息展示
|
|
class HomeController extends GetxController {
|
|
static const String _tag = 'HomeCtrl';
|
|
static const String _lastDeviceNameKey = 'last_connected_device_name'; // 存储键
|
|
static const String _lastDeviceImageKey =
|
|
'last_connected_device_image'; // 存储上一次设备图片的键
|
|
static HomeController get to => Get.find();
|
|
final GetStorage _storage = GetStorage();
|
|
String? _lastConnectedDeviceImage; // 新增:存储上一次连接设备的图片链接
|
|
|
|
final _isFullyConnected = false.obs;
|
|
String? _lastConnectedDeviceName;
|
|
|
|
// 播报打断开关状态(与 AgentController 对齐)
|
|
final isTtsInterruptEnabled = false.obs; // 是否启用播报打断
|
|
|
|
// 移除了电池信息定时刷新相关变量
|
|
|
|
// OTA检查服务
|
|
late final OtaCheckService _otaCheckService;
|
|
|
|
bool get isFullyConnected => _isFullyConnected.value;
|
|
RxBool get isFullyConnectedRx => _isFullyConnected;
|
|
BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice;
|
|
Rx<DeviceInfo?> get deviceInfo => _bleManager.deviceInfo;
|
|
|
|
static final RxInt leftEarBattery = 0.obs;
|
|
static final RxInt rightEarBattery = 0.obs;
|
|
final caseBattery = 0.obs;
|
|
final leftEarCharging = false.obs;
|
|
final rightEarCharging = false.obs;
|
|
final caseCharging = false.obs;
|
|
final firmwareVersion = ''.obs;
|
|
|
|
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? _classicBluetoothWorker;
|
|
Worker? _bleConnectionWorker;
|
|
|
|
/// 全局 5s 刷新定时器。声明为 static 是为了让它跨 HomeController 实例存活:
|
|
/// `Get.offAllNamed(Routes.home)` 把旧 controller 干掉、新 controller 起来时,
|
|
/// 这个定时器始终是同一个,永远在跑,直接调当前注册的 HomeController 的
|
|
/// `refreshDeviceConnection`(=点刷新按钮的逻辑)。
|
|
static Timer? _staticPollTimer;
|
|
static const Duration _pollInterval = Duration(seconds: 5);
|
|
|
|
final _bluetoothManager = Get.find<BluetoothManager>();
|
|
final _bleManager = Get.find<BleManager>();
|
|
|
|
final noviceGuideService = Get.find<NoviceGuideService>();
|
|
|
|
/// 按设备名安全地查产品(AppConfig.getproduct 会抛,见调用处注释)。
|
|
DBProduct? _findProductByName(String deviceName) {
|
|
if (!AppConfig.isInitialized()) return null;
|
|
final n = deviceName.trim();
|
|
if (n.isEmpty) return null;
|
|
for (final p in AppConfig.config.products) {
|
|
if (p.devicename.contains(n)) return p;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_otaCheckService = Get.find<OtaCheckService>();
|
|
// 实名状态:InitialBinding 里那次拉取发生在登录之前、必然拿不到,
|
|
// 登录后所有流程都会落到 home,在这里补一次(已拉到过就不会重复请求)。
|
|
if (Get.isRegistered<IdVerifyService>()) {
|
|
IdVerifyService.to.ensureLoaded();
|
|
}
|
|
_loadLastConnectedDeviceName(); // 加载持久化的设备名称
|
|
_initWithCachedInfo();
|
|
_loadTtsInterrupt(); // 加载播报打断开关初始状态
|
|
// 监听存储中的“agent_tts_interrupe”键变化,实现与 Agent 页同步
|
|
// 中文注释:当 Agent 页或 Home 页任意一处切换时,另一处通过此监听自动刷新开关状态
|
|
_storage.listenKey('agent_tts_interrupt', (value) {
|
|
try {
|
|
final bool newValue = value is bool ? value : (value == 'true');
|
|
if (isTtsInterruptEnabled.value != newValue) {
|
|
isTtsInterruptEnabled.value = newValue;
|
|
}
|
|
} catch (e) {
|
|
// 中文注释:忽略异常,保持现有状态,避免因类型转换失败导致崩溃
|
|
}
|
|
});
|
|
|
|
// 移除了电池信息定时刷新的启动
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_deviceInfoWorker?.dispose();
|
|
_classicBluetoothWorker?.dispose();
|
|
_bleConnectionWorker?.dispose();
|
|
super.onClose();
|
|
}
|
|
|
|
void _initWithCachedInfo() {
|
|
_setupStateListeners();
|
|
_checkConnectionStatus();
|
|
_ensureStaticPollTimer();
|
|
}
|
|
|
|
/// 启动静态 5s 刷新定时器。已经在跑就什么都不做。
|
|
/// 定时器一旦起来就永远不取消(HomeController 只是 lazyPut,控制器实例可能反复
|
|
/// 销毁/重建,但定时器跨实例存活),每次 fire 都去当前注册的 HomeController
|
|
/// 调 refreshDeviceConnection——和用户点刷新按钮完全等价。
|
|
static void _ensureStaticPollTimer() {
|
|
if (_staticPollTimer != null && _staticPollTimer!.isActive) return;
|
|
_staticPollTimer = Timer.periodic(_pollInterval, (_) {
|
|
try {
|
|
if (Get.isRegistered<HomeController>()) {
|
|
Get.find<HomeController>().refreshDeviceConnection();
|
|
}
|
|
} catch (_) {
|
|
// 忽略:home 还没注册或者刚销毁中间窗口期
|
|
}
|
|
});
|
|
}
|
|
|
|
void _setupStateListeners() {
|
|
_deviceInfoWorker = ever(_bleManager.deviceInfo, (DeviceInfo? info) {
|
|
if (info != null) updateDeviceInfo(info);
|
|
});
|
|
|
|
_classicBluetoothWorker =
|
|
ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) {
|
|
if (device != null) {
|
|
_saveLastConnectedDeviceName(device.name); // 使用新的保存方法
|
|
}
|
|
_checkConnectionStatus();
|
|
});
|
|
|
|
_bleConnectionWorker = ever(_bleManager.connectionStateRx, (int state) {
|
|
_checkConnectionStatus();
|
|
});
|
|
}
|
|
|
|
/// 加载“播报打断”开关状态
|
|
/// 逻辑与 AgentController 保持一致:
|
|
/// 1)若本地不存在存储,则根据设备产品的 iscontinuouschat 取反作为默认值;
|
|
/// 2)若存在存储则直接读取;
|
|
/// 3)同步到原生服务 AgentService.isInterrupt。
|
|
Future<void> _loadTtsInterrupt() async {
|
|
try {
|
|
if (!_storage.hasData('agent_tts_interrupt')) {
|
|
final deviceName = getDeviceName();
|
|
// 这里用本地的 _findProductByName 而不是 AppConfig.getproduct():
|
|
// 后者只做「产品名包含设备名」单向匹配,本地这份是双向包含 + 兜底,
|
|
// 设备名与产品名不完全一致时才匹配得上。
|
|
// (getproduct 本身已经不会抛了,2026-09-03 起查不到返回 null。)
|
|
final DBProduct? product = _findProductByName(deviceName);
|
|
if (deviceName.isNotEmpty && product != null) {
|
|
isTtsInterruptEnabled.value = !(product.iscontinuouschat ?? true);
|
|
} else {
|
|
isTtsInterruptEnabled.value = false;
|
|
}
|
|
} else {
|
|
isTtsInterruptEnabled.value =
|
|
_storage.read('agent_tts_interrupt') ?? false;
|
|
}
|
|
await AgentService.isInterrupt(isTtsInterruptEnabled.value);
|
|
} catch (e) {
|
|
Logger.e(_tag, '加载播报打断开关失败: $e');
|
|
isTtsInterruptEnabled.value = false;
|
|
}
|
|
}
|
|
|
|
/// 切换“播报打断”开关
|
|
/// 同步保存到本地,并通知原生服务。
|
|
Future<void> toggleTtsInterrupt() async {
|
|
try {
|
|
isTtsInterruptEnabled.toggle();
|
|
await _storage.write('agent_tts_interrupt', isTtsInterruptEnabled.value);
|
|
await AgentService.isInterrupt(isTtsInterruptEnabled.value);
|
|
} catch (e) {
|
|
Logger.e(_tag, '保存播报打断偏好失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 从本地存储加载最后连接的设备名称
|
|
void _loadLastConnectedDeviceName() {
|
|
_lastConnectedDeviceName = _storage.read(_lastDeviceNameKey);
|
|
Logger.d(_tag, '加载最后连接的设备名称: $_lastConnectedDeviceName');
|
|
}
|
|
|
|
/// 保存最后连接的设备名称到本地存储
|
|
void _saveLastConnectedDeviceName(String deviceName) {
|
|
_lastConnectedDeviceName = deviceName;
|
|
_storage.write(_lastDeviceNameKey, deviceName);
|
|
Logger.d(_tag, '保存最后连接的设备名称: $deviceName');
|
|
}
|
|
|
|
/// 保存上一次连接设备的图片链接到本地存储
|
|
void _saveLastConnectedDeviceImage(String? imageUrl) {
|
|
if (imageUrl != null && imageUrl.isNotEmpty) {
|
|
_lastConnectedDeviceImage = imageUrl;
|
|
_storage.write(_lastDeviceImageKey, imageUrl);
|
|
Logger.d(_tag, '保存上一次连接设备的图片: $imageUrl');
|
|
}
|
|
}
|
|
|
|
Future<void> refreshDeviceConnection() async {
|
|
await _checkConnectionStatus();
|
|
|
|
// 如果设备已连接,主动刷新设备信息
|
|
if (_isFullyConnected.value) {
|
|
try {
|
|
Logger.d(_tag, '刷新设备信息');
|
|
await _bleManager.fetchDeviceInfo();
|
|
} catch (e) {
|
|
Logger.e(_tag, '刷新设备信息失败: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
void rescanDevices() {
|
|
Get.toNamed(Routes.devices);
|
|
}
|
|
|
|
void openTranslation() {
|
|
AgentService.interruptCurrentResponse();
|
|
Get.toNamed(Routes.translationMiddleware);
|
|
}
|
|
|
|
/// 翻译跳转:按 translationId 进入对应翻译模式
|
|
/// 逻辑参考 TranslationMiddlewareController.navigateToTranslationFeature
|
|
/// translationId 取值:simultaneous / faceToFace / call / audioVideo
|
|
/// 其中 call、audioVideo 依赖设备连接,未连接则提示并返回
|
|
void openTranslationFeature(String translationId) {
|
|
if (IdVerifyGuard.blocked()) return;
|
|
// 只有通话翻译必须依赖耳机链路;多媒体翻译走手机摄像头/麦克风,无需连接设备
|
|
final bool needBle = translationId == 'call';
|
|
// 三条链路任意一条连上都算已连接(杰理 / 蓝讯支架 / 恒玄耳机)。
|
|
// 之前只看 _isFullyConnected(杰理专用),恒玄耳机连上了照样被拦。
|
|
// 通话翻译还要求正处于通话中——耳机得先进通话模式(0xD1)才有双路音频
|
|
if (needBle && DeviceConnection.any && !DeviceConnection.inCall) {
|
|
Get.snackbar('callRequiredTitle'.tr, 'callRequiredDesc'.tr,
|
|
snackPosition: SnackPosition.TOP,
|
|
duration: const Duration(seconds: 2));
|
|
return;
|
|
}
|
|
if (needBle && !DeviceConnection.any) {
|
|
Get.snackbar(
|
|
'connectDevice'.tr, // 连接设备
|
|
'pleaseConnectDeepSoundDevice'.tr, // 请连接 Eaimar 设备
|
|
snackPosition: SnackPosition.TOP,
|
|
duration: const Duration(seconds: 1),
|
|
);
|
|
return;
|
|
}
|
|
if (DeviceConnection.isBlockedByCall(translationId)) {
|
|
Get.snackbar('inCallBusyTitle'.tr, 'inCallBusyDesc'.tr,
|
|
snackPosition: SnackPosition.TOP,
|
|
duration: const Duration(seconds: 2));
|
|
return;
|
|
}
|
|
AgentService.interruptCurrentResponse();
|
|
Get.toNamed(Routes.translation,
|
|
arguments: {'translationId': translationId});
|
|
}
|
|
|
|
/// 打开通话录音:与首页卡片置灰条件统一,未连接设备则提示并返回
|
|
void openCallRecording() {
|
|
if (IdVerifyGuard.blocked()) return;
|
|
if (DeviceConnection.any && !DeviceConnection.inCall) {
|
|
Get.snackbar('callRequiredTitle'.tr, 'callRequiredDesc'.tr,
|
|
snackPosition: SnackPosition.TOP,
|
|
duration: const Duration(seconds: 2));
|
|
return;
|
|
}
|
|
if (!DeviceConnection.any) {
|
|
Get.snackbar(
|
|
'connectDevice'.tr, // 连接设备
|
|
'pleaseConnectDeepSoundDevice'.tr, // 请连接 Eaimar 设备
|
|
snackPosition: SnackPosition.TOP,
|
|
duration: const Duration(seconds: 1),
|
|
);
|
|
return;
|
|
}
|
|
Get.toNamed(Routes.recording,
|
|
arguments: {'title': 'agentCallRecording', 'folder': 'call_recordings'});
|
|
}
|
|
|
|
/// 打开现场录音。
|
|
///
|
|
/// 不再要求先连设备:连上了走设备录音通路,没连就用手机麦克风
|
|
/// (具体分支见 CallRecordingController._startRecording)。
|
|
void openFieldRecording() {
|
|
if (DeviceConnection.isBlockedByCall('fieldRecording')) {
|
|
Get.snackbar('inCallBusyTitle'.tr, 'inCallBusyDesc'.tr,
|
|
snackPosition: SnackPosition.TOP,
|
|
duration: const Duration(seconds: 2));
|
|
return;
|
|
}
|
|
if (IdVerifyGuard.blocked()) return;
|
|
Get.toNamed(Routes.fieldRecording, arguments: {
|
|
'title': 'agentFieldRecording',
|
|
'folder': 'field_recordings',
|
|
// 录完归档进语音记录助手,由它统一管理并上云
|
|
'archiveToMeeting': true,
|
|
});
|
|
}
|
|
|
|
/// 打开会议模块
|
|
/// 参数:无
|
|
/// 返回值:`Future<void>` 无返回数据
|
|
/// 异常:不显式抛出;内部仅记录日志
|
|
/// 说明:进入会议页前关闭唤醒;返回首页后根据录音状态决定是否开启唤醒(存在录音任务则不启用)
|
|
void openMeet() {
|
|
if (IdVerifyGuard.blocked()) return;
|
|
AgentService.interruptCurrentResponse();
|
|
Get.toNamed(Routes.meeting);
|
|
}
|
|
|
|
Future<void> _checkConnectionStatus() async {
|
|
try {
|
|
// 直接问杰理 SDK 当前已连接设备
|
|
JieliDevice? sdkDevice;
|
|
try {
|
|
sdkDevice = await Jielihome.instance.connectedDevice();
|
|
} catch (e) {
|
|
Logger.w(_tag, '查询杰理已连接设备失败: $e');
|
|
sdkDevice = null;
|
|
}
|
|
final hasBleConnection =
|
|
sdkDevice != null && sdkDevice.address.isNotEmpty;
|
|
bool wasConnected = _isFullyConnected.value;
|
|
bool isNowConnected = hasBleConnection;
|
|
|
|
if (_isFullyConnected.value != isNowConnected) {
|
|
_isFullyConnected.value = isNowConnected;
|
|
_isFullyConnected.refresh();
|
|
//update();
|
|
|
|
if (!wasConnected && isNowConnected) {
|
|
// 连接成功后等待1秒再获取设备信息
|
|
Future.delayed(const Duration(seconds: 1), () async {
|
|
await _bleManager.fetchDeviceInfo();
|
|
});
|
|
|
|
// 移除了连接成功后的电池信息刷新
|
|
|
|
// 连接成功后检查OTA升级
|
|
if (noviceGuideService.isHomeGuide) {
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
_checkOtaUpgrade();
|
|
});
|
|
}
|
|
}
|
|
// 移除了根据连接状态控制定时器的逻辑
|
|
}
|
|
|
|
//update();
|
|
} catch (e) {
|
|
Logger.e(_tag, '${'checkConnectionStatusFailed'.tr}: $e');
|
|
if (_isFullyConnected.value) {
|
|
_isFullyConnected.value = false;
|
|
_isFullyConnected.refresh();
|
|
// 移除了发生错误时停止定时器的逻辑
|
|
// 注意:不再重置OTA检查状态,因为现在由全局服务管理
|
|
//update();
|
|
}
|
|
}
|
|
}
|
|
|
|
Color getConnectionStatusColor() {
|
|
return _isFullyConnected.value ? Colors.green : Colors.grey;
|
|
}
|
|
|
|
String getConnectionStatusText() {
|
|
return _isFullyConnected.value ? 'connected'.tr : 'unconnected'.tr;
|
|
}
|
|
|
|
bool get shouldShowDeviceInfo => currentDevice != null;
|
|
|
|
String getDeviceName() {
|
|
if (_isFullyConnected.value && currentDevice == null) {
|
|
_checkConnectionStatus();
|
|
}
|
|
|
|
final deviceName = currentDevice?.name;
|
|
//final deviceName = '102';
|
|
if (deviceName != null && deviceName.isNotEmpty) {
|
|
return deviceName;
|
|
}
|
|
|
|
final bleDeviceName = deviceInfo.value?.name;
|
|
if (bleDeviceName != null && bleDeviceName.isNotEmpty) {
|
|
return bleDeviceName;
|
|
}
|
|
|
|
if (!_isFullyConnected.value &&
|
|
_lastConnectedDeviceName != null &&
|
|
_lastConnectedDeviceName!.isNotEmpty) {
|
|
return _lastConnectedDeviceName!;
|
|
}
|
|
|
|
return 'unknownEquipment'.tr;
|
|
}
|
|
|
|
String getSignalStrength() {
|
|
if (!_isFullyConnected.value) {
|
|
return 'zeroSignal'.tr;
|
|
}
|
|
|
|
final random = DateTime.now().millisecondsSinceEpoch % 3;
|
|
switch (random) {
|
|
case 0:
|
|
return 'good'.tr;
|
|
case 1:
|
|
return 'fine'.tr;
|
|
default:
|
|
return 'excellent'.tr;
|
|
}
|
|
}
|
|
|
|
String getDeviceId() {
|
|
return deviceInfo.value?.address ?? currentDevice?.address ?? 'unknown'.tr;
|
|
}
|
|
|
|
String getBatteryText() {
|
|
return deviceInfo.value?.batteryLevel != null &&
|
|
deviceInfo.value!.batteryLevel > 0
|
|
? '${deviceInfo.value?.batteryLevel}%'
|
|
: 'unknown'.tr;
|
|
}
|
|
|
|
String getEarbudsText() {
|
|
if (deviceInfo.value == null ||
|
|
(deviceInfo.value!.leftEarBattery <= 0 &&
|
|
deviceInfo.value!.rightEarBattery <= 0 ||
|
|
deviceInfo.value!.caseBattery > 100)) {
|
|
return 'unknown'.tr;
|
|
}
|
|
|
|
String leftStatus = deviceInfo.value!.leftEarBattery > 0
|
|
? '${deviceInfo.value!.leftEarBattery}%'
|
|
: 'N/A';
|
|
String rightStatus = deviceInfo.value!.rightEarBattery > 0
|
|
? '${deviceInfo.value!.rightEarBattery}%'
|
|
: 'N/A';
|
|
|
|
leftStatus =
|
|
deviceInfo.value!.leftEarCharging ? '$leftStatus ⚡' : leftStatus;
|
|
rightStatus =
|
|
deviceInfo.value!.rightEarCharging ? '$rightStatus ⚡' : rightStatus;
|
|
|
|
return '${'leftEar'.tr}: $leftStatus / ${'rightEar'.tr}: $rightStatus'; // 左耳: xxx / 右耳: xxx
|
|
}
|
|
|
|
String getCaseText() {
|
|
if (deviceInfo.value == null ||
|
|
deviceInfo.value!.caseBattery <= 0 ||
|
|
deviceInfo.value!.caseBattery > 100) {
|
|
return 'unknown'.tr;
|
|
}
|
|
|
|
String caseStatus = '${deviceInfo.value!.caseBattery}%';
|
|
if (deviceInfo.value!.caseCharging) {
|
|
caseStatus = '$caseStatus ⚡';
|
|
}
|
|
|
|
return '${'chargingCase'.tr}: $caseStatus'; // 充电仓: xxx
|
|
}
|
|
|
|
String getChargingText() {
|
|
if (deviceInfo.value == null) {
|
|
return 'unknown'.tr;
|
|
}
|
|
|
|
bool isCharging = deviceInfo.value!.leftEarCharging ||
|
|
deviceInfo.value!.rightEarCharging ||
|
|
deviceInfo.value!.caseCharging;
|
|
|
|
return isCharging ? 'charging'.tr : 'notCharging'.tr;
|
|
}
|
|
|
|
/// 获取当前设备类型
|
|
/// 返回设备类型,如果无法获取则返回未知类型
|
|
DeviceType getCurrentDeviceType() {
|
|
try {
|
|
final deviceName = getDeviceName();
|
|
if (deviceName == 'unknownEquipment'.tr) {
|
|
return DeviceType.unknown;
|
|
}
|
|
|
|
// 通过AppConfig获取产品信息
|
|
final product = AppConfig.getproduct(deviceName);
|
|
if (product != null) {
|
|
// 根据产品的devicetype字段返回对应的DeviceType
|
|
switch (product.devicetype) {
|
|
case 1:
|
|
return DeviceType.classicBluetoothHeadset;
|
|
case 2:
|
|
return DeviceType.bleBluetoothDoubleHeadset;
|
|
case 3:
|
|
return DeviceType.bleBluetoothSingleHeadset;
|
|
case 4:
|
|
return DeviceType.smartVoiceRecorder;
|
|
case 5:
|
|
return DeviceType.smartEye;
|
|
default:
|
|
return DeviceType.unknown;
|
|
}
|
|
}
|
|
|
|
return DeviceType.unknown;
|
|
} catch (e) {
|
|
Logger.e(_tag, '获取设备类型失败: $e');
|
|
return DeviceType.unknown;
|
|
}
|
|
}
|
|
|
|
/// 判断当前设备是否为单端耳机
|
|
bool isSingleHeadset() {
|
|
return getCurrentDeviceType() == DeviceType.bleBluetoothSingleHeadset ||
|
|
getCurrentDeviceType() == DeviceType.smartVoiceRecorder ||
|
|
getCurrentDeviceType() == DeviceType.smartEye;
|
|
}
|
|
|
|
String getVersionText() {
|
|
return deviceInfo.value?.firmwareVersion.isNotEmpty == true
|
|
? deviceInfo.value!.firmwareVersion
|
|
: 'unknown'.tr;
|
|
}
|
|
|
|
// 移除了电池信息定时刷新相关方法
|
|
|
|
/// 检查OTA固件升级
|
|
/// 在BLE连接成功后调用,检查是否需要升级
|
|
Future<void> _checkOtaUpgrade() async {
|
|
Logger.d(_tag, '触发OTA升级检查...');
|
|
|
|
// 打印当前OTA检查状态(用于调试)
|
|
final status = _otaCheckService.getCheckStatus();
|
|
Logger.d(_tag, 'OTA检查状态: $status');
|
|
|
|
// 使用全局OTA检查服务执行检查
|
|
final success =
|
|
await _otaCheckService.performOtaCheck(_isFullyConnected.value);
|
|
|
|
if (success) {
|
|
Logger.d(_tag, 'OTA检查已成功执行');
|
|
} else {
|
|
Logger.d(_tag, 'OTA检查被跳过或执行失败');
|
|
}
|
|
}
|
|
|
|
/// 调试方法:获取OTA检查状态
|
|
/// 可以在开发阶段用于调试
|
|
Map<String, dynamic> getOtaCheckStatus() {
|
|
return _otaCheckService.getCheckStatus();
|
|
}
|
|
|
|
/// 获取当前设备的产品图片路径
|
|
/// 如果没有配置产品图片或获取失败,返回null使用默认图片
|
|
String? getCurrentDeviceImage() {
|
|
try {
|
|
final deviceName = getDeviceName();
|
|
if (deviceName == 'unknownEquipment'.tr) {
|
|
Logger.d(_tag, '设备未知,使用默认图片');
|
|
return null;
|
|
}
|
|
|
|
// 通过AppConfig获取产品信息
|
|
if (!AppConfig.isInitialized()) {
|
|
Logger.w(_tag, 'AppConfig未初始化,使用默认图片');
|
|
return null;
|
|
}
|
|
|
|
final product = AppConfig.getproduct(deviceName);
|
|
if (product != null &&
|
|
product.productimage != null &&
|
|
product.productimage!.isNotEmpty) {
|
|
Logger.d(_tag, '获取到设备产品图片: ${product.productimage}');
|
|
_saveLastConnectedDeviceImage(product.productimage);
|
|
return product.productimage;
|
|
} else {
|
|
Logger.d(_tag, '设备 "$deviceName" 未配置产品图片,使用默认图片');
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
Logger.e(_tag, '获取设备产品图片失败: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// 获取设备图片的完整资源路径
|
|
/// 处理网络图片URL和本地资源路径
|
|
String getDeviceImagePath(bool isDarkMode) {
|
|
final productImage = getCurrentDeviceImage();
|
|
|
|
if (productImage != null) {
|
|
// 如果是网络图片URL
|
|
if (productImage.startsWith('http://') ||
|
|
productImage.startsWith('https://')) {
|
|
return productImage;
|
|
}
|
|
// 如果是本地资源路径,确保以assets/开头
|
|
else if (productImage.startsWith('assets/')) {
|
|
return productImage;
|
|
}
|
|
// 如果只是文件名,添加assets/images/前缀
|
|
else {
|
|
return 'assets/images/$productImage';
|
|
}
|
|
}
|
|
|
|
// 使用默认耳机图片
|
|
return 'assets/images/defaultPicture.png';
|
|
}
|
|
|
|
/// 判断是否为网络图片
|
|
bool isNetworkImage(String imagePath) {
|
|
return imagePath.startsWith('http://') || imagePath.startsWith('https://');
|
|
}
|
|
}
|
|
|