diff --git a/lib/models/ble_packet_log.dart b/lib/models/ble_packet_log.dart index a7f2b0d..ad26450 100644 --- a/lib/models/ble_packet_log.dart +++ b/lib/models/ble_packet_log.dart @@ -1,4 +1,5 @@ import 'dart:typed_data'; +import '../services/meshcore_opcode_names.dart'; /// Represents a logged BLE packet with timestamp and metadata class BlePacketLog { @@ -21,26 +22,46 @@ class BlePacketLog { return rawData.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' '); } + /// Get opcode name for this packet + String get opcodeName { + if (responseCode == null) return 'N/A'; + return MeshCoreOpcodeNames.getOpcodeName( + responseCode!, + isTx: direction == PacketDirection.tx, + ); + } + + /// Get full opcode description (name + hex code) + String get opcodeDescription { + if (responseCode == null) return 'N/A'; + return MeshCoreOpcodeNames.getOpcodeDescription( + responseCode!, + isTx: direction == PacketDirection.tx, + ); + } + /// Get short summary of the packet String get summary { final dir = direction == PacketDirection.rx ? 'RX' : 'TX'; final code = responseCode != null ? '0x${responseCode!.toRadixString(16).padLeft(2, '0')}' : 'N/A'; - return '[$dir] Code: $code, Size: ${rawData.length} bytes'; + final name = responseCode != null ? opcodeName : ''; + return '[$dir] $name Code: $code, Size: ${rawData.length} bytes'; } /// Convert to CSV format for export String toCsvRow() { final dir = direction == PacketDirection.rx ? 'RX' : 'TX'; final code = responseCode?.toString() ?? ''; + final name = responseCode != null ? opcodeName : ''; final hex = hexData; final desc = description ?? ''; - return '${timestamp.toIso8601String()},$dir,${rawData.length},$code,"$hex","$desc"'; + return '${timestamp.toIso8601String()},$dir,${rawData.length},$name,$code,"$hex","$desc"'; } /// Convert to human-readable log format String toLogString() { final dir = direction == PacketDirection.rx ? 'RX' : 'TX'; - final code = responseCode != null ? ' [0x${responseCode!.toRadixString(16).padLeft(2, '0')}]' : ''; + final code = responseCode != null ? ' [$opcodeDescription]' : ''; final desc = description != null ? ' - $description' : ''; return '${timestamp.toIso8601String()} [$dir]$code ${rawData.length} bytes: $hexData$desc'; } diff --git a/lib/screens/packet_log_screen.dart b/lib/screens/packet_log_screen.dart index 56afe6e..22c7823 100644 --- a/lib/screens/packet_log_screen.dart +++ b/lib/screens/packet_log_screen.dart @@ -65,7 +65,7 @@ class _PacketLogScreenState extends State { // Create CSV content final buffer = StringBuffer(); - buffer.writeln('Timestamp,Direction,Size (bytes),Code,Hex Data,Description'); + buffer.writeln('Timestamp,Direction,Size (bytes),Opcode Name,Code,Hex Data,Description'); for (final log in logs) { buffer.writeln(log.toCsvRow()); } @@ -439,19 +439,16 @@ class _PacketLogCard extends StatelessWidget { ), ), const SizedBox(width: 8), - if (log.description != null) - Flexible( - child: Text( - log.description!, - style: const TextStyle(fontSize: 14), - overflow: TextOverflow.ellipsis, + Flexible( + child: Text( + log.responseCode != null ? log.opcodeName : 'N/A', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, ), - ) - else - Text( - 'Code: ${log.responseCode != null ? "0x${log.responseCode!.toRadixString(16).padLeft(2, '0')}" : "N/A"}', - style: const TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, ), + ), ], ), subtitle: Column( @@ -516,7 +513,7 @@ class _PacketLogCard extends StatelessWidget { if (log.responseCode != null) _InfoChip( icon: Icons.tag, - label: 'Code: 0x${log.responseCode!.toRadixString(16).padLeft(2, '0')} (${log.responseCode})', + label: log.opcodeDescription, ), ], ), diff --git a/lib/services/meshcore_ble_service.dart b/lib/services/meshcore_ble_service.dart index d1252a7..5210652 100644 --- a/lib/services/meshcore_ble_service.dart +++ b/lib/services/meshcore_ble_service.dart @@ -9,6 +9,7 @@ import '../models/ble_packet_log.dart'; import 'buffer_reader.dart'; import 'buffer_writer.dart'; import 'meshcore_constants.dart'; +import 'meshcore_opcode_names.dart'; /// Callback types for MeshCore events typedef OnContactCallback = void Function(Contact contact); @@ -226,37 +227,41 @@ class MeshCoreBleService { throw Exception('Not connected'); } try { - print('📝 [BLE] Writing ${data.length} bytes to RX characteristic...'); - print(' RX Characteristic properties: ${_rxCharacteristic!.properties}'); + // Extract command code from first byte + final commandCode = data.isNotEmpty ? data[0] : null; + final opcodeName = commandCode != null + ? MeshCoreOpcodeNames.getCommandName(commandCode) + : 'UNKNOWN'; + final opcodeHex = commandCode != null + ? '0x${commandCode.toRadixString(16).padLeft(2, '0').toUpperCase()}' + : 'N/A'; + + print('📤 [TX] Sending command: $opcodeName ($opcodeHex)'); + print(' Data size: ${data.length} bytes'); + print(' Hex: ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}'); // Check if the characteristic supports write without response final supportsWriteWithoutResponse = _rxCharacteristic!.properties.writeWithoutResponse; final supportsWrite = _rxCharacteristic!.properties.write; - print(' Supports writeWithoutResponse: $supportsWriteWithoutResponse'); - print(' Supports write: $supportsWrite'); - if (supportsWriteWithoutResponse) { - print(' Using write without response'); await _rxCharacteristic!.write(data, withoutResponse: true); } else if (supportsWrite) { - print(' Using write with response'); await _rxCharacteristic!.write(data, withoutResponse: false); } else { throw Exception('Characteristic does not support write operations'); } - // Log TX packet (extract command code from first byte) - final commandCode = data.isNotEmpty ? data[0] : null; + // Log TX packet _logPacket(data, PacketDirection.tx, responseCode: commandCode); // Increment TX packet counter and trigger activity indicator _txPacketCount++; onTxActivity?.call(); - print('✅ [BLE] Write successful'); + print('✅ [TX] Command sent successfully'); } catch (e) { - print('❌ [BLE] Write error: $e'); + print('❌ [TX] Write error: $e'); onError?.call('Write error: $e'); rethrow; } @@ -265,11 +270,9 @@ class MeshCoreBleService { /// Handle incoming data from TX characteristic void _onDataReceived(List data) { try { - print('📥 [BLE] Received ${data.length} bytes from TX characteristic'); - // Handle empty data if (data.isEmpty) { - print(' ⚠️ Empty data received, ignoring'); + print('⚠️ [RX] Empty data received, ignoring'); return; } @@ -279,12 +282,17 @@ class MeshCoreBleService { _rxPacketCount++; onRxActivity?.call(); - print(' Raw data: ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}'); - final reader = BufferReader(dataBytes); final responseCode = reader.readByte(); - print(' Response code: $responseCode (0x${responseCode.toRadixString(16)})'); - print(' Remaining bytes: ${reader.remainingBytesCount}'); + + // Get opcode name for logging + final opcodeName = MeshCoreOpcodeNames.getOpcodeName(responseCode, isTx: false); + final opcodeHex = '0x${responseCode.toRadixString(16).padLeft(2, '0').toUpperCase()}'; + + print('📥 [RX] Received: $opcodeName ($opcodeHex)'); + print(' Data size: ${data.length} bytes'); + print(' Hex: ${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}'); + print(' Payload: ${reader.remainingBytesCount} bytes'); // Log RX packet (before processing so we capture everything) _logPacket(dataBytes, PacketDirection.rx, responseCode: responseCode); @@ -808,31 +816,20 @@ class MeshCoreBleService { /// Send AppStart command Future _sendAppStart() async { - print('📤 [BLE] Preparing AppStart command...'); final writer = BufferWriter(); writer.writeByte(MeshCoreConstants.cmdAppStart); writer.writeByte(1); // appVer writer.writeBytes(Uint8List(6)); // reserved writer.writeString('MeshCore SAR'); // appName - final data = writer.toBytes(); - print(' Command: ${MeshCoreConstants.cmdAppStart}'); - print(' Data length: ${data.length} bytes'); - await _writeData(data); - print('✅ [BLE] AppStart command sent'); + await _writeData(writer.toBytes()); } /// Send DeviceQuery command Future _sendDeviceQuery() async { - print('📤 [BLE] Preparing DeviceQuery command...'); final writer = BufferWriter(); writer.writeByte(MeshCoreConstants.cmdDeviceQuery); writer.writeByte(MeshCoreConstants.supportedCompanionProtocolVersion); - final data = writer.toBytes(); - print(' Command: ${MeshCoreConstants.cmdDeviceQuery}'); - print(' Protocol version: ${MeshCoreConstants.supportedCompanionProtocolVersion}'); - print(' Data length: ${data.length} bytes'); - await _writeData(data); - print('✅ [BLE] DeviceQuery command sent'); + await _writeData(writer.toBytes()); await _sendAppStart(); } diff --git a/lib/services/meshcore_opcode_names.dart b/lib/services/meshcore_opcode_names.dart new file mode 100644 index 0000000..ecb70a1 --- /dev/null +++ b/lib/services/meshcore_opcode_names.dart @@ -0,0 +1,188 @@ +import 'meshcore_constants.dart'; + +/// Maps MeshCore protocol opcodes to human-readable names +class MeshCoreOpcodeNames { + /// Get command name from opcode + static String getCommandName(int opcode) { + switch (opcode) { + case MeshCoreConstants.cmdAppStart: + return 'APP_START'; + case MeshCoreConstants.cmdSendTxtMsg: + return 'SEND_TXT_MSG'; + case MeshCoreConstants.cmdSendChannelTxtMsg: + return 'SEND_CHANNEL_TXT_MSG'; + case MeshCoreConstants.cmdGetContacts: + return 'GET_CONTACTS'; + case MeshCoreConstants.cmdGetDeviceTime: + return 'GET_DEVICE_TIME'; + case MeshCoreConstants.cmdSetDeviceTime: + return 'SET_DEVICE_TIME'; + case MeshCoreConstants.cmdSendSelfAdvert: + return 'SEND_SELF_ADVERT'; + case MeshCoreConstants.cmdSetAdvertName: + return 'SET_ADVERT_NAME'; + case MeshCoreConstants.cmdAddUpdateContact: + return 'ADD_UPDATE_CONTACT'; + case MeshCoreConstants.cmdSyncNextMessage: + return 'SYNC_NEXT_MESSAGE'; + case MeshCoreConstants.cmdSetRadioParams: + return 'SET_RADIO_PARAMS'; + case MeshCoreConstants.cmdSetTxPower: + return 'SET_TX_POWER'; + case MeshCoreConstants.cmdResetPath: + return 'RESET_PATH'; + case MeshCoreConstants.cmdSetAdvertLatLon: + return 'SET_ADVERT_LAT_LON'; + case MeshCoreConstants.cmdRemoveContact: + return 'REMOVE_CONTACT'; + case MeshCoreConstants.cmdShareContact: + return 'SHARE_CONTACT'; + case MeshCoreConstants.cmdExportContact: + return 'EXPORT_CONTACT'; + case MeshCoreConstants.cmdImportContact: + return 'IMPORT_CONTACT'; + case MeshCoreConstants.cmdReboot: + return 'REBOOT'; + case MeshCoreConstants.cmdGetBatteryVoltage: + return 'GET_BATTERY_VOLTAGE'; + case MeshCoreConstants.cmdSetTuningParams: + return 'SET_TUNING_PARAMS'; + case MeshCoreConstants.cmdDeviceQuery: + return 'DEVICE_QUERY'; + case MeshCoreConstants.cmdExportPrivateKey: + return 'EXPORT_PRIVATE_KEY'; + case MeshCoreConstants.cmdImportPrivateKey: + return 'IMPORT_PRIVATE_KEY'; + case MeshCoreConstants.cmdSendRawData: + return 'SEND_RAW_DATA'; + case MeshCoreConstants.cmdSendLogin: + return 'SEND_LOGIN'; + case MeshCoreConstants.cmdSendStatusReq: + return 'SEND_STATUS_REQ'; + case MeshCoreConstants.cmdGetChannel: + return 'GET_CHANNEL'; + case MeshCoreConstants.cmdSetChannel: + return 'SET_CHANNEL'; + case MeshCoreConstants.cmdSignStart: + return 'SIGN_START'; + case MeshCoreConstants.cmdSignData: + return 'SIGN_DATA'; + case MeshCoreConstants.cmdSignFinish: + return 'SIGN_FINISH'; + case MeshCoreConstants.cmdSendTracePath: + return 'SEND_TRACE_PATH'; + case MeshCoreConstants.cmdSetOtherParams: + return 'SET_OTHER_PARAMS'; + case MeshCoreConstants.cmdSendTelemetryReq: + return 'SEND_TELEMETRY_REQ'; + case MeshCoreConstants.cmdSendBinaryReq: + return 'SEND_BINARY_REQ'; + default: + return 'CMD_UNKNOWN'; + } + } + + /// Get response name from opcode + static String getResponseName(int opcode) { + switch (opcode) { + case MeshCoreConstants.respOk: + return 'OK'; + case MeshCoreConstants.respErr: + return 'ERROR'; + case MeshCoreConstants.respContactsStart: + return 'CONTACTS_START'; + case MeshCoreConstants.respContact: + return 'CONTACT'; + case MeshCoreConstants.respEndOfContacts: + return 'END_OF_CONTACTS'; + case MeshCoreConstants.respSelfInfo: + return 'SELF_INFO'; + case MeshCoreConstants.respSent: + return 'SENT'; + case MeshCoreConstants.respContactMsgRecv: + return 'CONTACT_MSG_RECV'; + case MeshCoreConstants.respChannelMsgRecv: + return 'CHANNEL_MSG_RECV'; + case MeshCoreConstants.respCurrTime: + return 'CURR_TIME'; + case MeshCoreConstants.respNoMoreMessages: + return 'NO_MORE_MESSAGES'; + case MeshCoreConstants.respExportContact: + return 'EXPORT_CONTACT'; + case MeshCoreConstants.respBatteryVoltage: + return 'BATTERY_VOLTAGE'; + case MeshCoreConstants.respDeviceInfo: + return 'DEVICE_INFO'; + case MeshCoreConstants.respPrivateKey: + return 'PRIVATE_KEY'; + case MeshCoreConstants.respDisabled: + return 'DISABLED'; + case MeshCoreConstants.respChannelInfo: + return 'CHANNEL_INFO'; + case MeshCoreConstants.respSignStart: + return 'SIGN_START'; + case MeshCoreConstants.respSignature: + return 'SIGNATURE'; + default: + return 'RESP_UNKNOWN'; + } + } + + /// Get push notification name from opcode + static String getPushName(int opcode) { + switch (opcode) { + case MeshCoreConstants.pushAdvert: + return 'ADVERT'; + case MeshCoreConstants.pushPathUpdated: + return 'PATH_UPDATED'; + case MeshCoreConstants.pushSendConfirmed: + return 'SEND_CONFIRMED'; + case MeshCoreConstants.pushMsgWaiting: + return 'MSG_WAITING'; + case MeshCoreConstants.pushRawData: + return 'RAW_DATA'; + case MeshCoreConstants.pushLoginSuccess: + return 'LOGIN_SUCCESS'; + case MeshCoreConstants.pushLoginFail: + return 'LOGIN_FAIL'; + case MeshCoreConstants.pushStatusResponse: + return 'STATUS_RESPONSE'; + case MeshCoreConstants.pushLogRxData: + return 'LOG_RX_DATA'; + case MeshCoreConstants.pushTraceData: + return 'TRACE_DATA'; + case MeshCoreConstants.pushNewAdvert: + return 'NEW_ADVERT'; + case MeshCoreConstants.pushTelemetryResponse: + return 'TELEMETRY_RESPONSE'; + case MeshCoreConstants.pushBinaryResponse: + return 'BINARY_RESPONSE'; + default: + return 'PUSH_UNKNOWN'; + } + } + + /// Get opcode name for any code (tries to determine type automatically) + static String getOpcodeName(int opcode, {bool isTx = false}) { + // If TX (sent to device), it's a command + if (isTx) { + return getCommandName(opcode); + } + + // If RX (received from device), determine if it's a push or response + if (opcode >= 0x80) { + return getPushName(opcode); + } else { + return getResponseName(opcode); + } + } + + /// Get full opcode description with code in hex + static String getOpcodeDescription(int opcode, {bool isTx = false}) { + final name = getOpcodeName(opcode, isTx: isTx); + final hex = '0x${opcode.toRadixString(16).padLeft(2, '0').toUpperCase()}'; + return '$name ($hex)'; + } + + MeshCoreOpcodeNames._(); // Private constructor to prevent instantiation +}