mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add trace button and raw packets
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
Reference in New Issue
Block a user