|
|
|
@ -28,6 +28,9 @@ class WakeupVoiceController extends GetxController { |
|
|
|
// 设备状态(用于页面头部展示) |
|
|
|
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; |
|
|
|
@ -54,6 +57,10 @@ class WakeupVoiceController extends GetxController { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 绑定 BLE 相关的观察者 |
|
|
|
/// 参数:无 |
|
|
|
/// 返回:无 |
|
|
|
/// 异常:发生监听绑定异常时捕获并记录日志,不抛出 |
|
|
|
void _bindBleObservers() { |
|
|
|
// 同步连接状态与设备名 |
|
|
|
try { |
|
|
|
@ -61,6 +68,7 @@ class WakeupVoiceController extends GetxController { |
|
|
|
final info = bleManager.deviceInfo.value; |
|
|
|
if (info != null && info.name.isNotEmpty) { |
|
|
|
deviceName.value = info.name; |
|
|
|
_syncDeviceWakeVersions(info); |
|
|
|
} |
|
|
|
// 监听连接状态变化 |
|
|
|
ever<int>(bleManager.connectionStateRx, (state) { |
|
|
|
@ -68,12 +76,14 @@ class WakeupVoiceController extends GetxController { |
|
|
|
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); |
|
|
|
} |
|
|
|
}); |
|
|
|
// 监听升级进度 |
|
|
|
@ -107,6 +117,10 @@ class WakeupVoiceController extends GetxController { |
|
|
|
} |
|
|
|
|
|
|
|
// 供外部设置当前设备语音包信息 |
|
|
|
/// 设置当前设备的唤醒语音包信息 |
|
|
|
/// 参数:`id` 唤醒词ID;`version` 版本字符串(如 `1.2.3.4`) |
|
|
|
/// 返回:无 |
|
|
|
/// 异常:无 |
|
|
|
void setCurrentDeviceWakeVoice({required int id, required String version}) { |
|
|
|
currentDeviceVoiceId.value = id; |
|
|
|
currentDeviceVoiceVersion.value = version; |
|
|
|
@ -116,6 +130,10 @@ class WakeupVoiceController extends GetxController { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 拉取服务端唤醒语音包列表 |
|
|
|
/// 参数:无 |
|
|
|
/// 返回:`Future<void>`,完成后 `voices`、`selectedVoiceId` 等被更新 |
|
|
|
/// 异常:网络或数据解析异常会写入 `loadError` 并弹出提示,不抛出 |
|
|
|
Future<void> fetchVoices() async { |
|
|
|
try { |
|
|
|
isLoading.value = true; |
|
|
|
@ -134,13 +152,25 @@ class WakeupVoiceController extends GetxController { |
|
|
|
.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) { |
|
|
|
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); |
|
|
|
@ -149,43 +179,138 @@ class WakeupVoiceController extends GetxController { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 解析版本字符串为四段整型数组 |
|
|
|
/// 参数:`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 (curId == sel) { |
|
|
|
if (curVer != null && curVer != selected.version) { |
|
|
|
return '升级'; |
|
|
|
if (_isSameWakeId(selected)) { |
|
|
|
return _isSameWakeVersion(selected) ? '已是最新' : '升级'; |
|
|
|
} |
|
|
|
return '已是最新'; |
|
|
|
} else { |
|
|
|
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 '切换'; |
|
|
|
} |
|
|
|
|
|
|
|
if (curId == sel) { |
|
|
|
return curVer != null && curVer != selected.version; // 版本不一致才可升级 |
|
|
|
} else { |
|
|
|
return true; // 选择其他可切换 |
|
|
|
/// 计算指定语音项是否允许操作(不影响当前选择项) |
|
|
|
/// 参数:`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; |
|
|
|
@ -202,12 +327,20 @@ class WakeupVoiceController extends GetxController { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 将当前设备升级到已选择项 |
|
|
|
/// 参数:无(使用 `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; |
|
|
|
@ -215,12 +348,20 @@ class WakeupVoiceController extends GetxController { |
|
|
|
} |
|
|
|
|
|
|
|
// 直接切换到指定语音包(页面卡片按钮使用) |
|
|
|
/// 直接切换到指定语音项 |
|
|
|
/// 参数:`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); |
|
|
|
@ -230,6 +371,10 @@ class WakeupVoiceController extends GetxController { |
|
|
|
final RxnInt upgradingVoiceId = RxnInt(); |
|
|
|
final RxInt upgradeProgress = 0.obs; |
|
|
|
|
|
|
|
/// 按指定语音项发起升级流程(下载并写入设备) |
|
|
|
/// 参数:`item` 目标语音项 |
|
|
|
/// 返回:`Future<void>` |
|
|
|
/// 异常:下载或BLE交互失败会通过提示与日志反馈,不抛出 |
|
|
|
Future<void> _startUpgradeByItem(WakeupVoiceItem item) async { |
|
|
|
try { |
|
|
|
if (item.packagaddress.isEmpty) { |
|
|
|
@ -263,6 +408,10 @@ class WakeupVoiceController extends GetxController { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 下载语音包二进制数据 |
|
|
|
/// 参数:`url` 语音包地址 |
|
|
|
/// 返回:`Future<List<int>>` 字节列表;失败返回空列表 |
|
|
|
/// 异常:网络异常会记录日志并返回空列表 |
|
|
|
Future<List<int>> _downloadVoiceBytes(String url) async { |
|
|
|
try { |
|
|
|
final dio = Dio(); |
|
|
|
@ -280,6 +429,10 @@ class WakeupVoiceController extends GetxController { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 根据ID查找语音项 |
|
|
|
/// 参数:`id` 语音包ID |
|
|
|
/// 返回:`WakeupVoiceItem?` 找到返回对象,否则 `null` |
|
|
|
/// 异常:无 |
|
|
|
WakeupVoiceItem? _findById(int? id) { |
|
|
|
if (id == null) return null; |
|
|
|
for (final v in voices) { |
|
|
|
|