Fix echo metadata and bump build number #123

This commit is contained in:
Janez T
2026-04-26 08:02:53 +02:00
parent f9dfdc2340
commit 4181b33da1
15 changed files with 498 additions and 39 deletions

View File

@@ -2018,9 +2018,9 @@
},
"reply": "Reply",
"@reply": {},
"technicalDetails": "Technical details",
"technicalDetails": "Details",
"@technicalDetails": {},
"messageTechnicalDetails": "Message technical details",
"messageTechnicalDetails": "Message details",
"@messageTechnicalDetails": {},
"linkQuality": "Link quality",
"@linkQuality": {},

View File

@@ -2769,13 +2769,13 @@ abstract class AppLocalizations {
/// No description provided for @technicalDetails.
///
/// In en, this message translates to:
/// **'Technical details'**
/// **'Details'**
String get technicalDetails;
/// No description provided for @messageTechnicalDetails.
///
/// In en, this message translates to:
/// **'Message technical details'**
/// **'Message details'**
String get messageTechnicalDetails;
/// No description provided for @linkQuality.

View File

@@ -1471,10 +1471,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get reply => 'Reply';
@override
String get technicalDetails => 'Technical details';
String get technicalDetails => 'Details';
@override
String get messageTechnicalDetails => 'Message technical details';
String get messageTechnicalDetails => 'Message details';
@override
String get linkQuality => 'Link quality';

View File

@@ -1773,7 +1773,13 @@ class AppProvider with ChangeNotifier {
debugPrint(
'🔊 [AppProvider] Echo detected - Message: $messageId, Count: $echoCount',
);
messagesProvider.handleMessageEcho(messageId, echoCount, snrRaw, rssiDbm);
messagesProvider.handleMessageEcho(
messageId,
echoCount,
snrRaw,
rssiDbm,
pathBytes: _latestChannelEchoPathBytes(),
);
};
connectionProvider.prepareDirectMessageSendCallback =
@@ -4056,6 +4062,23 @@ class AppProvider with ChangeNotifier {
return decoded.pathBytes;
}
Uint8List? _latestChannelEchoPathBytes() {
for (final log in connectionProvider.bleService.packetLogs.reversed) {
if (log.responseCode != 0x88) continue;
final decoded = LogRxRouteDecoder.decode(log.rawData);
if (decoded == null ||
decoded.payloadType != 0x05 ||
decoded.pathBytes.isEmpty) {
continue;
}
return Uint8List.fromList(decoded.pathBytes);
}
return null;
}
// Removed _syncMessages() - messages are automatically synced via PUSH_CODE_MSG_WAITING events
// The ConnectionProvider's onMessageWaiting callback handles automatic message fetching

View File

@@ -198,6 +198,9 @@ class MessagesProvider with ChangeNotifier {
_messages[index] = _messages[index].copyWith(
usedFloodFallback: selection.usesFlood,
pathLen: nextPathLen,
pathBytes: selection.hasDirectPath
? Uint8List.fromList(selection.pathBytes)
: Uint8List(0),
);
}
@@ -803,15 +806,30 @@ class MessagesProvider with ChangeNotifier {
if (contactLocationSnapshot != null) {
_messageContactLocations[existingId] = contactLocationSnapshot;
}
_messageReceptionDetails[existingId] =
MessageReceptionDetails.mergeDuplicate(
existing: _messageReceptionDetails[existingId],
incoming: receptionDetailsSnapshot,
);
final mergedReceptionDetails = MessageReceptionDetails.mergeDuplicate(
existing: _messageReceptionDetails[existingId],
incoming: receptionDetailsSnapshot,
);
_messageReceptionDetails[existingId] = mergedReceptionDetails;
final existingMessage = _messages[matchingSentReplayIndex];
final routeMetadata = _messageRouteMetadata[existingId];
_messages[matchingSentReplayIndex] = existingMessage.copyWith(
pathLen: finalMessage.pathLen > 0 ? finalMessage.pathLen : existingMessage.pathLen,
pathBytes: finalMessage.pathBytes ?? existingMessage.pathBytes,
echoCount: _mergeSentReplayEchoCount(
existingMessage,
mergedReceptionDetails,
),
pathLen: _mergeSentReplayPathLen(
existingMessage,
finalMessage,
routeMetadata,
),
pathBytes: _mergeSentReplayPathBytes(
existingMessage,
finalMessage,
routeMetadata,
),
firstEchoAt: existingMessage.firstEchoAt ?? DateTime.now(),
lastEchoAt: DateTime.now(),
);
_persistMessages();
return;
@@ -1032,6 +1050,51 @@ class MessagesProvider with ChangeNotifier {
return _matchesDuplicateSenderIdentity(existing, message);
}
int _mergeSentReplayEchoCount(
Message existing,
MessageReceptionDetails mergedReceptionDetails,
) {
final replayCount = mergedReceptionDetails.receivedCopies > 0
? mergedReceptionDetails.receivedCopies - 1
: 0;
return replayCount > existing.echoCount ? replayCount : existing.echoCount;
}
int _mergeSentReplayPathLen(
Message existing,
Message incoming,
MessageRouteMetadata? routeMetadata,
) {
if (routeMetadata?.mode == PathSelectionMode.flood ||
existing.usedFloodFallback) {
return existing.pathLen;
}
final routeHopCount = routeMetadata?.hopCount;
if (routeHopCount != null && routeHopCount > 0) {
return routeHopCount;
}
if (existing.pathLen > 0) {
return existing.pathLen;
}
return incoming.pathLen > 0 ? incoming.pathLen : existing.pathLen;
}
Uint8List? _mergeSentReplayPathBytes(
Message existing,
Message incoming,
MessageRouteMetadata? routeMetadata,
) {
if (routeMetadata?.mode == PathSelectionMode.flood ||
existing.usedFloodFallback) {
return existing.pathBytes;
}
return existing.pathBytes ?? incoming.pathBytes;
}
/// Add multiple messages
void addMessages(List<Message> messages) {
int addedCount = 0;
@@ -2166,7 +2229,9 @@ class MessagesProvider with ChangeNotifier {
int echoCount,
int snrRaw,
int rssiDbm,
) {
{
Uint8List? pathBytes,
}) {
debugPrint('🔊 [MessagesProvider] handleMessageEcho called');
debugPrint(' Message ID: $messageId');
debugPrint(' Echo count: $echoCount');
@@ -2181,18 +2246,35 @@ class MessagesProvider with ChangeNotifier {
' ✅ Found message: ${message.text.substring(0, message.text.length > 30 ? 30 : message.text.length)}...',
);
final nextEchoCount = echoCount > message.echoCount
? echoCount
: message.echoCount + 1;
// Update echo count
final updatedMessage = message.copyWith(
echoCount: echoCount,
echoCount: nextEchoCount,
firstEchoAt: message.firstEchoAt ?? DateTime.now(),
lastEchoSnrRaw: snrRaw.toSigned(8),
lastEchoRssiDbm: rssiDbm.toSigned(8),
lastEchoAt: DateTime.now(),
);
_messages[index] = updatedMessage;
_messageReceptionDetails[messageId] = _messageReceptionDetails[messageId]
?.copyWith(
capturedAt: DateTime.now(),
rssiDbm: rssiDbm.toSigned(8),
snrDb: snrRaw.toSigned(8) / 4.0,
pathBytes: pathBytes?.toList(),
) ??
MessageReceptionDetails(
capturedAt: DateTime.now(),
rssiDbm: rssiDbm.toSigned(8),
snrDb: snrRaw.toSigned(8) / 4.0,
pathBytes: pathBytes?.toList(),
);
_clearChannelSendWarning(messageId);
debugPrint(' Updated echo count to: $echoCount');
debugPrint(' Updated echo count to: $nextEchoCount');
_persistMessages();
notifyListeners();
debugPrint(' ✅ Echo update complete, UI notified');

View File

@@ -79,7 +79,7 @@ class LogRxRouteDecoder {
}
final pathBytes = rawPacketData.sublist(index, index + pathByteLen);
final hashSize = pathMode == 0
? inferHashSize(pathBytes, preferredHashSize: preferredHashSize)
? 1
: (descriptorHashSize(pathDescriptor) ??
inferHashSize(pathBytes, preferredHashSize: preferredHashSize));

View File

@@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import '../models/message.dart';
import '../models/message_route_metadata.dart';
import '../l10n/app_localizations.dart';
import '../providers/messages_provider.dart';
@@ -14,7 +15,7 @@ extension MessageLocalization on Message {
// For channel messages, show echo count instead of delivery status
if (isChannelMessage && deliveryStatus == MessageDeliveryStatus.sent) {
final latestMeta = _formatEchoMeta(context);
final latestMeta = _formatChannelStatusMeta(context, routeMetadata);
if (echoCount == 0) {
if (messagesProvider.hasChannelSendWarning(id)) {
return 'Broadcast may have failed';
@@ -127,6 +128,41 @@ extension MessageLocalization on Message {
return parts.join('');
}
String? _formatChannelStatusMeta(
BuildContext context,
MessageRouteMetadata? routeMetadata,
) {
final parts = <String>[];
final hopLabel = _formatChannelHopMeta(routeMetadata);
if (hopLabel != null) {
parts.add(hopLabel);
}
final echoMeta = _formatEchoMeta(context);
if (echoMeta != null) {
parts.add(echoMeta);
}
if (parts.isEmpty) {
return null;
}
return parts.join('');
}
String? _formatChannelHopMeta(MessageRouteMetadata? routeMetadata) {
if (routeMetadata?.mode.name == 'flood') {
return routeMetadata!.modeLabel;
}
final effectivePathLen = routeMetadata?.hopCount ?? pathLen;
if (effectivePathLen <= 0 || effectivePathLen >= 255) {
return null;
}
return '$effectivePathLen hop${effectivePathLen == 1 ? '' : 's'}';
}
String _barsForRssi(int rssiDbm) {
// Approximate useful RSSI range: -120..-70 dBm
final score = ((rssiDbm + 120) / 10).round().clamp(0, 5);

View File

@@ -534,7 +534,7 @@ class _MessageBubbleState extends State<MessageBubble> {
_hideDrawingFromMap(parentContext);
},
),
// Technical details option
// Details option
ListTile(
leading: Icon(Icons.data_object),
title: Text(l10n.technicalDetails),
@@ -676,6 +676,15 @@ class _MessageBubbleState extends State<MessageBubble> {
final packetPathHex = (receptionDetails?.pathBytes ?? packetPathBytes)
?.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(':');
final lastEchoRelayHash = receptionDetails?.pathBytes?.isNotEmpty == true
? receptionDetails!.pathBytes!.last
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase()
: null;
final lastEchoBytesReport = _formatPathBytesReport(
receptionDetails?.pathBytes,
);
final snrDb =
receptionDetails?.snrDb ??
matchedRxLog?.logRxDataInfo?.snrDb ??
@@ -712,6 +721,9 @@ class _MessageBubbleState extends State<MessageBubble> {
'Received at (RFC3339): ${_formatRfc3339(widget.message.receivedAt)}',
'Channel index: ${widget.message.channelIdx ?? '-'}',
'Echo count: ${widget.message.echoCount}',
'Last echo relay hash: ${lastEchoRelayHash ?? '-'}',
'Last echo path bytes: ${receptionDetails?.pathBytesHex ?? '-'}',
'Last echo bytes report: ${lastEchoBytesReport ?? '-'}',
'Last echo RSSI: ${widget.message.lastEchoRssiDbm ?? '-'}',
'Last echo SNR: ${snrDb?.toStringAsFixed(2) ?? '-'}',
'Matched RX RSSI: ${rssiDbm ?? '-'}',
@@ -1044,6 +1056,33 @@ class _MessageBubbleState extends State<MessageBubble> {
label: l10n.receivedCopies,
value: '${widget.receivedCopies}',
),
if (lastEchoRelayHash != null)
_detailRow(
sheetContext,
label: 'Last echo relay',
value: lastEchoRelayHash,
onCopy: () =>
copyField(sheetContext, lastEchoRelayHash),
),
if (receptionDetails?.pathBytesHex
case final echoPath?)
_detailRow(
sheetContext,
label: 'Last echo path',
value: echoPath,
onCopy: () =>
copyField(sheetContext, echoPath),
),
if (lastEchoBytesReport != null)
_detailRow(
sheetContext,
label: 'Last echo bytes report',
value: lastEchoBytesReport,
onCopy: () => copyField(
sheetContext,
lastEchoBytesReport,
),
),
if (widget.message.suggestedTimeoutMs != null)
_detailRow(
sheetContext,
@@ -1624,6 +1663,23 @@ class _MessageBubbleState extends State<MessageBubble> {
return '$durationMs ms';
}
String? _formatPathBytesReport(List<int>? pathBytes) {
if (pathBytes == null || pathBytes.isEmpty) {
return null;
}
final byteHex = pathBytes
.map((byte) => byte.toRadixString(16).padLeft(2, '0').toUpperCase())
.toList();
final indexedHops = byteHex
.asMap()
.entries
.map((entry) => '#${entry.key + 1}=${entry.value}')
.join(', ');
final byteLabel = pathBytes.length == 1 ? 'byte' : 'bytes';
return '${pathBytes.length} $byteLabel [${byteHex.join(' ')}] • hops $indexedHops';
}
BlePacketLog? _findBestMatchingRxLog(
List<BlePacketLog> logs,
Message message,