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
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;
|
|
}
|
|
}
|
|
|