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