feat: Enhance BLE packet logging with opcode names and descriptions

This commit is contained in:
Janez T
2025-10-14 16:49:19 +02:00
parent 59de627289
commit 13df47b297
4 changed files with 250 additions and 47 deletions

View File

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

View File

@@ -65,7 +65,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
// 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,
),
],
),

View File

@@ -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<int> 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<void> _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<void> _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();
}

View File

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