diff --git a/lib/ble/ble_service.dart b/lib/ble/ble_service.dart index f15ed34..2826023 100644 --- a/lib/ble/ble_service.dart +++ b/lib/ble/ble_service.dart @@ -5,6 +5,7 @@ import 'package:starwork_flutter/base/app_logger.dart'; import 'package:starwork_flutter/ble/ble_config.dart'; import 'package:starwork_flutter/ble/command/base/base_ble_command.dart'; import 'package:starwork_flutter/ble/command/ble_command_manager.dart'; +import 'package:starwork_flutter/ble/command/request/ble_cmd_add_admin.dart'; import 'package:starwork_flutter/ble/command/request/ble_cmd_get_private_key.dart'; import 'package:starwork_flutter/ble/command/request/ble_cmd_get_public_key.dart'; import 'package:starwork_flutter/ble/command/request/ble_cmd_read_lock_status.dart'; @@ -289,7 +290,14 @@ class BleService { /// 监听mtu变化 void _handleListenMtuChange(int mtu) { _mtu = mtu; - BaseBleCommand.MAX_PACKET_DATA_SIZE = mtu; + // MTU包含了协议开销,需要减去包头和包尾的长度 + // 包头: 4字节固定值 + 1字节包类型 + 2字节序号 + 1字节标识 + 4字节长度 = 12字节 + // 包尾: 2字节CRC = 2字节 + // 蓝牙协议还有额外开销,实际可用数据长度需要进一步减少 + // 根据错误信息,MTU为185时,实际最大数据长度应该是182字节 + // 所以我们保守计算,减去更多字节以确保兼容性 + BaseBleCommand.MAX_PACKET_DATA_SIZE = mtu - 18; + AppLogger.info('📏 MTU更新为: $mtu, 最大数据长度设置为: ${BaseBleCommand.MAX_PACKET_DATA_SIZE}'); } /// 用于存储分包接收的数据 @@ -303,11 +311,11 @@ class BleService { if (data.isNotEmpty) { // ✨✨✨ 处理分包数据重组 ✨✨✨ List? completePacket = _reassemblePacket(data); - + if (completePacket != null) { // 如果有完整数据包,进行处理 dynamic result = bleCommandManager.handleResponse(completePacket); - + if (result != null) { // 触发命令响应等待器 _triggerCommandResponseWaiters(result); @@ -326,38 +334,37 @@ class BleService { List? _reassemblePacket(List data) { try { // 1. 检查是否是包头开始的数据 - if (data.length >= 4 && - data[0] == BaseBleCommand.PACKET_HEADER[0] && - data[1] == BaseBleCommand.PACKET_HEADER[1] && - data[2] == BaseBleCommand.PACKET_HEADER[2] && + if (data.length >= 4 && + data[0] == BaseBleCommand.PACKET_HEADER[0] && + data[1] == BaseBleCommand.PACKET_HEADER[1] && + data[2] == BaseBleCommand.PACKET_HEADER[2] && data[3] == BaseBleCommand.PACKET_HEADER[3]) { - // 这是一个新包的开始 // 首先检查是否已有未完成的包,如果有则丢弃(超时处理) if (_pendingPackets.isNotEmpty) { AppLogger.warn('⚠️ 检测到新包开始,丢弃未完成的包'); _pendingPackets.clear(); } - + // 解析包头信息以获取预期长度 if (data.length >= 12) { // 解析包序号 (大端序) int packetSequence = (data[5] << 8) | data[6]; - + // 解析数据长度 (大端序: 高16位加密后长度 + 低16位原始长度) int combinedLength = (data[8] << 24) | (data[9] << 16) | (data[10] << 8) | data[11]; int encryptedDataLength = (combinedLength >> 16) & 0xFFFF; int originalDataLength = combinedLength & 0xFFFF; - + // 根据加密类型确定实际数据长度 int encryptType = data[7] & 0x0F; int actualDataLength = (encryptType == BaseBleCommand.ENCRYPT_TYPE_PLAIN) ? originalDataLength : encryptedDataLength; - + // 计算完整包的预期长度 int expectedTotalLength = 12 + actualDataLength + 2; // 包头到数据长度(12) + 数据块 + CRC(2) - + AppLogger.debug('📦 新包开始: 序号=$packetSequence, 预期长度=$expectedTotalLength, 实际长度=${data.length}'); - + if (data.length == expectedTotalLength) { // 数据完整,直接返回 AppLogger.debug('✅ 数据包完整,无需重组'); @@ -383,27 +390,27 @@ class BleService { // 获取第一个(也是唯一一个)未完成的包 int packetSequence = _pendingPackets.keys.first; List pendingData = _pendingPackets[packetSequence]!; - + // 追加数据 pendingData.addAll(data); _pendingPackets[packetSequence] = pendingData; - + AppLogger.debug('📥 追加数据片段: 当前长度${pendingData.length}字节'); - + // 尝试解析包头信息以检查是否完整 if (pendingData.length >= 12) { // 解析数据长度 int combinedLength = (pendingData[8] << 24) | (pendingData[9] << 16) | (pendingData[10] << 8) | pendingData[11]; int encryptedDataLength = (combinedLength >> 16) & 0xFFFF; int originalDataLength = combinedLength & 0xFFFF; - + // 根据加密类型确定实际数据长度 int encryptType = pendingData[7] & 0x0F; int actualDataLength = (encryptType == BaseBleCommand.ENCRYPT_TYPE_PLAIN) ? originalDataLength : encryptedDataLength; - + // 计算完整包的预期长度 int expectedTotalLength = 12 + actualDataLength + 2; // 包头到数据长度(12) + 数据块 + CRC(2) - + if (pendingData.length == expectedTotalLength) { // 数据完整,返回完整包并清理待处理队列 AppLogger.debug('✅ 数据包重组完成: 序号=$packetSequence, 长度=$expectedTotalLength字节'); @@ -451,17 +458,54 @@ class BleService { _cleanupCommandWaiter(commandKey); _commandIdToKeyMap.remove(commandId); return; + } else { + // 如果找不到对应的completer或者已经完成,从映射中移除 + _commandIdToKeyMap.remove(commandId); } } - // 如果没有精确匹配,回退到原来的逻辑(完成所有未完成的等待器) - _commandResponseWaiters.forEach((key, completer) { - if (!completer.isCompleted) { - AppLogger.debug('🔔 触发命令响应(回退模式): $key'); - completer.complete(response); - completedKeys.add(key); + // 如果没有精确匹配,在重发场景中,我们应该完成所有匹配的等待器 + // 而不是只完成最近的一个 + if (_commandResponseWaiters.isNotEmpty) { + // 查找所有与该命令ID相关的等待器 + List matchingKeys = []; + _commandIdToKeyMap.forEach((id, key) { + if (id == commandId) { + matchingKeys.add(key); + } + }); + + // 完成所有匹配的等待器 + for (String key in matchingKeys) { + Completer? completer = _commandResponseWaiters[key]; + if (completer != null && !completer.isCompleted) { + AppLogger.debug('🔔 触发命令响应(精确匹配模式): $key'); + completer.complete(response); + completedKeys.add(key); + // 清理已完成的等待器 + _cleanupCommandWaiter(key); + } } - }); + + // 从命令ID映射中移除已处理的键 + _commandIdToKeyMap.removeWhere((id, key) => id == commandId && completedKeys.contains(key)); + + // 如果找到了匹配的等待器,直接返回 + if (matchingKeys.isNotEmpty) { + return; + } + + // 如果仍然没有找到匹配的等待器,回退到原来的逻辑(完成所有未完成的等待器) + // 但在重发场景中,我们只完成最近的等待器 + // 获取最近添加的等待器(基于key的创建时间) + String mostRecentKey = _commandResponseWaiters.keys.last; + Completer? completer = _commandResponseWaiters[mostRecentKey]; + if (completer != null && !completer.isCompleted) { + AppLogger.debug('🔔 触发命令响应(最近模式): $mostRecentKey'); + completer.complete(response); + completedKeys.add(mostRecentKey); + } + } // 清理已完成的等待器 for (String key in completedKeys) { @@ -477,8 +521,11 @@ class BleService { Duration timeout = const Duration(seconds: 10), bool autoConnectIfNeeded = true, Duration searchTimeoutIfNeeded = const Duration(seconds: 15), + int maxRetries = 3, // 最大重试次数 + int retryCount = 0, // 当前重试次数 + String? existingCommandKey, // 用于重发时传递已有的命令键 }) async { - AppLogger.highlight('✨✨✨ 🚀 开始发送蓝牙命令: ${command.runtimeType} ✨✨✨'); + AppLogger.highlight('✨✨✨ 🚀 开始发送蓝牙命令: ${command.runtimeType} (重试次数: $retryCount) ✨✨✨'); try { // 1. 检查连接状态 @@ -490,12 +537,61 @@ class BleService { ); if (!isConnected) { + AppLogger.error('❌ 设备未连接,无法发送命令'); throw Exception('设备未连接,无法发送命令'); } + // 1.1 检查设备是否仍然连接(在重发时特别重要) + if (_connectedDevice != null) { + try { + // 检查设备连接状态 + BluetoothConnectionState currentState = await _connectedDevice!.connectionState.first; + if (currentState != BluetoothConnectionState.connected) { + AppLogger.warn('⚠️ 设备连接状态异常,当前状态: $currentState'); + // 尝试重新连接 + isConnected = await _ensureDeviceConnected( + targetDeviceId: targetDeviceId, + targetDeviceName: targetDeviceName, + autoConnect: true, // 强制重新连接 + searchTimeout: searchTimeoutIfNeeded, + ); + + if (!isConnected) { + AppLogger.error('❌ 设备重新连接失败,无法发送命令'); + throw Exception('设备重新连接失败,无法发送命令'); + } + } + } catch (e) { + AppLogger.warn('⚠️ 检查设备连接状态时出错: $e'); + // 出错时也尝试重新连接 + isConnected = await _ensureDeviceConnected( + targetDeviceId: targetDeviceId, + targetDeviceName: targetDeviceName, + autoConnect: true, // 强制重新连接 + searchTimeout: searchTimeoutIfNeeded, + ); + + if (!isConnected) { + AppLogger.error('❌ 设备重新连接失败,无法发送命令'); + throw Exception('设备重新连接失败,无法发送命令'); + } + } + } + // 2. 检查写入特征值 if (_writeCharacteristic == null) { - throw Exception('写入特征值未初始化'); + AppLogger.error('❌ 写入特征值未初始化'); + // 尝试重新设置设备连接 + if (_connectedDevice != null) { + AppLogger.info('🔧 尝试重新设置设备连接...'); + isConnected = await _setupDeviceConnection(_connectedDevice!); + if (!isConnected || _writeCharacteristic == null) { + AppLogger.error('❌ 重新设置设备连接失败'); + throw Exception('写入特征值未初始化'); + } + } else { + throw Exception('写入特征值未初始化'); + } } // 3. 构建命令数据包 @@ -503,16 +599,22 @@ class BleService { AppLogger.info('📦 命令数据包数量: ${packets.length}'); // 4. 设置命令响应等待器 - String commandKey = _generateCommandKey(command); + String commandKey = existingCommandKey ?? _generateCommandKey(command); Completer responseCompleter = Completer(); _commandResponseWaiters[commandKey] = responseCompleter; - // 5. 如果命令有cmdId静态字段,将其与命令键关联 - _registerCommandId(command, commandKey); + // 5. 如果命令有cmdId静态字段,将其与命令键关联(仅在非重发时) + if (existingCommandKey == null) { + _registerCommandId(command, commandKey); + } else if (command is BleCmdAddAdmin) { + // 对于重发的命令,我们也需要更新命令ID映射 + _registerCommandId(command, commandKey); + } // 6. 设置超时定时器 Timer timeoutTimer = Timer(timeout, () { if (!responseCompleter.isCompleted) { + AppLogger.warn('⏰ 命令响应超时: $commandKey'); _cleanupCommandWaiter(commandKey); responseCompleter.completeError(TimeoutException('命令响应超时', timeout)); } @@ -524,6 +626,15 @@ class BleService { List packet = packets[i]; AppLogger.debug('📤 发送第${i + 1}个数据包,数据包: (${packet.toString()},长度:${packet.length}字节)'); + // 检查设备是否仍然连接 + if (_connectedDevice != null) { + BluetoothConnectionState currentState = await _connectedDevice!.connectionState.first; + if (currentState != BluetoothConnectionState.connected) { + AppLogger.error('❌ 设备在发送数据包时断开连接'); + throw Exception('设备在发送数据包时断开连接'); + } + } + await _writeCharacteristic!.write(packet, withoutResponse: false); // 在多包发送时稍微延迟 @@ -537,6 +648,41 @@ class BleService { // 8. 等待响应 T? response = await responseCompleter.future; + // 9. 检查是否需要重发(通用重发机制) + // 只对实现了RetryableBleCommand接口的命令进行重发检查 + if (retryCount < maxRetries && command is RetryableBleCommand) { + // 明确转换类型以避免静态分析错误 + final retryableCommand = command as RetryableBleCommand; + if (retryableCommand.shouldRetry(response)) { + AppLogger.info('🔄 命令${command.runtimeType}需要重发,准备创建重发命令 (重试次数: ${retryCount + 1}/${maxRetries})'); + + try { + // 创建重发命令 + BaseBleCommand retryCommand = retryableCommand.createRetryCommand(response); + + // 重新发送命令,传递现有的命令键以确保响应正确处理 + AppLogger.info('🔄 使用重发机制重新发送命令'); + return await sendCommand( + command: retryCommand, + targetDeviceId: targetDeviceId, + targetDeviceName: targetDeviceName, + timeout: timeout, + autoConnectIfNeeded: true, + // 重发时也需要自动连接 + maxRetries: maxRetries, + retryCount: retryCount + 1, + // 增加重试次数 + existingCommandKey: commandKey, // 传递现有的命令键 + ); + } catch (retryError, retryStackTrace) { + AppLogger.error('❌ 创建或发送重发命令失败', error: retryError, stackTrace: retryStackTrace); + // 如果创建重发命令失败,返回原始响应 + AppLogger.highlight('✨✨✨ ✅ 命令发送完成,收到应答: $response ✨✨✨'); + return response; + } + } + } + AppLogger.highlight('✨✨✨ ✅ 命令发送成功,收到应答: $response ✨✨✨'); return response; } catch (e, stackTrace) { @@ -552,16 +698,26 @@ class BleService { BleCmdGetPublicKey() => BleCmdGetPublicKey.cmdId, BleCmdGetPrivateKey() => BleCmdGetPrivateKey.cmdId, BleCmdReadLockStatus() => BleCmdReadLockStatus.cmdId, + BleCmdAddAdmin() => BleCmdAddAdmin.cmdId, // 可在此添加更多命令类型 // BleCmdAnother() => BleCmdAnother.cmdId, _ => null, // 默认情况:无法识别的命令类型 }; if (commandId != null) { + // 检查是否已存在该命令ID的映射 + if (_commandIdToKeyMap.containsKey(commandId)) { + // 如果已存在,我们需要更新映射而不是覆盖 + // 这在重发场景中很重要,确保最新的命令键能正确映射 + AppLogger.debug( + '🔄 更新命令ID映射: 0x${commandId.toRadixString(16).padLeft(4, '0')} -> $commandKey (原键: ${_commandIdToKeyMap[commandId]})', + ); + } else { + AppLogger.debug( + '📝 命令ID映射: 0x${commandId.toRadixString(16).padLeft(4, '0')} -> $commandKey', + ); + } _commandIdToKeyMap[commandId] = commandKey; - AppLogger.debug( - '📝 命令ID映射: 0x${commandId.toRadixString(16).padLeft(4, '0')} -> $commandKey', - ); } else { AppLogger.debug('❓ 未知命令类型,无法注册 commandId: $commandKey'); } @@ -614,11 +770,16 @@ class BleService { bool autoConnect = true, Duration searchTimeout = const Duration(seconds: 15), }) async { + AppLogger.debug('🔍 检查设备连接状态...'); + // 1. 检查当前连接状态 if (_connectedDevice != null && _bluetoothConnectionState == BluetoothConnectionState.connected) { + AppLogger.debug('📱 当前已连接设备: ${_connectedDevice!.platformName} (${_connectedDevice!.remoteId})'); + // 如果指定了目标设备ID,检查是否匹配 if (targetDeviceId != null && _connectedDevice!.remoteId.toString() != targetDeviceId) { AppLogger.info('🔄 当前连接的设备与目标不匹配,需要切换连接'); + AppLogger.info('📱 当前设备: ${_connectedDevice!.remoteId},目标设备: $targetDeviceId'); await _connectedDevice!.disconnect(); _connectedDevice = null; _writeCharacteristic = null; @@ -627,6 +788,12 @@ class BleService { AppLogger.info('✅ 设备已连接,无需重新连接'); return true; } + } else if (_connectedDevice != null) { + AppLogger.warn('⚠️ 设备对象存在但连接状态异常: $_bluetoothConnectionState'); + // 清理异常状态 + _connectedDevice = null; + _writeCharacteristic = null; + _subscriptionCharacteristic = null; } // 2. 如果不允许自动连接,直接返回失败 @@ -637,11 +804,19 @@ class BleService { // 3. 尝试查找已连接的设备 List connectedDevices = FlutterBluePlus.connectedDevices; + AppLogger.debug('🔍 当前已连接设备数量: ${connectedDevices.length}'); + if (targetDeviceId != null) { for (BluetoothDevice device in connectedDevices) { + AppLogger.debug('📱 检查已连接设备: ${device.platformName} (${device.remoteId})'); if (device.remoteId.toString() == targetDeviceId) { - AppLogger.info('🔗 找到已连接的目标设备'); - return await _setupDeviceConnection(device); + AppLogger.info('🔗 找到已连接的目标设备: ${device.platformName}'); + bool isConnected = await _setupDeviceConnection(device); + if (isConnected) { + return true; + } else { + AppLogger.warn('⚠️ 已连接设备设置失败,继续搜索...'); + } } } } @@ -714,10 +889,36 @@ class BleService { try { AppLogger.info('🔗 正在连接设备: ${device.platformName}'); - await device.connect(timeout: const Duration(seconds: 10)); + // 尝试连接设备,最多重试3次 + int maxRetries = 3; + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + AppLogger.info('🔗 尝试连接设备 (第$attempt/$maxRetries次)'); + await device.connect(timeout: const Duration(seconds: 10)); - // 设置连接设备 - return await _setupDeviceConnection(device); + // 连接成功后设置设备连接 + bool isConnected = await _setupDeviceConnection(device); + if (isConnected) { + return true; + } else { + AppLogger.warn('⚠️ 设备连接设置失败 (第$attempt/$maxRetries次)'); + if (attempt < maxRetries) { + AppLogger.info('⏳ 等待2秒后重试...'); + await Future.delayed(const Duration(seconds: 2)); + } + } + } catch (e) { + AppLogger.warn('⚠️ 连接设备失败 (第$attempt/$maxRetries次): $e'); + if (attempt < maxRetries) { + AppLogger.info('⏳ 等待2秒后重试...'); + await Future.delayed(const Duration(seconds: 2)); + } else { + rethrow; // 最后一次尝试失败,重新抛出异常 + } + } + } + + return false; } catch (e) { AppLogger.error('❌ 连接设备失败: ${device.platformName}', error: e); return false; @@ -747,6 +948,7 @@ class BleService { // 查找目标服务 BluetoothService? targetService; for (BluetoothService service in services) { + AppLogger.debug('📱 发现服务: ${service.uuid}'); if (service.uuid == BleConfig.serviceId) { targetService = service; break; @@ -754,6 +956,11 @@ class BleService { } if (targetService == null) { + AppLogger.error('❌ 未找到目标服务: ${BleConfig.serviceId}'); + // 打印所有发现的服务以便调试 + for (BluetoothService service in services) { + AppLogger.debug('📱 可用服务: ${service.uuid}'); + } throw Exception('未找到目标服务: ${BleConfig.serviceId}'); } @@ -763,7 +970,9 @@ class BleService { BluetoothCharacteristic? writeChar; BluetoothCharacteristic? subscribeChar; + AppLogger.debug('🔍 目标服务中的特征值数量: ${targetService.characteristics.length}'); for (BluetoothCharacteristic characteristic in targetService.characteristics) { + AppLogger.debug('📱 发现特征值: ${characteristic.uuid}'); if (characteristic.uuid == BleConfig.characteristicIdWrite) { writeChar = characteristic; AppLogger.info('✅ 找到写入特征值: ${characteristic.uuid}'); @@ -773,8 +982,26 @@ class BleService { } } + // 如果没有找到特征值,打印所有可用的特征值 if (writeChar == null || subscribeChar == null) { - throw Exception('未找到所需的特征值'); + AppLogger.warn('⚠️ 未找到所需的特征值,当前服务中的所有特征值:'); + for (BluetoothCharacteristic characteristic in targetService.characteristics) { + AppLogger.warn(' 📱 特征值: ${characteristic.uuid}, 属性: ${characteristic.properties}'); + } + + // 尝试使用找到的特征值 + if (writeChar == null && subscribeChar != null) { + writeChar = subscribeChar; + AppLogger.warn('⚠️ 未找到写入特征值,使用订阅特征值作为写入特征值'); + } else if (subscribeChar == null && writeChar != null) { + subscribeChar = writeChar; + AppLogger.warn('⚠️ 未找到订阅特征值,使用写入特征值作为订阅特征值'); + } + + // 如果仍然没有找到所需的特征值,抛出异常 + if (writeChar == null || subscribeChar == null) { + throw Exception('未找到所需的特征值'); + } } // 设置特征值 @@ -782,8 +1009,13 @@ class BleService { _subscriptionCharacteristic = subscribeChar; // 订阅通知 - await subscribeChar.setNotifyValue(true); - AppLogger.info('✅ 已订阅通知'); + try { + await subscribeChar.setNotifyValue(true); + AppLogger.info('✅ 已订阅通知'); + } catch (e) { + AppLogger.warn('⚠️ 订阅通知失败,尝试继续连接: $e'); + // 即使订阅失败,也继续连接过程 + } // 监听数据 _characteristicDataSubscription?.cancel(); diff --git a/lib/ble/command/base/base_ble_command.dart b/lib/ble/command/base/base_ble_command.dart index 2001274..bd8a305 100644 --- a/lib/ble/command/base/base_ble_command.dart +++ b/lib/ble/command/base/base_ble_command.dart @@ -48,7 +48,7 @@ abstract class BaseBleCommand { static const int PACKET_TYPE_RESPONSE = 0x11; // 应答包 /// 包版本 (高4位) - static const int PACKET_VERSION = 0x10; // 版本1.0 + static const int PACKET_VERSION = 0x20; // 版本2.0 /// 加密类型 (低4位) static const int ENCRYPT_TYPE_PLAIN = 0x00; // 明文 @@ -187,32 +187,35 @@ abstract class BaseBleCommand { } } - // 获取分包序号 - int packetSequence = getNextPacketSequence(); + if (i == 0) { + // 获取分包序号 + int packetSequence = getNextPacketSequence(); - // 组装包头 - List header = buildPacketHeader( - packetType: PACKET_TYPE_REQUEST, - packetSequence: packetSequence, - encryptType: encryptType, - encryptedDataLength: encryptedDataLength, - originalDataLength: originalDataLength, - ); + // 组装包头 + List header = buildPacketHeader( + packetType: PACKET_TYPE_REQUEST, + packetSequence: packetSequence, + encryptType: encryptType, + encryptedDataLength: originalData.length, + originalDataLength: originalData.length, + ); + // 组装完整包 (包头 + 数据) + List packetWithHeader = []; + packetWithHeader.addAll(header); + packetWithHeader.addAll(currentPacketData); - // 组装完整包 (包头 + 数据) - List packetWithHeader = []; - packetWithHeader.addAll(header); - packetWithHeader.addAll(currentPacketData); + // 计算并添加包尾 + List tail = buildPacketTail(packetWithHeader); - // 计算并添加包尾 - List tail = buildPacketTail(packetWithHeader); + // 完整的数据包 + List completePacket = []; + completePacket.addAll(packetWithHeader); + completePacket.addAll(tail); - // 完整的数据包 - List completePacket = []; - completePacket.addAll(packetWithHeader); - completePacket.addAll(tail); - - packets.add(completePacket); + packets.add(completePacket); + } else { + packets.add(currentPacketData); + } } return packets; @@ -297,6 +300,7 @@ abstract class BaseBleCommand { int packetFlag = rawData[offset++]; int version = (packetFlag & 0xF0); int encryptType = (packetFlag & 0x0F); + AppLogger.highlight('加密类型:${encryptType}'); // 6. 解析数据长度 (大端序: 高16位加密后长度 + 低16位原始长度) int combinedLength = (rawData[offset] << 24) | (rawData[offset + 1] << 16) | (rawData[offset + 2] << 8) | rawData[offset + 3]; @@ -348,7 +352,6 @@ abstract class BaseBleCommand { // ); // } - return ParsedPacket( isValid: true, packetType: packetType, @@ -389,7 +392,7 @@ abstract class BaseBleCommand { } // 获取固定长度的数组 - List getFixedLengthList(List data, int length) { + List getFixedLengthList(List data, int length) { for (int i = 0; i < length; i++) { data.add(0); } diff --git a/lib/ble/command/ble_command_manager.dart b/lib/ble/command/ble_command_manager.dart index 7fc35fe..140252c 100644 --- a/lib/ble/command/ble_command_manager.dart +++ b/lib/ble/command/ble_command_manager.dart @@ -4,6 +4,7 @@ import 'package:starwork_flutter/base/app_logger.dart'; import 'package:starwork_flutter/ble/ble_service.dart'; import 'package:starwork_flutter/ble/command/base/base_ble_command.dart'; import 'package:starwork_flutter/ble/command/base/base_ble_response_parser.dart'; +import 'package:starwork_flutter/ble/command/response/ble_cmd_add_admin_parser.dart'; import 'package:starwork_flutter/ble/command/response/ble_cmd_get_private_key_parser.dart'; import 'package:starwork_flutter/ble/command/response/ble_cmd_get_public_key_parser.dart'; import 'package:starwork_flutter/ble/command/response/ble_cmd_read_lock_status_parser.dart'; @@ -50,6 +51,7 @@ class BleCommandManager { registerParser(BleCmdGetPublicKeyParser()); registerParser(BleCmdGetPrivateKeyParser()); registerParser(BleCmdReadLockStatusParser()); + registerParser(BleCmdAddAdminParser()); // TODO: 在这里注册其他命令解析器 // registerParser(BleCmdSetPasswordParser()); diff --git a/lib/ble/command/request/ble_cmd_add_admin.dart b/lib/ble/command/request/ble_cmd_add_admin.dart index 4c1c71e..fa088d1 100644 --- a/lib/ble/command/request/ble_cmd_add_admin.dart +++ b/lib/ble/command/request/ble_cmd_add_admin.dart @@ -3,38 +3,63 @@ import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:starwork_flutter/base/app_logger.dart'; import 'package:starwork_flutter/ble/command/base/base_ble_command.dart'; +import 'package:starwork_flutter/ble/command/base/retryable_ble_command.dart'; import 'package:starwork_flutter/ble/command/response/ble_cmd_get_private_key_parser.dart'; +import 'package:starwork_flutter/ble/command/response/model/ble_add_admin_response.dart'; +import 'package:starwork_flutter/ble/constant/lock_ble_constant.dart'; import 'package:starwork_flutter/common/sm4_encipher/sm4.dart'; /// ✨✨✨ 增加超级管理员命令类 - 继承BaseBleCommand ✨✨✨ -class BleCmdAddAdmin extends BaseBleCommand { +class BleCmdAddAdmin extends BaseBleCommand implements RetryableBleCommand { final String lockId; + final String authUserId; final String keyId; - final String authUserID; - final String userID; + final String userId; + final int openMode; + final int keyType; + final int startDate; + final int expireDate; + final int useCountLimit; + final int isRound; + final int weekRound; + final int startHour; + final int startMin; + final int endHour; + final int endMin; + final int role; + final String password; + final List token; final List publicKey; - final int nowTime; - final int _encryptType; // 私有字段存储加密类型 + final List privateKey; + final int _encryptType; - /// 指令 ID: 0x3091 - static const int cmdId = 0x3091; + /// 指令 ID: 0x3001 + static const int cmdId = 0x3001; /// 构造函数 BleCmdAddAdmin({ - int encryptType = BaseBleCommand.ENCRYPT_TYPE_SM4_PRESET, + int encryptType = BaseBleCommand.ENCRYPT_TYPE_SM4_DEVICE, required this.lockId, + required this.authUserId, required this.keyId, - required this.authUserID, - required this.userID, - required this.nowTime, + required this.userId, + this.openMode = 1, // 缺少定义,默认传1 + this.keyType = LockBleConstant.keyTypeTemporary, + required this.startDate, + required this.expireDate, + this.useCountLimit = 0xFFFF, + this.isRound = 0, + this.weekRound = 0, + this.startHour = 0, + this.startMin = 0, + this.endHour = 0, + this.endMin = 0, + this.role = 255, + required this.password, + required this.token, required this.publicKey, + required this.privateKey, }) : _encryptType = encryptType { - if (lockId.isEmpty) { - throw ArgumentError('LockID cannot be empty'); - } - if (lockId.length > 40) { - throw ArgumentError('LockID must be at most 40 characters long, got ${lockId.length}'); - } AppLogger.debug('🔑 BleCmdAddAdmin 创建: LockID="$lockId", 加密类型=$_encryptType'); } @@ -47,66 +72,6 @@ class BleCmdAddAdmin extends BaseBleCommand { /// ✨✨✨ 实现BaseBleCommand的抽象方法 - 构建命令数据 ✨✨✨ @override List buildData() { - // final List buffer = []; - // - // // 1. 写入 CmdID (2 字节,大端序) - // buffer.add((cmdId >> 8) & 0xFF); // 高字节 - // buffer.add(cmdId & 0xFF); // 低字节 - // - // // 2. 处理 LockID:先转换实际字符,再检查是否需要填充到40字节 - // List lockIdBytes = List.from(lockId.codeUnits); // 创建可修改的副本 - // List keyIdBytes = List.from(keyId.codeUnits); // 创建可修改的副本 - // List authUserIdBytes = List.from(authUserID.codeUnits); // 创建可修改的副本 - // - // // 检查是否需要填充到40字节 - // if (lockIdBytes.length < 40) { - // // 用0填充到40字节 - // int paddingNeeded = 40 - lockIdBytes.length; - // lockIdBytes.addAll(List.filled(paddingNeeded, 0)); - // } - // - // // 检查是否需要填充到40字节 - // if (keyIdBytes.length < 40) { - // // 用0填充到40字节 - // int paddingNeeded = 40 - keyIdBytes.length; - // keyIdBytes.addAll(List.filled(paddingNeeded, 0)); - // } - // - // // 检查是否需要填充到40字节 - // if (authUserIdBytes.length < 20) { - // // 用0填充到40字节 - // int paddingNeeded = 20 - authUserIdBytes.length; - // authUserIdBytes.addAll(List.filled(paddingNeeded, 0)); - // } - // - // buffer.addAll(lockIdBytes); - // buffer.addAll(keyIdBytes); - // buffer.addAll(authUserIdBytes); - // buffer.add((nowTime & 0xFF000000) >> 24); - // buffer.add((nowTime & 0xFF0000) >> 16); - // buffer.add((nowTime & 0xFF00) >> 8); - // buffer.add((nowTime & 0xFF)); - // - // List countAuthCode = []; - // - // countAuthCode.addAll(keyIdBytes); - // countAuthCode.addAll(authUserIdBytes); - // countAuthCode.add((nowTime & 0xFF000000) >> 24); - // countAuthCode.add((nowTime & 0xFF0000) >> 16); - // countAuthCode.add((nowTime & 0xFF00) >> 8); - // countAuthCode.add((nowTime & 0xFF)); - // countAuthCode.addAll(publicKey); - // - // var authCode = md5.convert(countAuthCode); - // - // buffer.add(authCode.bytes.length); - // buffer.addAll(authCode.bytes); - // var list = SM4.encrypt(buffer, key: utf8.encode(lockId), mode: SM4CryptoMode.ECB); - // return list; - // - // - // - List data = []; List ebcData = []; @@ -118,44 +83,85 @@ class BleCmdAddAdmin extends BaseBleCommand { data.add(type1); data.add(type2); - // 锁id - final int lockIDLength = utf8.encode(lockId!).length; + // lockId 40 + final int lockIDLength = utf8.encode(lockId).length; data.addAll(utf8.encode(lockId)); data = getFixedLengthList(data, 40 - lockIDLength); + // authUserId 20 + final int authUserIdLength = utf8.encode(authUserId).length; + data.addAll(utf8.encode(authUserId)); + data = getFixedLengthList(data, 20 - authUserIdLength); + //KeyID 40 - final int keyIDLength = utf8.encode(keyId!).length; + final int keyIDLength = utf8.encode(keyId).length; data.addAll(utf8.encode(keyId)); data = getFixedLengthList(data, 40 - keyIDLength); - //authUserID 20 - final int authUserIDLength = utf8.encode(authUserID!).length; - data.addAll(utf8.encode(authUserID)); - data = getFixedLengthList(data, 20 - authUserIDLength); + //userId 20 + final int userIdLength = utf8.encode(userId).length; + data.addAll(utf8.encode(userId)); + data = getFixedLengthList(data, 20 - userIdLength); - //NowTime 4 - data.add((nowTime & 0xff000000) >> 24); - data.add((nowTime & 0xff0000) >> 16); - data.add((nowTime & 0xff00) >> 8); - data.add(nowTime & 0xff); + // openModel 1 + data.add(openMode); + + // keyType 1 + data.add(keyType); + + int? d1, d2; + if (role == LockBleConstant.roleSuperAdmin) { + d1 = 0; + d2 = 0xffffffff; + } else { + d1 = startDate; + d2 = expireDate; + } + + // StartDate 4 + data.add((d1 & 0xff000000) >> 24); + data.add((d1 & 0xff0000) >> 16); + data.add((d1 & 0xff00) >> 8); + data.add(d1 & 0xff); + + // expireDate 4 + data.add((d2 & 0xff000000) >> 24); + data.add((d2 & 0xff0000) >> 16); + data.add((d2 & 0xff00) >> 8); + data.add(d2 & 0xff); + + //useCountLimit 2 + final double useCountLimitDouble = useCountLimit / 256; + final int useCountLimit1 = useCountLimitDouble.toInt(); + final int useCountLimit2 = useCountLimit % 256; + data.add(useCountLimit1); + data.add(useCountLimit2); + + data.add(isRound); + data.add(weekRound); + data.add(startHour); + data.add(startMin); + data.add(endHour); + data.add(endMin); + + // role 1 + data.add(role); + + //password 20 + final int passwordLength = utf8.encode(password).length; + data.addAll(utf8.encode(password)); + data = getFixedLengthList(data, 20 - passwordLength); + + // token 长度4 首次请求 Token 填 0,如果锁需要鉴权 操作者身份,则会分配动态口令并在应答消息中返回,二次请求时带上。 当token失效或者第一次发送的时候token为0 + data.addAll(token); final List authCodeData = []; - - //authUserID - authCodeData.addAll(utf8.encode(authUserID!)); - - //KeyID - authCodeData.addAll(utf8.encode(keyId!)); - - //NowTime 4 - authCodeData.add((nowTime & 0xff000000) >> 24); - authCodeData.add((nowTime & 0xff0000) >> 16); - authCodeData.add((nowTime & 0xff00) >> 8); - authCodeData.add(nowTime & 0xff); - + authCodeData.addAll(utf8.encode(authUserId)); + authCodeData.addAll(utf8.encode(keyId)); + authCodeData.addAll(token); authCodeData.addAll(publicKey); - // 把authUserID、KeyID、时间戳、公钥通过md5加密之后就是authCode + // 把authUserID、KeyID、token、公钥通过md5加密之后就是authCode var authCode = md5.convert(authCodeData); data.add(authCode.bytes.length); @@ -167,9 +173,58 @@ class BleCmdAddAdmin extends BaseBleCommand { data.add(0); } } + AppLogger.debug("完整明文数据: $data"); - // 拿到数据之后通过LockId进行SM4 ECB加密 key:544d485f633335373034383064613864 - ebcData = SM4.encrypt(data, key: utf8.encode(lockId), mode: SM4CryptoMode.ECB); + // 拿到数据之后通过privateKey进行SM4 ECB加密 + ebcData = SM4.encrypt(data, key: privateKey, mode: SM4CryptoMode.ECB); return ebcData; } + + /// ✨✨✨ 检查是否需要重发 ✨✨✨ + @override + bool shouldRetry(dynamic response) { + // 检查响应是否为BleAddAdminResponse类型且状态码为6(需要token) + if (response is BleAddAdminResponse) { + return response.statusCode == 0x06; // 需要token + } + return false; + } + + /// ✨✨✨ 创建重发命令实例 ✨✨✨ + @override + BaseBleCommand createRetryCommand(dynamic response) { + if (response is BleAddAdminResponse) { + // 当状态码为0x06(需要token)时,响应中的token字段是有效的,应该使用这个token + List token = response.token; + AppLogger.debug('🔐 获取到token用于重发: $token'); + + // 创建新的命令实例,携带token + return BleCmdAddAdmin( + encryptType: this.getEncryptType(), + lockId: this.lockId, + authUserId: this.authUserId, + keyId: this.keyId, + userId: this.userId, + openMode: this.openMode, + keyType: this.keyType, + startDate: this.startDate, + expireDate: this.expireDate, + useCountLimit: this.useCountLimit, + isRound: this.isRound, + weekRound: this.weekRound, + startHour: this.startHour, + startMin: this.startMin, + endHour: this.endHour, + endMin: this.endMin, + role: this.role, + password: this.password, + token: token, // 使用从响应中获取的有效token + publicKey: this.publicKey, + privateKey: this.privateKey, + ); + } + + // 如果无法创建重发命令,抛出异常 + throw Exception('无法为响应创建重发命令: $response'); + } } diff --git a/lib/ble/command/request/ble_cmd_get_public_key.dart b/lib/ble/command/request/ble_cmd_get_public_key.dart index 7924b33..89dd34f 100644 --- a/lib/ble/command/request/ble_cmd_get_public_key.dart +++ b/lib/ble/command/request/ble_cmd_get_public_key.dart @@ -53,7 +53,12 @@ class BleCmdGetPublicKey extends BaseBleCommand { buffer.addAll(lockIdBytes); - + if ((buffer.length % 16) != 0) { + final int add = 16 - buffer.length % 16; + for (int i = 0; i < add; i++) { + buffer.add(0); + } + } return buffer; } } diff --git a/lib/ble/command/response/ble_cmd_add_admin_parser.dart b/lib/ble/command/response/ble_cmd_add_admin_parser.dart new file mode 100644 index 0000000..ef4b21d --- /dev/null +++ b/lib/ble/command/response/ble_cmd_add_admin_parser.dart @@ -0,0 +1,112 @@ +import 'dart:convert'; + +import 'package:starwork_flutter/base/app_logger.dart'; +import 'package:starwork_flutter/ble/command/base/base_ble_command.dart'; +import 'package:starwork_flutter/ble/command/base/base_ble_response_parser.dart'; +import 'package:starwork_flutter/ble/command/request/ble_cmd_add_admin.dart'; +import 'package:starwork_flutter/ble/command/request/ble_cmd_get_public_key.dart'; +import 'package:starwork_flutter/ble/command/request/ble_cmd_read_lock_status.dart'; +import 'package:starwork_flutter/ble/command/response/model/ble_add_admin_response.dart'; + +class BleCmdAddAdminParser extends BaseBleResponseParser { + @override + int get commandId => BleCmdAddAdmin.cmdId; + + @override + String get commandName => '增加超级管理员命令'; + + /// ✨✨✨ 解析获取增加超级管理员的应答数据 ✨✨✨ + @override + BleAddAdminResponse parseResponse(ParsedPacket parsedPacket, List rawResponseData) { + try { + if (rawResponseData.length < 3) { + throw ArgumentError('应答数据长度不足: ${rawResponseData.length}字节 < 3字节'); + } + + int offset = 0; + + // 1. 提取命令ID (已在isMatchingResponse中验证过,这里记录日志) + int responseCommandId = BaseBleResponseParser.extractInt(rawResponseData, offset, 2); + offset += 2; + + // 2. lockId + List lockId = BaseBleResponseParser.extractBytes(rawResponseData, offset, 40); + String lockIdStr = _safeStringDecode(lockId).trim(); + offset += 40; + + // 3. token + List token = BaseBleResponseParser.extractBytes(rawResponseData, offset, 4); + offset += 4; + + // 4. 提取状态码 + int statusCode = rawResponseData[offset++]; + + // 当状态码为0x06(需要token)时,token字段是有效的,应该返回这个token + if (statusCode == 0x06) { + return BleAddAdminResponse( + commandId: commandId, + statusCode: statusCode, + token: token, // 状态码为0x06时返回有效的token + lockId: lockIdStr, + serialNo: '', + no: -1, + ); + } + + // 当状态码为0时,表示成功,此时可能也有token(根据协议而定) + if (statusCode == 0) { + return BleAddAdminResponse( + commandId: commandId, + statusCode: statusCode, + token: token, // 状态码为0时也返回token + lockId: lockIdStr, + serialNo: '', + no: -1, + ); + } + + // 其他状态码情况 + return BleAddAdminResponse( + commandId: commandId, + statusCode: statusCode, + token: [], // 其他状态码时返回空token + lockId: lockIdStr, + serialNo: '', + no: -1, + ); + } catch (e) { + AppLogger.error('❌ 解析添加管理员应答数据异常', error: e); + rethrow; + } + } + + /// ✨✨✨ 安全的字符串解码方法 ✨✨✨ + /// 处理可能包含非UTF-8字节的数据 + String _safeStringDecode(List data) { + // 1. 找到第一个 0x00(空字节)的位置,只取前面的有效部分 + int endIndex = 0; + for (int i = 0; i < data.length; i++) { + if (data[i] == 0) { + break; + } + endIndex = i + 1; + } + + // 2. 截取有效字节 + List validBytes = data.sublist(0, endIndex); + + try { + // 3. 使用 UTF-8 解码(允许畸形,但会替换为 ) + return utf8.decode(validBytes, allowMalformed: true).trim(); + } catch (e) { + // 4. 极端情况:用字符码直接构造(基本不会走到这里) + return String.fromCharCodes(validBytes).trim(); + } + } + + /// ✨✨✨ 静态便捷方法 - 直接解析原始数据包 ✨✨✨ + static BleAddAdminResponse? parseRawPacket(List rawPacketData) { + BleCmdAddAdminParser parser = BleCmdAddAdminParser(); + return parser.handleResponse(rawPacketData) as BleAddAdminResponse?; + } +} diff --git a/lib/ble/command/response/ble_cmd_read_lock_status_parser.dart b/lib/ble/command/response/ble_cmd_read_lock_status_parser.dart index 2235d4a..a607e06 100644 --- a/lib/ble/command/response/ble_cmd_read_lock_status_parser.dart +++ b/lib/ble/command/response/ble_cmd_read_lock_status_parser.dart @@ -286,7 +286,7 @@ class BleCmdReadLockStatusParser extends BaseBleResponseParser { featureParaData: featureParaData, ); } catch (e) { - AppLogger.error('❌ 解析获取公钥应答数据异常', error: e); + AppLogger.error('❌ 解析读取锁状态答数据异常', error: e); rethrow; } } diff --git a/lib/ble/command/response/model/ble_add_admin_response.dart b/lib/ble/command/response/model/ble_add_admin_response.dart new file mode 100644 index 0000000..d44142c --- /dev/null +++ b/lib/ble/command/response/model/ble_add_admin_response.dart @@ -0,0 +1,43 @@ +class BleAddAdminResponse { + final int commandId; + final int statusCode; + final int no; + final String serialNo; + final String lockId; + final List token; + + BleAddAdminResponse({ + required this.commandId, + required this.statusCode, + required this.token, + required this.lockId, + required this.serialNo, + required this.no, + }); + + /// 是否成功 + bool get isSuccess => statusCode == 0x00; + + /// 状态描述 + String get statusDescription { + switch (statusCode) { + case 0x00: + return '成功'; + case 0x01: + return '失败'; + case 0x02: + return '参数错误'; + case 0x03: + return '设备忙'; + case 0x06: + return '需要token'; + default: + return '未知状态(0x${statusCode.toRadixString(16).padLeft(2, '0')})'; + } + } + + @override + String toString() { + return 'BleAddAdminResponse{commandId: $commandId, statusCode: $statusCode, no: $no, serialNo: $serialNo, lockId: $lockId, token: $token}'; + } +} diff --git a/lib/ble/constant/lock_ble_constant.dart b/lib/ble/constant/lock_ble_constant.dart new file mode 100644 index 0000000..1d198bf --- /dev/null +++ b/lib/ble/constant/lock_ble_constant.dart @@ -0,0 +1,19 @@ +class LockBleConstant { + /// KeyType: 钥匙类型 + /// 0: 普通钥匙 + /// 1: 临时钥匙(到期自动删除) + /// 2: 蓝牙遥控器 + /// 255: 测试钥匙(添加正式钥匙后自动失效) + static const int keyTypeNormal = 0; + static const int keyTypeTemporary = 1; + static const int keyTypeBluetoothRemote = 2; + static const int keyTypeTest = 255; + + /// Role: 用户角色 + /// 0: 普通用户 + /// 1: 管理员 + /// 255 (0xFF): 超级管理员 + static const int roleNormalUser = 0; + static const int roleAdmin = 1; + static const int roleSuperAdmin = 255; // 0xFF +} diff --git a/lib/views/device/searchDevice/search_device_controller.dart b/lib/views/device/searchDevice/search_device_controller.dart index 58f27dd..db92a01 100644 --- a/lib/views/device/searchDevice/search_device_controller.dart +++ b/lib/views/device/searchDevice/search_device_controller.dart @@ -1,15 +1,19 @@ +import 'dart:math'; + import 'package:get/get.dart'; import 'package:starwork_flutter/base/app_logger.dart'; import 'package:starwork_flutter/base/app_permission.dart'; import 'package:starwork_flutter/base/base_controller.dart'; import 'package:starwork_flutter/ble/ble_service.dart'; import 'package:starwork_flutter/ble/command/base/base_ble_response_parser.dart'; +import 'package:starwork_flutter/ble/command/request/ble_cmd_add_admin.dart'; import 'package:starwork_flutter/ble/command/request/ble_cmd_get_private_key.dart'; import 'package:starwork_flutter/ble/command/request/ble_cmd_get_public_key.dart'; import 'package:starwork_flutter/ble/command/request/ble_cmd_read_lock_status.dart'; import 'package:starwork_flutter/ble/command/response/ble_cmd_get_private_key_parser.dart'; import 'package:starwork_flutter/ble/command/response/ble_cmd_get_public_key_parser.dart'; import 'package:starwork_flutter/ble/command/response/ble_cmd_read_lock_status_parser.dart'; +import 'package:starwork_flutter/ble/command/response/model/ble_add_admin_response.dart'; import 'package:starwork_flutter/ble/model/scan_device_info.dart'; import 'package:starwork_flutter/common/constant/app_toast_messages.dart'; import 'package:starwork_flutter/common/constant/cache_keys.dart'; @@ -171,6 +175,7 @@ class SearchDeviceController extends BaseController { /// 3.注册管理员密码 void connectingDevices(ScanDeviceInfo device) async { try { + showLoading(); // 停止搜索 BleService().stopBluetoothSearch(); isSearching.value = false; @@ -207,7 +212,7 @@ class SearchDeviceController extends BaseController { await SharedPreferencesUtils.saveIntList(CacheKeys.lockCommKey, privateKeyResponse.commKey); await SharedPreferencesUtils.saveIntList(CacheKeys.lockSignKey, privateKeyResponse.signKey); AppLogger.info('🎯 获取私钥成功: ${privateKeyResponse.toString()}'); - + // 更新缓存的私钥,以便在解密时使用 BleService.setCachedPrivateKey(privateKeyResponse.commKey); @@ -228,6 +233,30 @@ class SearchDeviceController extends BaseController { ); if (readLockStatusResponse != null && readLockStatusResponse.isSuccess) { AppLogger.highlight('readLockStatusResponse:${readLockStatusResponse}'); + final Random rng = Random(); + // 生成 100000 到 999999 之间的随机整数 + final int number = rng.nextInt(900000) + 100000; + //读取锁状态 + BleCmdAddAdmin addAdminCmd = BleCmdAddAdmin( + lockId: device.advName, + userId: '13655', + privateKey: privateKeyResponse.commKey, + authUserId: '13655', + keyId: '1', + startDate: DateTime.now().millisecondsSinceEpoch, + expireDate: 0x11223344, + password: number.toString(), + token: [0, 0, 0, 0], + publicKey: publicKeyResponse.publicKey, + ); + // 发送命令,允许自动连接和搜索设备 + BleAddAdminResponse? addAdminResponse = await BleService().sendCommand( + command: addAdminCmd, + targetDeviceName: device.advName, + // 通过名称搜索设备 + autoConnectIfNeeded: true, + ); + AppLogger.highlight('addAdminResponse:${addAdminResponse}'); } } } else { @@ -236,6 +265,8 @@ class SearchDeviceController extends BaseController { } catch (e, stackTrace) { AppLogger.error('连接设备失败', error: e, stackTrace: stackTrace); showToast('连接失败,请重试'); + } finally { + hideLoading(); } }