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.

543 lines
18 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;
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<int>(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<DeviceInfo?>(bleManager.deviceInfo, (info) {
if (info != null && info.name.isNotEmpty) {
deviceName.value = info.name;
_syncDeviceWakeVersions(info);
}
});
// 监听升级进度
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');
}
}
// 供外部设置当前设备语音包信息
/// 设置当前设备的唤醒语音包信息
/// 参数:`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<void>`,完成后 `voices`、`selectedVoiceId` 等被更新
/// 异常:网络或数据解析异常会写入 `loadError` 并弹出提示,不抛出
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 (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<int>?` 长度为4的数组,若解析失败返回 `null`
/// 异常:无
List<int>? _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<void>`
/// 异常:无;当不允许操作时直接返回
Future<void> onActionPressed() async {
final label = actionText;
if (!actionEnabled) return;
switch (label) {
case '升级':
await upgradeWakeVoice();
break;
case '切换':
await switchWakeVoice();
break;
default:
break;
}
}
/// 将当前设备升级到已选择项
/// 参数:无(使用 `selectedVoiceId`)
/// 返回:`Future<void>`
/// 异常:下载或升级失败会通过提示与日志反馈,不抛出
Future<void> upgradeWakeVoice() async {
final sel = _findById(selectedVoiceId.value);
if (sel == null) return;
await _startUpgradeByItem(sel);
}
/// 将当前设备切换到已选择项
/// 参数:无(使用 `selectedVoiceId`)
/// 返回:`Future<void>`
/// 异常:下载或升级失败会通过提示与日志反馈,不抛出
Future<void> switchWakeVoice() async {
final sel = _findById(selectedVoiceId.value);
if (sel == null) return;
await _startUpgradeByItem(sel);
}
// 直接切换到指定语音包(页面卡片按钮使用)
/// 直接切换到指定语音项
/// 参数:`item` 目标语音项
/// 返回:`Future<void>`
/// 异常:下载或升级失败会通过提示与日志反馈,不抛出
Future<void> switchWakeVoiceTo(WakeupVoiceItem item) async {
selectedVoiceId.value = item.id;
await _startUpgradeByItem(item);
}
// 直接升级到指定语音包(页面卡片按钮使用)
/// 直接升级到指定语音项
/// 参数:`item` 目标语音项
/// 返回:`Future<void>`
/// 异常:下载或升级失败会通过提示与日志反馈,不抛出
Future<void> upgradeWakeVoiceTo(WakeupVoiceItem item) async {
selectedVoiceId.value = item.id;
await _startUpgradeByItem(item);
}
// 升级相关状态
final RxnInt upgradingVoiceId = RxnInt();
final RxInt upgradeProgress = 0.obs;
/// 按指定语音项发起升级流程(下载并写入设备)
/// 参数:`item` 目标语音项
/// 返回:`Future<void>`
/// 异常:下载或BLE交互失败会通过提示与日志反馈,不抛出
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());
}
}
/// 下载语音包二进制数据
/// 参数:`url` 语音包地址
/// 返回:`Future<List<int>>` 字节列表;失败返回空列表
/// 异常:网络异常会记录日志并返回空列表
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 [];
}
}
/// 根据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<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;
}
}