mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Enhance BLE packet logging with opcode names and descriptions
This commit is contained in:
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
188
lib/services/meshcore_opcode_names.dart
Normal file
188
lib/services/meshcore_opcode_names.dart
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user