diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 69afd6c7d..9bcd63069 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -24,6 +24,7 @@ class BleCommands { static const int GET_PRODUCT_COLOR = 0x03; static const int GET_BATTERY_INFO = 0x04; static const int CONTROL_CODEC = 0x05; + static const int GET_WAKEUP_VERSION = 0x08; } /// 蓝牙设备信息缓存模型 @@ -49,6 +50,10 @@ class DeviceInfo { final String firmwareProductColor; final DateTime lastUpdated; + //唤醒词版本 + final String wakeupLeftVersion; + final String wakeupRightVersion; + DeviceInfo({ required this.address, required this.name, @@ -63,6 +68,8 @@ class DeviceInfo { this.firmwareProductId = '', this.firmwareProductColor = '', DateTime? lastUpdated, + this.wakeupLeftVersion = '', + this.wakeupRightVersion = '', }) : lastUpdated = lastUpdated ?? DateTime.now(); /// 创建更新后的设备信息副本 @@ -79,6 +86,8 @@ class DeviceInfo { String? firmwareVersion, String? firmwareProductId, String? firmwareProductColor, + String? wakeupLeftVersion, + String? wakeupRightVersion, }) { return DeviceInfo( address: address ?? this.address, @@ -94,6 +103,8 @@ class DeviceInfo { firmwareProductId: firmwareProductId ?? this.firmwareProductId, firmwareProductColor: firmwareProductColor ?? this.firmwareProductColor, lastUpdated: DateTime.now(), + wakeupLeftVersion: wakeupLeftVersion ?? this.wakeupLeftVersion, + wakeupRightVersion: wakeupRightVersion ?? this.wakeupRightVersion, ); } @@ -102,7 +113,9 @@ class DeviceInfo { return 'DeviceInfo(address: $address, name: $name, ' 'battery: $batteryLevel%, L: $leftEarBattery%, R: $rightEarBattery%, Case: $caseBattery%, ' 'charging: L:$leftEarCharging, R:$rightEarCharging, C:$caseCharging, ' - 'version: $firmwareVersion)'; + 'version: $firmwareVersion, ' + 'wakeupLeftVersion: $wakeupLeftVersion, ' + 'wakeupRightVersion: $wakeupRightVersion)'; } } @@ -218,6 +231,8 @@ class BleManager extends GetxService { firmwareVersion: '', firmwareProductId: '', firmwareProductColor: '', + wakeupLeftVersion: '', + wakeupRightVersion: '', ); // 断开连接时不清除设备信息,保留最后的电量信息 // 只有在需要完全清除设备时才重置信息 @@ -337,6 +352,10 @@ class BleManager extends GetxService { print(" CONTROL_CODEC: $commandId,$data"); //_isCall.value = data['callStatus'] as bool? ?? false; break; + case BleCommands.GET_WAKEUP_VERSION: + Logger.info('lxm--处理唤醒语音版本命令'); + _updateWakeupVersionFromData(data); + break; default: Logger.warning('lxm--未处理的命令ID: $commandId,数据: $data'); print(" 未处理的命令ID: $commandId,$data"); @@ -499,6 +518,33 @@ class BleManager extends GetxService { } } + /// 更新唤醒语音版本信息 + void _updateWakeupVersionFromData(Map data) { + try { + // 确保数据中包含版本信息 + if (!data.containsKey('leftVersionDec') || + !data.containsKey('rightVersionDec')) { + Logger.warning('唤醒语音版本信息数据不完整'); + return; + } + + // 提取版本信息 + final String wakeupLeftVersion = data['leftVersionDec'] as String? ?? ''; + final String wakeupRightVersion = + data['rightVersionDec'] as String? ?? ''; + + // 更新设备信息 + _updateDeviceInfoIfNeeded( + wakeupLeftVersion: wakeupLeftVersion, + wakeupRightVersion: wakeupRightVersion, + ); + + Logger.info('设备唤醒语音版本信息已更新: 左=$wakeupLeftVersion, 右=$wakeupRightVersion'); + } catch (e) { + Logger.error('更新唤醒语音版本信息失败: ${e.toString()}'); + } + } + /// 更新设备信息(如果已存在设备) void _updateDeviceInfoIfNeeded({ int? batteryLevel, @@ -511,6 +557,8 @@ class BleManager extends GetxService { String? firmwareVersion, String? firmwareProductId, String? firmwareProductColor, + String? wakeupLeftVersion, + String? wakeupRightVersion, }) { Logger.info('lxm--_updateDeviceInfoIfNeeded被调用'); Logger.info( @@ -540,6 +588,10 @@ class BleManager extends GetxService { firmwareProductId ?? deviceInfo.value!.firmwareProductId, firmwareProductColor: firmwareProductColor ?? deviceInfo.value!.firmwareProductColor, + wakeupLeftVersion: + wakeupLeftVersion ?? deviceInfo.value!.wakeupLeftVersion, + wakeupRightVersion: + wakeupRightVersion ?? deviceInfo.value!.wakeupRightVersion, ); Logger.info('lxm--更新后的设备信息: ${deviceInfo.value}'); @@ -568,6 +620,8 @@ class BleManager extends GetxService { firmwareVersion: firmwareVersion ?? '', firmwareProductId: firmwareProductId ?? '', firmwareProductColor: firmwareProductColor ?? '', + wakeupLeftVersion: wakeupLeftVersion ?? '', + wakeupRightVersion: wakeupRightVersion ?? '', ); Logger.info('lxm--新创建的设备信息: ${deviceInfo.value}'); @@ -628,6 +682,8 @@ class BleManager extends GetxService { firmwareVersion: '', firmwareProductId: '', firmwareProductColor: '', + wakeupLeftVersion: '', + wakeupRightVersion: '', ); } diff --git a/lib/modules/wakeupvoice/controllers/wakeup_voice_controller.dart b/lib/modules/wakeupvoice/controllers/wakeup_voice_controller.dart index c316ea62d..7ef9b60c4 100644 --- a/lib/modules/wakeupvoice/controllers/wakeup_voice_controller.dart +++ b/lib/modules/wakeupvoice/controllers/wakeup_voice_controller.dart @@ -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(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(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`,完成后 `voices`、`selectedVoiceId` 等被更新 + /// 异常:网络或数据解析异常会写入 `loadError` 并弹出提示,不抛出 Future fetchVoices() async { try { isLoading.value = true; @@ -134,13 +152,25 @@ class WakeupVoiceController extends GetxController { .map((e) => WakeupVoiceItem.fromJson(e as Map)) .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?` 长度为4的数组,若解析失败返回 `null` + /// 异常:无 + List? _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 '升级'; - } - return '已是最新'; - } else { - 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; + } - if (curId == sel) { - return curVer != null && curVer != selected.version; // 版本不一致才可升级 - } else { - 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` + /// 异常:无;当不允许操作时直接返回 Future onActionPressed() async { final label = actionText; if (!actionEnabled) return; @@ -202,12 +327,20 @@ class WakeupVoiceController extends GetxController { } } + /// 将当前设备升级到已选择项 + /// 参数:无(使用 `selectedVoiceId`) + /// 返回:`Future` + /// 异常:下载或升级失败会通过提示与日志反馈,不抛出 Future upgradeWakeVoice() async { final sel = _findById(selectedVoiceId.value); if (sel == null) return; await _startUpgradeByItem(sel); } + /// 将当前设备切换到已选择项 + /// 参数:无(使用 `selectedVoiceId`) + /// 返回:`Future` + /// 异常:下载或升级失败会通过提示与日志反馈,不抛出 Future switchWakeVoice() async { final sel = _findById(selectedVoiceId.value); if (sel == null) return; @@ -215,12 +348,20 @@ class WakeupVoiceController extends GetxController { } // 直接切换到指定语音包(页面卡片按钮使用) + /// 直接切换到指定语音项 + /// 参数:`item` 目标语音项 + /// 返回:`Future` + /// 异常:下载或升级失败会通过提示与日志反馈,不抛出 Future switchWakeVoiceTo(WakeupVoiceItem item) async { selectedVoiceId.value = item.id; await _startUpgradeByItem(item); } // 直接升级到指定语音包(页面卡片按钮使用) + /// 直接升级到指定语音项 + /// 参数:`item` 目标语音项 + /// 返回:`Future` + /// 异常:下载或升级失败会通过提示与日志反馈,不抛出 Future 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` + /// 异常:下载或BLE交互失败会通过提示与日志反馈,不抛出 Future _startUpgradeByItem(WakeupVoiceItem item) async { try { if (item.packagaddress.isEmpty) { @@ -263,6 +408,10 @@ class WakeupVoiceController extends GetxController { } } + /// 下载语音包二进制数据 + /// 参数:`url` 语音包地址 + /// 返回:`Future>` 字节列表;失败返回空列表 + /// 异常:网络异常会记录日志并返回空列表 Future> _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) { diff --git a/lib/modules/wakeupvoice/views/wakeup_voice_view.dart b/lib/modules/wakeupvoice/views/wakeup_voice_view.dart index 379723c7e..9545a041f 100644 --- a/lib/modules/wakeupvoice/views/wakeup_voice_view.dart +++ b/lib/modules/wakeupvoice/views/wakeup_voice_view.dart @@ -54,11 +54,16 @@ class WakeupVoiceView extends GetView { } // 顶部渐变头部 + /// 构建顶部设备状态头部 + /// 参数:`isDark` 是否深色模式 + /// 返回:`Widget` 构建好的头部组件 + /// 异常:无 Widget _buildHeader(bool isDark) { final connected = controller.isConnected.value; final deviceName = controller.deviceName.value.isEmpty ? '未连接设备' : controller.deviceName.value; + final mismatch = controller.wakeupVersionMismatch.value; return Container( decoration: const BoxDecoration( gradient: LinearGradient( @@ -108,6 +113,23 @@ class WakeupVoiceView extends GetView { Text(connected ? '已连接' : '未连接', style: const TextStyle( color: Colors.white70, fontSize: 12)), + if (mismatch) ...[ + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.2), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.redAccent), + ), + child: const Text('设备异常', + style: TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500)), + ), + ] ], ) ], @@ -121,6 +143,10 @@ class WakeupVoiceView extends GetView { } // 当前唤醒词卡片 + /// 构建当前设备唤醒词卡片 + /// 参数:`isDark` 是否深色模式 + /// 返回:`Widget` 构建好的卡片 + /// 异常:无 Widget _buildCurrentWakeWordCard(bool isDark) { final curId = controller.currentDeviceVoiceId.value; final curVer = controller.currentDeviceVoiceVersion.value; @@ -146,17 +172,33 @@ class WakeupVoiceView extends GetView { ], ), const SizedBox(height: 8), - _buildVoiceCard( - isDark: isDark, - item: curItem, - isCurrent: true, - hasUpdate: _hasUpdateForCurrent(curItem), - ), + if (curItem != null) + _buildVoiceCard( + isDark: isDark, + item: curItem, + isCurrent: true, + hasUpdate: curItem != null && + controller.actionTextForItem(curItem) == '升级', + ), + // 版本一致的唤醒词放在“当前唤醒词”下面 + ...controller.voices + .where((v) => controller.actionTextForItem(v) == '已是最新') + .where((v) => !identical(v, curItem)) + .map((v) => _buildVoiceCard( + isDark: isDark, + item: v, + isCurrent: true, + hasUpdate: false, + )), ], ); } // 可用唤醒词列表 + /// 构建可用唤醒词列表 + /// 参数:`isDark` 是否深色模式 + /// 返回:`Widget` 构建好的列表 + /// 异常:无 Widget _buildAvailableWakeWords(bool isDark) { final curId = controller.currentDeviceVoiceId.value; return Column( @@ -168,24 +210,23 @@ class WakeupVoiceView extends GetView { if (controller.loadError.value.isNotEmpty) Text(controller.loadError.value, style: const TextStyle(color: Colors.red)), - ...controller.voices.where((v) => v.id != curId).map( - (v) => _buildVoiceCard(isDark: isDark, item: v, isCurrent: false)), - // 如果存在更新项(同ID但版本更高),也在此展示 + // 仅展示非“已是最新”的可用唤醒词(排除当前使用版本) ...controller.voices - .where((v) => - v.id == curId && - _versionHigherThan( - v.version, controller.currentDeviceVoiceVersion.value)) + .where((v) => controller.actionTextForItem(v) != '已是最新') .map((v) => _buildVoiceCard( isDark: isDark, item: v, isCurrent: false, - hasUpdate: true, + hasUpdate: controller.actionTextForItem(v) == '升级', )), ], ); } + /// 比较版本是否更高(三段比较) + /// 参数:`a` 候选版本;`b` 当前版本(可空) + /// 返回:`bool` 若 `a` 高于 `b` 返回 `true` + /// 异常:无 bool _versionHigherThan(String a, String? b) { if (b == null) return true; // 简易版本比较:按点位分割逐段比较 @@ -199,12 +240,20 @@ class WakeupVoiceView extends GetView { return false; } + /// 判断当前卡片是否存在可升级版本 + /// 参数:`curItem` 当前项(可空) + /// 返回:`bool` 存在升级返回 `true` + /// 异常:无 bool _hasUpdateForCurrent(WakeupVoiceItem? curItem) { if (curItem == null) return false; final curVer = controller.currentDeviceVoiceVersion.value; return _versionHigherThan(curItem.version, curVer); } + /// 构建通用语音卡片 + /// 参数:`isDark` 是否深色模式;`item` 语音项;`isCurrent` 是否当前项;`hasUpdate` 是否升级卡片 + /// 返回:`Widget` 卡片组件 + /// 异常:无 Widget _buildVoiceCard({ required bool isDark, required WakeupVoiceItem? item, @@ -212,28 +261,33 @@ class WakeupVoiceView extends GetView { bool hasUpdate = false, }) { if (item == null) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: isDark ? Colors.grey[900] : Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: const Text('未设置'), - ); + if (isCurrent) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: const Text('未设置'), + ); + } else { + return const SizedBox.shrink(); + } } final accuracyText = controller.extractAccuracy(item) ?? '识别率 -'; final showCheck = isCurrent && !hasUpdate; final actionColor = hasUpdate ? const Color(0xFFFF7A00) : const Color(0xFF6A5AE0); - final actionText = hasUpdate ? '更新唤醒词' : '切换唤醒词'; + final actionText = controller.actionTextForItem(item); + final actionEnabled = controller.actionEnabledForItem(item); final isUpgrading = item != null && controller.upgradingVoiceId.value == item.id; final progress = controller.upgradeProgress.value; @@ -355,12 +409,12 @@ class WakeupVoiceView extends GetView { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), - onPressed: isUpgrading + onPressed: isUpgrading || !actionEnabled ? null : () { - if (hasUpdate) { + if (actionText == '升级') { controller.upgradeWakeVoiceTo(item); - } else { + } else if (actionText == '切换') { controller.switchWakeVoiceTo(item); } }, diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt index 44bb4f630..f3aa0b9ff 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt @@ -421,7 +421,30 @@ class BleCommandSender { callback?.notifyDeviceInfoReceived(cmdType, resultMap) } - + BleConst.CMD_GET_WAKEUP_VERSION -> { + val left0 = data[3].toInt() and 0xFF + val left1 = data[4].toInt() and 0xFF + val left2 = data[5].toInt() and 0xFF + val left3 = data[6].toInt() and 0xFF + val right0 = data[7].toInt() and 0xFF + val right1 = data[8].toInt() and 0xFF + val right2 = data[9].toInt() and 0xFF + val right3 = data[10].toInt() and 0xFF + val leftVersionDec = "$left0.$left1.$left2.$left3" + val rightVersionDec = "$right0.$right1.$right2.$right3" + val leftVersionHex = String.format("0x%02X.0x%02X.0x%02X.0x%02X", left0, left1, left2, left3) + val rightVersionHex = String.format("0x%02X.0x%02X.0x%02X.0x%02X", right0, right1, right2, right3) + val resultMap = mutableMapOf( + "success" to true, + "command" to cmdType, + "leftVersionDec" to leftVersionDec, + "rightVersionDec" to rightVersionDec, + "leftVersionHex" to leftVersionHex, + "rightVersionHex" to rightVersionHex + ) + Log.d(TAG, "获取唤醒词版本号:leftVersionDec: $leftVersionDec,rightVersionDec: $rightVersionDec") + callback?.notifyDeviceInfoReceived(cmdType, resultMap) + } else -> { // 对于未知命令,返回原始数据 val resultMap = mapOf( diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt index b028617cc..50e793c63 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt @@ -86,8 +86,8 @@ object BleConst { const val CMD_VOLUME_UP = 0x06 /** 音量减小 */ const val CMD_VOLUME_DOWN = 0x07 - /** A2DP播放 */ - const val CMD_PLAY_A2DP = 0x08 + /** 获取唤醒词版本 */ + const val CMD_GET_WAKEUP_VERSION = 0x08 /** 唤醒指令 */ const val CMD_WAKE_UP = 0x11 diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index e34917374..b61fb4ef0 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -837,6 +837,7 @@ object BleService { } g.discoverServices() + } newState == BluetoothProfile.STATE_DISCONNECTED -> { @@ -934,7 +935,9 @@ object BleService { setupNotifications(g, callAudioChar) setupNotifications(g, audioChar) Log.i(TAG, "通话音频服务特征找到并设置通知") - + Handler(Looper.getMainLooper()).postDelayed({ + getDeviceWakeupVersion() + }, 1000) // 延迟3秒再请求版本,确保通知描述符写入完成 } else { Log.w(TAG, "通话音频服务特征未找到") } @@ -954,6 +957,7 @@ object BleService { } // 在服务发现完成后,请求最大MTU //requestMaxMtu(g) + } override fun onCharacteristicChanged(g: BluetoothGatt, c: BluetoothGattCharacteristic) { @@ -1520,8 +1524,9 @@ object BleService { * @return 是否成功发送命令 */ fun playA2DP(): Boolean { - Log.i(TAG, "A2DP播放...") - return commandSender.sendCommand(BleConst.CMD_PLAY_A2DP.toByte()) + // Log.i(TAG, "A2DP播放...") + // return commandSender.sendCommand(BleConst.CMD_PLAY_A2DP.toByte()) + return true } /** * 打开编码器 @@ -1585,6 +1590,19 @@ object BleService { return commandSender.sendCommand(BleConst.CMD_VOLUME_DOWN.toByte()) } + + /** + * 获取设备唤醒词版本 + * @return 是否成功发送命令 + */ + fun getDeviceWakeupVersion(): Boolean { + Log.i(TAG, "获取设备唤醒词版本...") + val cmdData = byteArrayOf( + 0xFF.toByte() + ) + return commandSender.sendCommand(BleConst.CMD_GET_WAKEUP_VERSION.toByte(),cmdData) + } + // ====================================================================================================== // 连接状态和工具方法 // ====================================================================================================== diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeupVoicesUpgradeSender.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeupVoicesUpgradeSender.kt index 4ffc124bd..fef5269d3 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeupVoicesUpgradeSender.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeupVoicesUpgradeSender.kt @@ -114,6 +114,7 @@ class BleWakeupVoicesUpgradeSender { fun setCallback(callback: CommandSenderCallback) { this.callback = callback } + /** * 发送命令到设备 - 通用方法 @@ -229,6 +230,27 @@ class BleWakeupVoicesUpgradeSender { // 握手/数据包响应都是以0xBB为头 if (header == (BleConst.DEVICE_RESPONSE_HEADER.toInt() and 0xFF)) { when (cmd) { + 0x08 -> { // 版本号查询回复: 0xBB + 0x08 + 0x08 + 左耳版本号(4Byte) + 右耳版本号(4Byte) + CRC + Log.d(TAG, "liwei------------- 版本号查询回复: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}") + if (data.size >= 12) { + val left0 = data[3].toInt() and 0xFF + val left1 = data[4].toInt() and 0xFF + val left2 = data[5].toInt() and 0xFF + val left3 = data[6].toInt() and 0xFF + val right0 = data[7].toInt() and 0xFF + val right1 = data[8].toInt() and 0xFF + val right2 = data[9].toInt() and 0xFF + val right3 = data[10].toInt() and 0xFF + val leftVersionDec = "$left0.$left1.$left2.$left3" + val rightVersionDec = "$right0.$right1.$right2.$right3" + val leftVersionHex = String.format("0x%02X.0x%02X.0x%02X.0x%02X", left0, left1, left2, left3) + val rightVersionHex = String.format("0x%02X.0x%02X.0x%02X.0x%02X", right0, right1, right2, right3) + Log.i(TAG, "liwei------------- 左耳版本号: $leftVersionDec ($leftVersionHex)") + Log.i(TAG, "liwei------------- 右耳版本号: $rightVersionDec ($rightVersionHex)") + } else { + Log.e(TAG, "liwei------------- 版本号查询回复数据长度异常: ${data.size}") + } + } 0x20 -> { // 握手确认: 0xBB + 0x20 + 0x05 + OTA包总字节数(4Byte) + 0x01 + CRC Log.d(TAG, "liwei------------- 握手响应成功") handshakeAcked = true @@ -442,9 +464,6 @@ class BleWakeupVoicesUpgradeSender { return crc.toByte() } - - - // ========================================== // OTA封包/发送辅助 // ==========================================