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.

390 lines
12 KiB

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<BleManager>();
// 本地自定义唤醒词(原有)
final RxList<String> wakeWords = <String>['你好,深声', 'Hey Voitrans'].obs;
final RxString inputText = ''.obs;
// 服务端唤醒语音包列表
final RxList<WakeupVoiceItem> voices = <WakeupVoiceItem>[].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;
// 预览/测试模式:跳过 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();
}
}
void _bindBleObservers() {
// 同步连接状态与设备名
try {
isConnected.value = bleManager.isConnected;
final info = bleManager.deviceInfo.value;
if (info != null && info.name.isNotEmpty) {
deviceName.value = info.name;
}
// 监听连接状态变化
ever<int>(bleManager.connectionStateRx, (state) {
isConnected.value = bleManager.isConnected;
final info = bleManager.deviceInfo.value;
if (info != null && info.name.isNotEmpty) {
deviceName.value = info.name;
}
});
// 监听设备信息更新
ever<DeviceInfo?>(bleManager.deviceInfo, (info) {
if (info != null && info.name.isNotEmpty) {
deviceName.value = info.name;
}
});
// 监听升级进度
ever<int>(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<int>(bleManager.wakeupVoiceUpgradeStatus, (s) {
// 升级状态字段未定义,暂存到日志
Logger.info('BLE 升级状态码: $s');
if (s == 2) {
// 升级成功
Get.snackbar('成功', '唤醒语音升级成功');
} else if (s == 3) {
// 升级失败
Get.snackbar('错误', '唤醒语音升级失败');
}
});
} catch (e) {
Logger.warning('绑定BLE观察者失败: $e');
}
}
// 供外部设置当前设备语音包信息
void setCurrentDeviceWakeVoice({required int id, required String version}) {
currentDeviceVoiceId.value = id;
currentDeviceVoiceVersion.value = version;
// 若已加载列表,默认选中当前设备项
if (voices.isNotEmpty) {
selectedVoiceId.value = id;
}
}
Future<void> 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<Map<String, dynamic>>.from(res['voices'] as List);
voices.assignAll(list
.map((e) => WakeupVoiceItem.fromJson(e as Map<String, dynamic>))
.toList());
// 默认选择:优先当前设备项,否则第一个
if (currentDeviceVoiceId.value != null &&
voices.any((v) => v.id == currentDeviceVoiceId.value)) {
selectedVoiceId.value = currentDeviceVoiceId.value;
} else if (voices.isNotEmpty) {
selectedVoiceId.value = voices.first.id;
}
} catch (e) {
loadError.value = '加载唤醒语音失败: $e';
Get.snackbar('错误', loadError.value);
} finally {
isLoading.value = false;
}
}
// 选择语音包
void selectVoice(int id) {
selectedVoiceId.value = id;
}
// 动作按钮文案与状态
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 (curId == sel) {
if (curVer != null && curVer != selected.version) {
return '升级';
}
return '已是最新';
} else {
return '切换';
}
}
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 (curId == sel) {
return curVer != null && curVer != selected.version; // 版本不一致才可升级
} else {
return true; // 选择其他可切换
}
}
Future<void> onActionPressed() async {
final label = actionText;
if (!actionEnabled) return;
switch (label) {
case '升级':
await upgradeWakeVoice();
break;
case '切换':
await switchWakeVoice();
break;
default:
break;
}
}
Future<void> upgradeWakeVoice() async {
final sel = _findById(selectedVoiceId.value);
if (sel == null) return;
await _startUpgradeByItem(sel);
}
Future<void> switchWakeVoice() async {
final sel = _findById(selectedVoiceId.value);
if (sel == null) return;
await _startUpgradeByItem(sel);
}
// 直接切换到指定语音包(页面卡片按钮使用)
Future<void> switchWakeVoiceTo(WakeupVoiceItem item) async {
selectedVoiceId.value = item.id;
await _startUpgradeByItem(item);
}
// 直接升级到指定语音包(页面卡片按钮使用)
Future<void> upgradeWakeVoiceTo(WakeupVoiceItem item) async {
selectedVoiceId.value = item.id;
await _startUpgradeByItem(item);
}
// 升级相关状态
final RxnInt upgradingVoiceId = RxnInt();
final RxInt upgradeProgress = 0.obs;
Future<void> _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<int>。
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());
}
}
Future<List<int>> _downloadVoiceBytes(String url) async {
try {
final dio = Dio();
final response = await dio.get<List<int>>(
url,
options: Options(
responseType: ResponseType.bytes,
receiveTimeout: const Duration(seconds: 60),
),
);
return response.data ?? [];
} catch (e) {
Logger.error('下载语音包失败: $url', e);
return [];
}
}
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<void> 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<String, dynamic> 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<String> 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;
}
}