import 'package:get/get.dart'; import 'package:dio/dio.dart'; import '../../../core/utils/logger.dart'; import '../../../data/services/ble_manager.dart'; import '../../../data/services/network/api.dart'; class WakeupVoiceController extends GetxController { // 是否启用唤醒词(原有本地词功能,可保留) final RxBool enabled = true.obs; final BleManager bleManager = Get.find(); // 本地自定义唤醒词(原有) final RxList wakeWords = ['你好,深声', 'Hey Voitrans'].obs; final RxString inputText = ''.obs; // 服务端唤醒语音包列表 final RxList voices = [].obs; // 当前设备的唤醒语音 id 与版本(由外部设备信息设置) final RxnInt currentDeviceVoiceId = RxnInt(53251); final RxnString currentDeviceVoiceVersion = RxnString('1.0.0'); // 当前页面选择的语音包 id final RxnInt selectedVoiceId = RxnInt(); // 加载状态 final RxBool isLoading = false.obs; final RxString loadError = ''.obs; // 设备状态(用于页面头部展示) final RxString deviceName = ''.obs; final RxBool isConnected = false.obs; final RxBool wakeupVersionMismatch = false.obs; final RxnInt deviceWakeIdCode = RxnInt(); final RxnInt deviceWakeVerCode = RxnInt(); // 预览/测试模式:跳过 onReady 自动拉取 bool skipAutoFetch = false; @override void onReady() { super.onReady(); _bindBleObservers(); // 允许通过路由参数传入当前设备唤醒语音信息 final args = Get.arguments; if (args is Map) { final id = args['wakeupVoiceId']; final ver = args['wakeupVoiceVersion']; if (id is int && ver is String) { setCurrentDeviceWakeVoice(id: id, version: ver); } final name = args['deviceName']; final connected = args['connected']; if (name is String) deviceName.value = name; if (connected is bool) isConnected.value = connected; } if (!skipAutoFetch) { fetchVoices(); } } /// 绑定 BLE 相关的观察者 /// 参数:无 /// 返回:无 /// 异常:发生监听绑定异常时捕获并记录日志,不抛出 void _bindBleObservers() { // 同步连接状态与设备名 try { isConnected.value = bleManager.isConnected; final info = bleManager.deviceInfo.value; if (info != null && info.name.isNotEmpty) { deviceName.value = info.name; _syncDeviceWakeVersions(info); } // 监听连接状态变化 ever(bleManager.connectionStateRx, (state) { isConnected.value = bleManager.isConnected; final info = bleManager.deviceInfo.value; if (info != null && info.name.isNotEmpty) { deviceName.value = info.name; _syncDeviceWakeVersions(info); } }); // 监听设备信息更新 ever(bleManager.deviceInfo, (info) { if (info != null && info.name.isNotEmpty) { deviceName.value = info.name; _syncDeviceWakeVersions(info); } }); // 监听升级进度 ever(bleManager.wakeupVoiceUpgradeProgress, (p) { upgradeProgress.value = p; if (p >= 100 && upgradingVoiceId.value != null) { final item = _findById(upgradingVoiceId.value); if (item != null) { currentDeviceVoiceId.value = item.id; currentDeviceVoiceVersion.value = item.version; selectedVoiceId.value = item.id; } upgradingVoiceId.value = null; } }); // 监听升级状态:仅当状态为“成功(2)”或“失败(3)”时弹出提示弹窗;升级中(1)或其他状态不提示 ever(bleManager.wakeupVoiceUpgradeStatus, (s) { // 升级状态字段未定义,暂存到日志 Logger.info('BLE 升级状态码: $s'); if (s == 2) { // 升级成功 Get.snackbar('成功', '唤醒语音升级成功'); } else if (s == 3) { // 升级失败 Get.snackbar('错误', '唤醒语音升级失败'); } }); } catch (e) { Logger.warning('绑定BLE观察者失败: $e'); } } // 供外部设置当前设备语音包信息 /// 设置当前设备的唤醒语音包信息 /// 参数:`id` 唤醒词ID;`version` 版本字符串(如 `1.2.3.4`) /// 返回:无 /// 异常:无 void setCurrentDeviceWakeVoice({required int id, required String version}) { currentDeviceVoiceId.value = id; currentDeviceVoiceVersion.value = version; // 若已加载列表,默认选中当前设备项 if (voices.isNotEmpty) { selectedVoiceId.value = id; } } /// 拉取服务端唤醒语音包列表 /// 参数:无 /// 返回:`Future`,完成后 `voices`、`selectedVoiceId` 等被更新 /// 异常:网络或数据解析异常会写入 `loadError` 并弹出提示,不抛出 Future fetchVoices() async { try { isLoading.value = true; loadError.value = ''; final res = await Api.getwakeupvoices(); Logger.info('获取唤醒语音列表成功 res: $res'); // 仅按约定结构解析:{ voices: [...] } if (res is! Map || res['voices'] is! List) { loadError.value = '接口返回异常:缺少 voices 列表'; voices.clear(); return; } final list = List>.from(res['voices'] as List); voices.assignAll(list .map((e) => WakeupVoiceItem.fromJson(e as Map)) .toList()); if (voices.isNotEmpty) { selectedVoiceId.value = voices.first.id; } final devIdCode = deviceWakeIdCode.value; if (devIdCode != null) { final sameIdItems = voices.where((v) { final p = _parseVersion(v.version); return p != null && p.length >= 2 && ((p[0] << 8) | p[1]) == devIdCode; }).toList(); if (sameIdItems.isNotEmpty) { selectedVoiceId.value = sameIdItems.first.id; currentDeviceVoiceId.value = devIdCode; } } if (wakeupVersionMismatch.value) { loadError.value = '设备异常:左右唤醒词版本不一致'; } } catch (e) { loadError.value = '加载唤醒语音失败: $e'; Get.snackbar('错误', loadError.value); } finally { isLoading.value = false; } } /// 解析版本字符串为四段整型数组 /// 参数:`v` 版本字符串,期望形如 `A.B.C.D`(可包含非数字分隔符) /// 返回:`List?` 长度为4的数组,若解析失败返回 `null` /// 异常:无 List? _parseVersion(String v) { final parts = v.split(RegExp(r'[^0-9]+')).where((e) => e.isNotEmpty).toList(); if (parts.length < 4) return null; final nums = parts.take(4).map((e) => int.tryParse(e) ?? 0).toList(); return nums; } /// 同步设备左右耳唤醒词版本信息,并计算设备唤醒词ID与版本编码 /// 参数:`info` 设备信息对象,需包含 `wakeupLeftVersion` 与 `wakeupRightVersion` /// 返回:无 /// 异常:无;若版本缺失或解析失败则静默返回 void _syncDeviceWakeVersions(DeviceInfo info) { final left = info.wakeupLeftVersion; final right = info.wakeupRightVersion; wakeupVersionMismatch.value = left.isNotEmpty && right.isNotEmpty && left != right; final base = left.isNotEmpty ? left : right; if (base.isEmpty) return; final p = _parseVersion(base); if (p == null || p.length < 4) return; deviceWakeIdCode.value = (p[0] << 8) | p[1]; deviceWakeVerCode.value = (p[2] << 8) | p[3]; currentDeviceVoiceId.value = deviceWakeIdCode.value; currentDeviceVoiceVersion.value = '${p[2]}.${p[3]}'; } /// 判断候选项与设备的唤醒词ID是否一致 /// 参数:`item` 唤醒词项,若为空返回 `false` /// 返回:`bool`,一致返回 `true` /// 异常:无 bool _isSameWakeId(WakeupVoiceItem? item) { if (item == null) return false; final p = _parseVersion(item.version); if (p == null || p.length < 2) return false; final idCode = (p[0] << 8) | p[1]; return deviceWakeIdCode.value != null && deviceWakeIdCode.value == idCode; } /// 判断候选项与设备的唤醒词版本是否一致 /// 参数:`item` 唤醒词项,若为空返回 `false` /// 返回:`bool`,一致返回 `true` /// 异常:无 bool _isSameWakeVersion(WakeupVoiceItem? item) { if (item == null) return false; final p = _parseVersion(item.version); if (p == null || p.length < 4) return false; final verCode = (p[2] << 8) | p[3]; return deviceWakeVerCode.value != null && deviceWakeVerCode.value == verCode; } // 选择语音包 /// 选择语音包 /// 参数:`id` 待选择的语音包ID /// 返回:无 /// 异常:无 void selectVoice(int id) { selectedVoiceId.value = id; } // 动作按钮文案与状态 /// 获取当前选择项对应的操作文案(基于设备与候选项版本关系) /// 参数:无(使用 `selectedVoiceId`) /// 返回:`String`,可能为 `设备异常`、`升级`、`切换`、`已是最新`、`处理` /// 异常:无 String get actionText { final sel = selectedVoiceId.value; final curId = currentDeviceVoiceId.value; final curVer = currentDeviceVoiceVersion.value; final selected = _findById(sel); if (sel == null || selected == null) return '处理'; if (wakeupVersionMismatch.value) return '设备异常'; if (_isSameWakeId(selected)) { return _isSameWakeVersion(selected) ? '已是最新' : '升级'; } return '切换'; } /// 获取当前选择项是否允许触发操作 /// 参数:无(使用 `selectedVoiceId`) /// 返回:`bool`,当 `设备异常` 或 `已是最新` 时为 `false` /// 异常:无 bool get actionEnabled { final sel = selectedVoiceId.value; final curId = currentDeviceVoiceId.value; final curVer = currentDeviceVoiceVersion.value; final selected = _findById(sel); if (sel == null || selected == null) return false; if (!isConnected.value) return false; if (wakeupVersionMismatch.value) return false; if (_isSameWakeId(selected)) { return !_isSameWakeVersion(selected); } return true; } /// 计算指定语音项的操作文案(不影响当前选择项) /// 参数:`item` 候选语音项 /// 返回:`String`,可能为 `设备异常`、`升级`、`切换`、`已是最新` /// 异常:无 String actionTextForItem(WakeupVoiceItem item) { if (wakeupVersionMismatch.value) return '设备异常'; if (_isSameWakeId(item)) { return _isSameWakeVersion(item) ? '已是最新' : '升级'; } return '切换'; } /// 计算指定语音项是否允许操作(不影响当前选择项) /// 参数:`item` 候选语音项 /// 返回:`bool`,当 `设备异常` 或 `已是最新` 时为 `false` /// 异常:无 bool actionEnabledForItem(WakeupVoiceItem item) { if (!isConnected.value) return false; if (wakeupVersionMismatch.value) return false; if (_isSameWakeId(item)) { return !_isSameWakeVersion(item); } return true; } /// 根据当前选择项触发操作 /// 参数:无 /// 返回:`Future` /// 异常:无;当不允许操作时直接返回 Future onActionPressed() async { final label = actionText; if (!actionEnabled) return; switch (label) { case '升级': await upgradeWakeVoice(); break; case '切换': await switchWakeVoice(); break; default: break; } } /// 将当前设备升级到已选择项 /// 参数:无(使用 `selectedVoiceId`) /// 返回:`Future` /// 异常:下载或升级失败会通过提示与日志反馈,不抛出 Future upgradeWakeVoice() async { final sel = _findById(selectedVoiceId.value); if (sel == null) return; await _startUpgradeByItem(sel); } /// 将当前设备切换到已选择项 /// 参数:无(使用 `selectedVoiceId`) /// 返回:`Future` /// 异常:下载或升级失败会通过提示与日志反馈,不抛出 Future switchWakeVoice() async { final sel = _findById(selectedVoiceId.value); if (sel == null) return; await _startUpgradeByItem(sel); } // 直接切换到指定语音包(页面卡片按钮使用) /// 直接切换到指定语音项 /// 参数:`item` 目标语音项 /// 返回:`Future` /// 异常:下载或升级失败会通过提示与日志反馈,不抛出 Future switchWakeVoiceTo(WakeupVoiceItem item) async { selectedVoiceId.value = item.id; await _startUpgradeByItem(item); } // 直接升级到指定语音包(页面卡片按钮使用) /// 直接升级到指定语音项 /// 参数:`item` 目标语音项 /// 返回:`Future` /// 异常:下载或升级失败会通过提示与日志反馈,不抛出 Future upgradeWakeVoiceTo(WakeupVoiceItem item) async { selectedVoiceId.value = item.id; await _startUpgradeByItem(item); } // 升级相关状态 final RxnInt upgradingVoiceId = RxnInt(); final RxInt upgradeProgress = 0.obs; /// 按指定语音项发起升级流程(下载并写入设备) /// 参数:`item` 目标语音项 /// 返回:`Future` /// 异常:下载或BLE交互失败会通过提示与日志反馈,不抛出 Future _startUpgradeByItem(WakeupVoiceItem item) async { try { if (item.packagaddress.isEmpty) { Get.snackbar('升级失败', '语音包地址为空'); return; } upgradingVoiceId.value = item.id; upgradeProgress.value = 0; final url = item.packagaddress; Logger.info('开始下载唤醒语音包: $url'); final bytes = await _downloadVoiceBytes(url); if (bytes.isEmpty) { Get.snackbar('升级失败', '语音包下载为空'); upgradingVoiceId.value = null; return; } Logger.info('下载完成,字节数: ${bytes.length},开始升级'); // 注意:MethodChannel 在 Android 端对 Uint8List 会编码为 byte[], // 而当前原生插件期望的是 List。为避免 ClassCastException,传递 List。 final ok = await bleManager.startWakeupVoicesUpgrade(bytes.toList()); if (!ok) { Get.snackbar('升级失败', 'BLE升级启动失败'); upgradingVoiceId.value = null; return; } Get.snackbar('升级已启动', '正在写入设备,请稍候'); } catch (e) { upgradingVoiceId.value = null; Logger.info('升级失败: $e'); Get.snackbar('升级失败', e.toString()); } } /// 下载语音包二进制数据 /// 参数:`url` 语音包地址 /// 返回:`Future>` 字节列表;失败返回空列表 /// 异常:网络异常会记录日志并返回空列表 Future> _downloadVoiceBytes(String url) async { try { final dio = Dio(); final response = await dio.get>( url, options: Options( responseType: ResponseType.bytes, receiveTimeout: const Duration(seconds: 60), ), ); return response.data ?? []; } catch (e) { Logger.error('下载语音包失败: $url', e); return []; } } /// 根据ID查找语音项 /// 参数:`id` 语音包ID /// 返回:`WakeupVoiceItem?` 找到返回对象,否则 `null` /// 异常:无 WakeupVoiceItem? _findById(int? id) { if (id == null) return null; for (final v in voices) { if (v.id == id) return v; } return null; } // 以下保留原有本地词功能(开关、增删) void toggleEnabled(bool value) { enabled.value = value; } void setInput(String value) { inputText.value = value; } String? validateWakeWord(String text) { final t = text.trim(); if (t.isEmpty) return '唤醒词不能为空'; if (t.length > 20) return '唤醒词过长(≤20个字符)'; if (wakeWords.contains(t)) return '重复的唤醒词'; return null; } bool addWakeWord(String text) { final err = validateWakeWord(text); if (err != null) { Get.snackbar('提示', err); return false; } if (wakeWords.length >= 5) { Get.snackbar('提示', '最多添加5个唤醒词'); return false; } wakeWords.add(text.trim()); inputText.value = ''; return true; } void removeWakeWord(String text) { wakeWords.remove(text); } Future saveChanges() async { Get.snackbar('保存成功', '唤醒词设置已更新'); } } class WakeupVoiceItem { final int id; final bool enable; final String name; final String desc; final String version; final String packagaddress; WakeupVoiceItem({ required this.id, required this.enable, required this.name, required this.desc, required this.version, required this.packagaddress, }); factory WakeupVoiceItem.fromJson(Map json) { return WakeupVoiceItem( id: (json['id'] ?? json['Id'] ?? 0) is String ? int.tryParse(json['id'] ?? json['Id']) ?? 0 : (json['id'] ?? json['Id'] ?? 0) as int, enable: (json['enable'] ?? json['Enable'] ?? false) == true || (json['enable'] ?? 'false').toString() == 'true', name: (json['name'] ?? '').toString(), desc: (json['desc'] ?? '').toString(), version: (json['version'] ?? '').toString(), packagaddress: (json['packagaddress'] ?? '').toString(), ); } // 从描述字段提取识别率与更新说明(供UI展示) } extension WakeupVoiceItemExt on WakeupVoiceController { String? extractAccuracy(WakeupVoiceItem item) { final d = item.desc; final reg = RegExp(r'(识别率|准确率)\s*([0-9]{1,3}(\.[0-9])?)%'); final m = reg.firstMatch(d); if (m != null) { return '识别率 ${m.group(2)}%'; } return null; } List extractUpdateNotes(WakeupVoiceItem item) { final d = item.desc.trim(); if (d.isEmpty) { return const ['提升识别准确率至99.2%', '优化唤醒提示音的响应', '修复复杂性延迟问题']; } // 按常见分隔符切分 final parts = d .split(RegExp(r'[;;\n\r]')) .map((e) => e.trim()) .where((e) => e.isNotEmpty) .toList(); return parts; } }