Add trace button and raw packets

This commit is contained in:
Janez T
2026-03-05 08:02:39 +01:00
parent 13b91e3cc9
commit b12567f613
2 changed files with 139 additions and 74 deletions

View File

@@ -769,8 +769,6 @@ class _MessagesTabState extends State<MessagesTab> {
final connectionProvider = context.read<ConnectionProvider>(); final connectionProvider = context.read<ConnectionProvider>();
final messagesProvider = context.read<MessagesProvider>(); final messagesProvider = context.read<MessagesProvider>();
final voiceProvider = context.read<VoiceProvider>(); final voiceProvider = context.read<VoiceProvider>();
// Insert the chat placeholder before sending (so it appears immediately).
final msgId = 'voice_${sessionId}_sent'; final msgId = 'voice_${sessionId}_sent';
final devicePublicKey = connectionProvider.deviceInfo.publicKey; final devicePublicKey = connectionProvider.deviceInfo.publicKey;
final senderPublicKeyPrefix = final senderPublicKeyPrefix =
@@ -782,24 +780,6 @@ class _MessagesTabState extends State<MessagesTab> {
MessageDestinationPreferences.destinationTypeChannel; MessageDestinationPreferences.destinationTypeChannel;
final recipient = _selectedRecipient; final recipient = _selectedRecipient;
final channelIdx = isChannel ? (recipient?.publicKey[1] ?? 0) : null; final channelIdx = isChannel ? (recipient?.publicKey[1] ?? 0) : null;
final sentMsg = Message(
id: msgId,
messageType: (!isChannel && recipient != null)
? MessageType.contact
: MessageType.channel,
senderPublicKeyPrefix: senderPublicKeyPrefix,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
text: '',
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sent,
isVoice: true,
voiceId: sessionId,
channelIdx: channelIdx,
recipientPublicKey: isChannel ? null : recipient?.publicKey,
);
messagesProvider.addSentMessage(sentMsg);
final encodedPackets = <VoicePacket>[]; final encodedPackets = <VoicePacket>[];
debugPrint( debugPrint(
@@ -859,6 +839,26 @@ class _MessagesTabState extends State<MessagesTab> {
); );
final envelopeText = envelope.encodeText(); final envelopeText = envelope.encodeText();
// Insert placeholder with real VE1 envelope text so technical details are populated.
final sentMsg = Message(
id: msgId,
messageType: (!isChannel && recipient != null)
? MessageType.contact
: MessageType.channel,
senderPublicKeyPrefix: senderPublicKeyPrefix,
pathLen: 0,
textType: MessageTextType.plain,
senderTimestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
text: envelopeText,
receivedAt: DateTime.now(),
deliveryStatus: MessageDeliveryStatus.sent,
isVoice: true,
voiceId: sessionId,
channelIdx: channelIdx,
recipientPublicKey: isChannel ? null : recipient?.publicKey,
);
messagesProvider.addSentMessage(sentMsg);
try { try {
if (isChannel) { if (isChannel) {
await connectionProvider.sendChannelMessage( await connectionProvider.sendChannelMessage(

View File

@@ -15,6 +15,7 @@ import '../../providers/voice_provider.dart';
import '../../providers/image_provider.dart' as ip; import '../../providers/image_provider.dart' as ip;
import '../contacts/direct_message_sheet.dart'; import '../contacts/direct_message_sheet.dart';
import '../drawing_minimap_preview.dart'; import '../drawing_minimap_preview.dart';
import '../../models/ble_packet_log.dart';
import '../../services/sar_template_service.dart'; import '../../services/sar_template_service.dart';
import '../../utils/toast_logger.dart'; import '../../utils/toast_logger.dart';
import '../../utils/sar_message_parser.dart'; import '../../utils/sar_message_parser.dart';
@@ -425,9 +426,21 @@ class _MessageBubbleState extends State<MessageBubble> {
?.sublist(0, recipientKey.length < 6 ? recipientKey.length : 6) ?.sublist(0, recipientKey.length < 6 ? recipientKey.length : 6)
.map((b) => b.toRadixString(16).padLeft(2, '0')) .map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(''); .join('');
final snrDb = widget.message.lastEchoSnrRaw != null final matchedRxLog = _findBestMatchingRxLog(
connectionProvider.bleService.packetLogs,
widget.message,
);
final packetPathBytes = _extractPathBytesFromLog(matchedRxLog);
final packetPathHex = packetPathBytes
?.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(':');
final snrDb =
matchedRxLog?.logRxDataInfo?.snrDb ??
(widget.message.lastEchoSnrRaw != null
? (widget.message.lastEchoSnrRaw!.toSigned(8) / 4.0) ? (widget.message.lastEchoSnrRaw!.toSigned(8) / 4.0)
: null; : null);
final rssiDbm =
matchedRxLog?.logRxDataInfo?.rssiDbm ?? widget.message.lastEchoRssiDbm;
final rawLines = <String>[ final rawLines = <String>[
'Message ID: ${widget.message.id}', 'Message ID: ${widget.message.id}',
@@ -444,6 +457,9 @@ class _MessageBubbleState extends State<MessageBubble> {
'Echo count: ${widget.message.echoCount}', 'Echo count: ${widget.message.echoCount}',
'Last echo RSSI: ${widget.message.lastEchoRssiDbm ?? '-'}', 'Last echo RSSI: ${widget.message.lastEchoRssiDbm ?? '-'}',
'Last echo SNR: ${snrDb?.toStringAsFixed(2) ?? '-'}', 'Last echo SNR: ${snrDb?.toStringAsFixed(2) ?? '-'}',
'Matched RX RSSI: ${rssiDbm ?? '-'}',
'Matched RX SNR: ${snrDb?.toStringAsFixed(2) ?? '-'}',
'Matched path bytes: ${packetPathHex ?? '-'}',
'Expected ACK tag: ${widget.message.expectedAckTag ?? '-'}', 'Expected ACK tag: ${widget.message.expectedAckTag ?? '-'}',
'Suggested timeout ms: ${widget.message.suggestedTimeoutMs ?? '-'}', 'Suggested timeout ms: ${widget.message.suggestedTimeoutMs ?? '-'}',
'Round-trip ms: ${widget.message.roundTripTimeMs ?? '-'}', 'Round-trip ms: ${widget.message.roundTripTimeMs ?? '-'}',
@@ -618,7 +634,8 @@ class _MessageBubbleState extends State<MessageBubble> {
], ],
), ),
if (widget.message.lastEchoRssiDbm != null || if (widget.message.lastEchoRssiDbm != null ||
snrDb != null) ...[ snrDb != null ||
rssiDbm != null) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
_techSection( _techSection(
context, context,
@@ -626,19 +643,19 @@ class _MessageBubbleState extends State<MessageBubble> {
title: l10n.linkQuality, title: l10n.linkQuality,
child: Column( child: Column(
children: [ children: [
if (widget.message.lastEchoRssiDbm != null) if (rssiDbm != null)
_signalRow( _signalRow(
context, context,
label: 'RSSI', label: 'RSSI',
valueLabel: '${widget.message.lastEchoRssiDbm} dBm', valueLabel: '$rssiDbm dBm',
normalized: normalized:
((widget.message.lastEchoRssiDbm!.toDouble() + ((rssiDbm.toDouble() +
120.0) / 120.0) /
70.0) 70.0)
.clamp(0.0, 1.0), .clamp(0.0, 1.0),
color: widget.message.lastEchoRssiDbm! >= -80 color: rssiDbm >= -80
? Colors.green ? Colors.green
: widget.message.lastEchoRssiDbm! >= -95 : rssiDbm >= -95
? Colors.amber ? Colors.amber
: Colors.redAccent, : Colors.redAccent,
), ),
@@ -672,29 +689,36 @@ class _MessageBubbleState extends State<MessageBubble> {
label: l10n.status, label: l10n.status,
value: widget.message.deliveryStatus.name, value: widget.message.deliveryStatus.name,
), ),
if (widget.message.expectedAckTag != null)
_detailRow( _detailRow(
context, context,
label: l10n.expectedAckTag, label: l10n.expectedAckTag,
value: widget.message.expectedAckTag?.toString() ?? '-', value: widget.message.expectedAckTag!.toString(),
), ),
if (widget.message.roundTripTimeMs != null)
_detailRow( _detailRow(
context, context,
label: l10n.roundTrip, label: l10n.roundTrip,
value: widget.message.roundTripTimeMs != null value: '${widget.message.roundTripTimeMs} ms',
? '${widget.message.roundTripTimeMs} ms'
: '-',
), ),
if (widget.message.retryAttempt > 0)
_detailRow( _detailRow(
context, context,
label: l10n.retryAttempt, label: l10n.retryAttempt,
value: widget.message.retryAttempt.toString(), value: widget.message.retryAttempt.toString(),
), ),
if (widget.message.usedFloodFallback)
_detailRow( _detailRow(
context, context,
label: l10n.floodFallback, label: l10n.floodFallback,
value: widget.message.usedFloodFallback value: l10n.yes,
? l10n.yes ),
: l10n.no, if (packetPathHex != null)
_detailRow(
context,
label: 'Path bytes',
value: packetPathHex,
onCopy: () => copyField(packetPathHex),
), ),
], ],
), ),
@@ -715,28 +739,27 @@ class _MessageBubbleState extends State<MessageBubble> {
_detailRow( _detailRow(
context, context,
label: l10n.sender, label: l10n.sender,
value: senderName ?? widget.message.senderName ?? '-', value: senderName ?? widget.message.senderName ?? 'Unknown',
), ),
if (senderPrefixHex != null)
_detailRow( _detailRow(
context, context,
label: l10n.senderKey, label: l10n.senderKey,
value: senderPrefixHex ?? '-', value: senderPrefixHex,
onCopy: senderPrefixHex != null onCopy: () => copyField(senderPrefixHex),
? () => copyField(senderPrefixHex)
: null,
), ),
if (recipientName != null)
_detailRow( _detailRow(
context, context,
label: l10n.recipient, label: l10n.recipient,
value: recipientName ?? '-', value: recipientName,
), ),
if (recipientPrefixHex != null)
_detailRow( _detailRow(
context, context,
label: l10n.recipientKey, label: l10n.recipientKey,
value: recipientPrefixHex ?? '-', value: recipientPrefixHex,
onCopy: recipientPrefixHex != null onCopy: () => copyField(recipientPrefixHex),
? () => copyField(recipientPrefixHex)
: null,
), ),
], ],
), ),
@@ -1020,6 +1043,48 @@ class _MessageBubbleState extends State<MessageBubble> {
); );
} }
BlePacketLog? _findBestMatchingRxLog(List<BlePacketLog> logs, Message message) {
if (message.pathLen < 0 || message.pathLen >= 255) return null;
final expectedPayloadType = message.messageType == MessageType.channel
? 0x05
: 0x02;
BlePacketLog? bestLog;
var bestDeltaMs = 999999999;
for (final log in logs) {
if (log.responseCode != 0x88) continue; // pushLogRxData
if (log.rawData.length < 6) continue;
// Logged frame format:
// [0]=response code 0x88, [1]=snrRaw, [2]=rssi, [3]=packet header, [4]=pathLen
final raw = log.rawData;
final payloadType = (raw[3] >> 2) & 0x0F;
final pathLen = raw[4];
if (payloadType != expectedPayloadType) continue;
if (pathLen != message.pathLen) continue;
if (raw.length < 5 + pathLen) continue;
final deltaMs =
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
if (deltaMs < bestDeltaMs) {
bestDeltaMs = deltaMs;
bestLog = log;
}
}
if (bestDeltaMs > 30000) return null;
return bestLog;
}
List<int>? _extractPathBytesFromLog(BlePacketLog? log) {
if (log == null) return null;
final raw = log.rawData;
if (raw.length < 6) return null;
final pathLen = raw[4];
if (pathLen <= 0 || raw.length < 5 + pathLen) return null;
return raw.sublist(5, 5 + pathLen);
}
void _showReplySheet(BuildContext context) { void _showReplySheet(BuildContext context) {
// Find the sender contact by public key prefix // Find the sender contact by public key prefix
final contactsProvider = context.read<ContactsProvider>(); final contactsProvider = context.read<ContactsProvider>();