mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
Fix echo metadata and bump build number #123
This commit is contained in:
@@ -489,7 +489,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 140;
|
CURRENT_PROJECT_VERSION = 141;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 140;
|
CURRENT_PROJECT_VERSION = 141;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -530,7 +530,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 140;
|
CURRENT_PROJECT_VERSION = 141;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -547,7 +547,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 140;
|
CURRENT_PROJECT_VERSION = 141;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -679,7 +679,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 140;
|
CURRENT_PROJECT_VERSION = 141;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -702,7 +702,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 140;
|
CURRENT_PROJECT_VERSION = 141;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>140</string>
|
<string>141</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -5,22 +5,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000271">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000252">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.342101">
|
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.48394">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="145.467717">
|
<testcase classname="fastlane.lanes" name="2: build_app" time="153.436107">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="188.692143">
|
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="6642.682605">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -2018,9 +2018,9 @@
|
|||||||
},
|
},
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"@reply": {},
|
"@reply": {},
|
||||||
"technicalDetails": "Technical details",
|
"technicalDetails": "Details",
|
||||||
"@technicalDetails": {},
|
"@technicalDetails": {},
|
||||||
"messageTechnicalDetails": "Message technical details",
|
"messageTechnicalDetails": "Message details",
|
||||||
"@messageTechnicalDetails": {},
|
"@messageTechnicalDetails": {},
|
||||||
"linkQuality": "Link quality",
|
"linkQuality": "Link quality",
|
||||||
"@linkQuality": {},
|
"@linkQuality": {},
|
||||||
|
|||||||
@@ -2769,13 +2769,13 @@ abstract class AppLocalizations {
|
|||||||
/// No description provided for @technicalDetails.
|
/// No description provided for @technicalDetails.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Technical details'**
|
/// **'Details'**
|
||||||
String get technicalDetails;
|
String get technicalDetails;
|
||||||
|
|
||||||
/// No description provided for @messageTechnicalDetails.
|
/// No description provided for @messageTechnicalDetails.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Message technical details'**
|
/// **'Message details'**
|
||||||
String get messageTechnicalDetails;
|
String get messageTechnicalDetails;
|
||||||
|
|
||||||
/// No description provided for @linkQuality.
|
/// No description provided for @linkQuality.
|
||||||
|
|||||||
@@ -1471,10 +1471,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get reply => 'Reply';
|
String get reply => 'Reply';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get technicalDetails => 'Technical details';
|
String get technicalDetails => 'Details';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get messageTechnicalDetails => 'Message technical details';
|
String get messageTechnicalDetails => 'Message details';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get linkQuality => 'Link quality';
|
String get linkQuality => 'Link quality';
|
||||||
|
|||||||
@@ -1773,7 +1773,13 @@ class AppProvider with ChangeNotifier {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'🔊 [AppProvider] Echo detected - Message: $messageId, Count: $echoCount',
|
'🔊 [AppProvider] Echo detected - Message: $messageId, Count: $echoCount',
|
||||||
);
|
);
|
||||||
messagesProvider.handleMessageEcho(messageId, echoCount, snrRaw, rssiDbm);
|
messagesProvider.handleMessageEcho(
|
||||||
|
messageId,
|
||||||
|
echoCount,
|
||||||
|
snrRaw,
|
||||||
|
rssiDbm,
|
||||||
|
pathBytes: _latestChannelEchoPathBytes(),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
connectionProvider.prepareDirectMessageSendCallback =
|
connectionProvider.prepareDirectMessageSendCallback =
|
||||||
@@ -4056,6 +4062,23 @@ class AppProvider with ChangeNotifier {
|
|||||||
return decoded.pathBytes;
|
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
|
// Removed _syncMessages() - messages are automatically synced via PUSH_CODE_MSG_WAITING events
|
||||||
// The ConnectionProvider's onMessageWaiting callback handles automatic message fetching
|
// The ConnectionProvider's onMessageWaiting callback handles automatic message fetching
|
||||||
|
|
||||||
|
|||||||
@@ -198,6 +198,9 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_messages[index] = _messages[index].copyWith(
|
_messages[index] = _messages[index].copyWith(
|
||||||
usedFloodFallback: selection.usesFlood,
|
usedFloodFallback: selection.usesFlood,
|
||||||
pathLen: nextPathLen,
|
pathLen: nextPathLen,
|
||||||
|
pathBytes: selection.hasDirectPath
|
||||||
|
? Uint8List.fromList(selection.pathBytes)
|
||||||
|
: Uint8List(0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,15 +806,30 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
if (contactLocationSnapshot != null) {
|
if (contactLocationSnapshot != null) {
|
||||||
_messageContactLocations[existingId] = contactLocationSnapshot;
|
_messageContactLocations[existingId] = contactLocationSnapshot;
|
||||||
}
|
}
|
||||||
_messageReceptionDetails[existingId] =
|
final mergedReceptionDetails = MessageReceptionDetails.mergeDuplicate(
|
||||||
MessageReceptionDetails.mergeDuplicate(
|
existing: _messageReceptionDetails[existingId],
|
||||||
existing: _messageReceptionDetails[existingId],
|
incoming: receptionDetailsSnapshot,
|
||||||
incoming: receptionDetailsSnapshot,
|
);
|
||||||
);
|
_messageReceptionDetails[existingId] = mergedReceptionDetails;
|
||||||
final existingMessage = _messages[matchingSentReplayIndex];
|
final existingMessage = _messages[matchingSentReplayIndex];
|
||||||
|
final routeMetadata = _messageRouteMetadata[existingId];
|
||||||
_messages[matchingSentReplayIndex] = existingMessage.copyWith(
|
_messages[matchingSentReplayIndex] = existingMessage.copyWith(
|
||||||
pathLen: finalMessage.pathLen > 0 ? finalMessage.pathLen : existingMessage.pathLen,
|
echoCount: _mergeSentReplayEchoCount(
|
||||||
pathBytes: finalMessage.pathBytes ?? existingMessage.pathBytes,
|
existingMessage,
|
||||||
|
mergedReceptionDetails,
|
||||||
|
),
|
||||||
|
pathLen: _mergeSentReplayPathLen(
|
||||||
|
existingMessage,
|
||||||
|
finalMessage,
|
||||||
|
routeMetadata,
|
||||||
|
),
|
||||||
|
pathBytes: _mergeSentReplayPathBytes(
|
||||||
|
existingMessage,
|
||||||
|
finalMessage,
|
||||||
|
routeMetadata,
|
||||||
|
),
|
||||||
|
firstEchoAt: existingMessage.firstEchoAt ?? DateTime.now(),
|
||||||
|
lastEchoAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
return;
|
return;
|
||||||
@@ -1032,6 +1050,51 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return _matchesDuplicateSenderIdentity(existing, message);
|
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
|
/// Add multiple messages
|
||||||
void addMessages(List<Message> messages) {
|
void addMessages(List<Message> messages) {
|
||||||
int addedCount = 0;
|
int addedCount = 0;
|
||||||
@@ -2166,7 +2229,9 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
int echoCount,
|
int echoCount,
|
||||||
int snrRaw,
|
int snrRaw,
|
||||||
int rssiDbm,
|
int rssiDbm,
|
||||||
) {
|
{
|
||||||
|
Uint8List? pathBytes,
|
||||||
|
}) {
|
||||||
debugPrint('🔊 [MessagesProvider] handleMessageEcho called');
|
debugPrint('🔊 [MessagesProvider] handleMessageEcho called');
|
||||||
debugPrint(' Message ID: $messageId');
|
debugPrint(' Message ID: $messageId');
|
||||||
debugPrint(' Echo count: $echoCount');
|
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)}...',
|
' ✅ 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
|
// Update echo count
|
||||||
final updatedMessage = message.copyWith(
|
final updatedMessage = message.copyWith(
|
||||||
echoCount: echoCount,
|
echoCount: nextEchoCount,
|
||||||
firstEchoAt: message.firstEchoAt ?? DateTime.now(),
|
firstEchoAt: message.firstEchoAt ?? DateTime.now(),
|
||||||
lastEchoSnrRaw: snrRaw.toSigned(8),
|
lastEchoSnrRaw: snrRaw.toSigned(8),
|
||||||
lastEchoRssiDbm: rssiDbm.toSigned(8),
|
lastEchoRssiDbm: rssiDbm.toSigned(8),
|
||||||
lastEchoAt: DateTime.now(),
|
lastEchoAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
_messages[index] = updatedMessage;
|
_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);
|
_clearChannelSendWarning(messageId);
|
||||||
|
|
||||||
debugPrint(' Updated echo count to: $echoCount');
|
debugPrint(' Updated echo count to: $nextEchoCount');
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
debugPrint(' ✅ Echo update complete, UI notified');
|
debugPrint(' ✅ Echo update complete, UI notified');
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class LogRxRouteDecoder {
|
|||||||
}
|
}
|
||||||
final pathBytes = rawPacketData.sublist(index, index + pathByteLen);
|
final pathBytes = rawPacketData.sublist(index, index + pathByteLen);
|
||||||
final hashSize = pathMode == 0
|
final hashSize = pathMode == 0
|
||||||
? inferHashSize(pathBytes, preferredHashSize: preferredHashSize)
|
? 1
|
||||||
: (descriptorHashSize(pathDescriptor) ??
|
: (descriptorHashSize(pathDescriptor) ??
|
||||||
inferHashSize(pathBytes, preferredHashSize: preferredHashSize));
|
inferHashSize(pathBytes, preferredHashSize: preferredHashSize));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../models/message.dart';
|
import '../models/message.dart';
|
||||||
|
import '../models/message_route_metadata.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../providers/messages_provider.dart';
|
import '../providers/messages_provider.dart';
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ extension MessageLocalization on Message {
|
|||||||
|
|
||||||
// For channel messages, show echo count instead of delivery status
|
// For channel messages, show echo count instead of delivery status
|
||||||
if (isChannelMessage && deliveryStatus == MessageDeliveryStatus.sent) {
|
if (isChannelMessage && deliveryStatus == MessageDeliveryStatus.sent) {
|
||||||
final latestMeta = _formatEchoMeta(context);
|
final latestMeta = _formatChannelStatusMeta(context, routeMetadata);
|
||||||
if (echoCount == 0) {
|
if (echoCount == 0) {
|
||||||
if (messagesProvider.hasChannelSendWarning(id)) {
|
if (messagesProvider.hasChannelSendWarning(id)) {
|
||||||
return 'Broadcast may have failed';
|
return 'Broadcast may have failed';
|
||||||
@@ -127,6 +128,41 @@ extension MessageLocalization on Message {
|
|||||||
return parts.join(' • ');
|
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) {
|
String _barsForRssi(int rssiDbm) {
|
||||||
// Approximate useful RSSI range: -120..-70 dBm
|
// Approximate useful RSSI range: -120..-70 dBm
|
||||||
final score = ((rssiDbm + 120) / 10).round().clamp(0, 5);
|
final score = ((rssiDbm + 120) / 10).round().clamp(0, 5);
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
_hideDrawingFromMap(parentContext);
|
_hideDrawingFromMap(parentContext);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// Technical details option
|
// Details option
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(Icons.data_object),
|
leading: Icon(Icons.data_object),
|
||||||
title: Text(l10n.technicalDetails),
|
title: Text(l10n.technicalDetails),
|
||||||
@@ -676,6 +676,15 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
final packetPathHex = (receptionDetails?.pathBytes ?? packetPathBytes)
|
final packetPathHex = (receptionDetails?.pathBytes ?? packetPathBytes)
|
||||||
?.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
?.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
.join(':');
|
.join(':');
|
||||||
|
final lastEchoRelayHash = receptionDetails?.pathBytes?.isNotEmpty == true
|
||||||
|
? receptionDetails!.pathBytes!.last
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0')
|
||||||
|
.toUpperCase()
|
||||||
|
: null;
|
||||||
|
final lastEchoBytesReport = _formatPathBytesReport(
|
||||||
|
receptionDetails?.pathBytes,
|
||||||
|
);
|
||||||
final snrDb =
|
final snrDb =
|
||||||
receptionDetails?.snrDb ??
|
receptionDetails?.snrDb ??
|
||||||
matchedRxLog?.logRxDataInfo?.snrDb ??
|
matchedRxLog?.logRxDataInfo?.snrDb ??
|
||||||
@@ -712,6 +721,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
'Received at (RFC3339): ${_formatRfc3339(widget.message.receivedAt)}',
|
'Received at (RFC3339): ${_formatRfc3339(widget.message.receivedAt)}',
|
||||||
'Channel index: ${widget.message.channelIdx ?? '-'}',
|
'Channel index: ${widget.message.channelIdx ?? '-'}',
|
||||||
'Echo count: ${widget.message.echoCount}',
|
'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 RSSI: ${widget.message.lastEchoRssiDbm ?? '-'}',
|
||||||
'Last echo SNR: ${snrDb?.toStringAsFixed(2) ?? '-'}',
|
'Last echo SNR: ${snrDb?.toStringAsFixed(2) ?? '-'}',
|
||||||
'Matched RX RSSI: ${rssiDbm ?? '-'}',
|
'Matched RX RSSI: ${rssiDbm ?? '-'}',
|
||||||
@@ -1044,6 +1056,33 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
label: l10n.receivedCopies,
|
label: l10n.receivedCopies,
|
||||||
value: '${widget.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)
|
if (widget.message.suggestedTimeoutMs != null)
|
||||||
_detailRow(
|
_detailRow(
|
||||||
sheetContext,
|
sheetContext,
|
||||||
@@ -1624,6 +1663,23 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
return '$durationMs ms';
|
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(
|
BlePacketLog? _findBestMatchingRxLog(
|
||||||
List<BlePacketLog> logs,
|
List<BlePacketLog> logs,
|
||||||
Message message,
|
Message message,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2026.0414.1+54
|
version: 2026.0420.1+55
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
|
|||||||
@@ -254,6 +254,47 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('repeated echo callbacks accumulate when radio reports one each time', () {
|
||||||
|
final provider = MessagesProvider();
|
||||||
|
provider.addSentMessage(
|
||||||
|
_buildSentChannelMessage(
|
||||||
|
id: 'c-echo-callbacks',
|
||||||
|
senderTimestamp: 1700000002,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
provider.markMessageSent('c-echo-callbacks', 0, 0);
|
||||||
|
|
||||||
|
provider.handleMessageEcho(
|
||||||
|
'c-echo-callbacks',
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
-90,
|
||||||
|
pathBytes: Uint8List.fromList([0xAA]),
|
||||||
|
);
|
||||||
|
provider.handleMessageEcho(
|
||||||
|
'c-echo-callbacks',
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
-89,
|
||||||
|
pathBytes: Uint8List.fromList([0xAA, 0xBB]),
|
||||||
|
);
|
||||||
|
provider.handleMessageEcho(
|
||||||
|
'c-echo-callbacks',
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
-88,
|
||||||
|
pathBytes: Uint8List.fromList([0xAA, 0xBB, 0xCC]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(provider.messages.single.echoCount, equals(3));
|
||||||
|
expect(provider.messages.single.lastEchoSnrRaw, equals(6));
|
||||||
|
expect(provider.messages.single.lastEchoRssiDbm, equals(-88));
|
||||||
|
expect(
|
||||||
|
provider.getMessageReceptionDetails('c-echo-callbacks')?.pathBytes,
|
||||||
|
[0xAA, 0xBB, 0xCC],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('channel warning clears when replay arrives after send', () {
|
test('channel warning clears when replay arrives after send', () {
|
||||||
fakeAsync((async) {
|
fakeAsync((async) {
|
||||||
final provider = MessagesProvider();
|
final provider = MessagesProvider();
|
||||||
@@ -302,9 +343,110 @@ void main() {
|
|||||||
|
|
||||||
expect(provider.messages, hasLength(1));
|
expect(provider.messages, hasLength(1));
|
||||||
expect(provider.messages.single.id, equals('c-echo'));
|
expect(provider.messages.single.id, equals('c-echo'));
|
||||||
|
expect(provider.messages.single.echoCount, equals(1));
|
||||||
expect(provider.messages.single.pathLen, equals(1));
|
expect(provider.messages.single.pathLen, equals(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('channel replay count reflects how many times a sent message was heard', () {
|
||||||
|
final provider = MessagesProvider();
|
||||||
|
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
|
||||||
|
provider.addSentMessage(
|
||||||
|
_buildSentChannelMessage(
|
||||||
|
id: 'c-repeat-count',
|
||||||
|
senderTimestamp: 1700000110,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
provider.markMessageSent('c-repeat-count', 0, 0);
|
||||||
|
|
||||||
|
provider.addMessage(
|
||||||
|
_buildReceivedChannelReplay(
|
||||||
|
id: 'c-repeat-count-1',
|
||||||
|
senderTimestamp: 1700000111,
|
||||||
|
senderName: 'dz0ny (SI)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
provider.addMessage(
|
||||||
|
_buildReceivedChannelReplay(
|
||||||
|
id: 'c-repeat-count-2',
|
||||||
|
senderTimestamp: 1700000112,
|
||||||
|
senderName: 'dz0ny (SI)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(provider.messages, hasLength(1));
|
||||||
|
expect(provider.messages.single.echoCount, equals(2));
|
||||||
|
expect(provider.getMessageReceptionDetails('c-repeat-count')?.receivedCopies, equals(3));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('channel replay keeps flood mode hop count untouched', () {
|
||||||
|
final provider = MessagesProvider();
|
||||||
|
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
|
||||||
|
provider.addSentMessage(
|
||||||
|
_buildSentChannelMessage(
|
||||||
|
id: 'c-flood-path',
|
||||||
|
senderTimestamp: 1700000120,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
provider.updateMessageRouteSelection(
|
||||||
|
'c-flood-path',
|
||||||
|
PathSelection.flood(),
|
||||||
|
routerFallbackAttempted: false,
|
||||||
|
);
|
||||||
|
provider.markMessageSent('c-flood-path', 0, 0);
|
||||||
|
|
||||||
|
provider.addMessage(
|
||||||
|
_buildReceivedChannelReplay(
|
||||||
|
id: 'c-flood-path-incoming',
|
||||||
|
senderTimestamp: 1700000121,
|
||||||
|
senderName: 'dz0ny (SI)',
|
||||||
|
).copyWith(pathBytes: Uint8List.fromList([0xAA, 0xBB])),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(provider.messages, hasLength(1));
|
||||||
|
expect(provider.messages.single.echoCount, equals(1));
|
||||||
|
expect(provider.messages.single.pathLen, equals(0));
|
||||||
|
expect(provider.messages.single.pathBytes, isEmpty);
|
||||||
|
expect(
|
||||||
|
provider.getMessageRouteMetadata('c-flood-path')?.mode,
|
||||||
|
PathSelectionMode.flood,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('channel replay preserves sent direct path bytes', () {
|
||||||
|
final provider = MessagesProvider();
|
||||||
|
provider.resolveContactNameCallback = (_) => 'dz0ny (SI)';
|
||||||
|
provider.addSentMessage(
|
||||||
|
_buildSentChannelMessage(
|
||||||
|
id: 'c-direct-path',
|
||||||
|
senderTimestamp: 1700000130,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
provider.updateMessageRouteSelection(
|
||||||
|
'c-direct-path',
|
||||||
|
PathSelection(
|
||||||
|
mode: PathSelectionMode.directCurrent,
|
||||||
|
pathBytes: Uint8List.fromList([0x01, 0x02]),
|
||||||
|
hopCount: 2,
|
||||||
|
hashSize: 1,
|
||||||
|
),
|
||||||
|
routerFallbackAttempted: false,
|
||||||
|
);
|
||||||
|
provider.markMessageSent('c-direct-path', 0, 0);
|
||||||
|
|
||||||
|
provider.addMessage(
|
||||||
|
_buildReceivedChannelReplay(
|
||||||
|
id: 'c-direct-path-incoming',
|
||||||
|
senderTimestamp: 1700000131,
|
||||||
|
senderName: 'dz0ny (SI)',
|
||||||
|
).copyWith(pathBytes: Uint8List.fromList([0xAA, 0xBB])),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(provider.messages, hasLength(1));
|
||||||
|
expect(provider.messages.single.echoCount, equals(1));
|
||||||
|
expect(provider.messages.single.pathLen, equals(2));
|
||||||
|
expect(provider.messages.single.pathBytes, [0x01, 0x02]);
|
||||||
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'channel replay is not deduped for different sender with same text',
|
'channel replay is not deduped for different sender with same text',
|
||||||
() {
|
() {
|
||||||
|
|||||||
@@ -26,9 +26,30 @@ void main() {
|
|||||||
expect(decoded!.payloadType, 0x01);
|
expect(decoded!.payloadType, 0x01);
|
||||||
expect(decoded.pathDescriptor, 0x04);
|
expect(decoded.pathDescriptor, 0x04);
|
||||||
expect(decoded.pathBytes, [0xc2, 0xba, 0x5f, 0xde]);
|
expect(decoded.pathBytes, [0xc2, 0xba, 0x5f, 0xde]);
|
||||||
expect(decoded.hashSize, 2);
|
expect(decoded.hashSize, 1);
|
||||||
expect(decoded.hopHashes, ['c2ba', '5fde']);
|
expect(decoded.hopHashes, ['c2', 'ba', '5f', 'de']);
|
||||||
expect(decoded.originalSenderHashHex, 'c2ba');
|
expect(decoded.originalSenderHashHex, 'c2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses legacy two byte paths as two one-byte hops', () {
|
||||||
|
final packet = Uint8List.fromList([
|
||||||
|
0x88,
|
||||||
|
0x37,
|
||||||
|
0xae,
|
||||||
|
0x05,
|
||||||
|
0x02,
|
||||||
|
0xc2,
|
||||||
|
0xba,
|
||||||
|
]);
|
||||||
|
|
||||||
|
final decoded = LogRxRouteDecoder.decode(packet);
|
||||||
|
|
||||||
|
expect(decoded, isNotNull);
|
||||||
|
expect(decoded!.pathDescriptor, 0x02);
|
||||||
|
expect(decoded.pathBytes, [0xc2, 0xba]);
|
||||||
|
expect(decoded.hashSize, 1);
|
||||||
|
expect(decoded.hopCount, 2);
|
||||||
|
expect(decoded.hopHashes, ['c2', 'ba']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('parses encoded descriptor with 2-byte hashes', () {
|
test('parses encoded descriptor with 2-byte hashes', () {
|
||||||
@@ -55,7 +76,7 @@ void main() {
|
|||||||
expect(decoded.hopHashes, ['c2ba', '5fde']);
|
expect(decoded.hopHashes, ['c2ba', '5fde']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uses preferred hash size when packet length is ambiguous', () {
|
test('uses one byte hashes for legacy packet lengths', () {
|
||||||
final packet = Uint8List.fromList([
|
final packet = Uint8List.fromList([
|
||||||
0x88,
|
0x88,
|
||||||
0x37,
|
0x37,
|
||||||
@@ -70,7 +91,7 @@ void main() {
|
|||||||
0xff,
|
0xff,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final decoded = LogRxRouteDecoder.decode(packet, preferredHashSize: 1);
|
final decoded = LogRxRouteDecoder.decode(packet, preferredHashSize: 2);
|
||||||
|
|
||||||
expect(decoded, isNotNull);
|
expect(decoded, isNotNull);
|
||||||
expect(decoded!.hashSize, 1);
|
expect(decoded!.hashSize, 1);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:geolocator/geolocator.dart';
|
|||||||
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
||||||
import 'package:meshcore_sar_app/models/message.dart';
|
import 'package:meshcore_sar_app/models/message.dart';
|
||||||
import 'package:meshcore_sar_app/models/message_contact_location.dart';
|
import 'package:meshcore_sar_app/models/message_contact_location.dart';
|
||||||
|
import 'package:meshcore_sar_app/models/path_selection.dart';
|
||||||
import 'package:meshcore_sar_app/providers/app_provider.dart';
|
import 'package:meshcore_sar_app/providers/app_provider.dart';
|
||||||
import 'package:meshcore_sar_app/providers/channels_provider.dart';
|
import 'package:meshcore_sar_app/providers/channels_provider.dart';
|
||||||
import 'package:meshcore_sar_app/providers/connection_provider.dart';
|
import 'package:meshcore_sar_app/providers/connection_provider.dart';
|
||||||
@@ -18,6 +19,7 @@ import 'package:meshcore_sar_app/providers/voice_provider.dart';
|
|||||||
import 'package:meshcore_sar_app/services/location_tracking_service.dart';
|
import 'package:meshcore_sar_app/services/location_tracking_service.dart';
|
||||||
import 'package:meshcore_sar_app/services/voice_codec_service.dart';
|
import 'package:meshcore_sar_app/services/voice_codec_service.dart';
|
||||||
import 'package:meshcore_sar_app/services/voice_player_service.dart';
|
import 'package:meshcore_sar_app/services/voice_player_service.dart';
|
||||||
|
import 'package:meshcore_sar_app/utils/message_extensions.dart';
|
||||||
import 'package:meshcore_sar_app/widgets/messages/message_bubble.dart';
|
import 'package:meshcore_sar_app/widgets/messages/message_bubble.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -187,6 +189,103 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('sent channel status shows heard count and flood route', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final harness = await _TestHarness.create();
|
||||||
|
try {
|
||||||
|
final message = Message(
|
||||||
|
id: 'sent-channel-status',
|
||||||
|
messageType: MessageType.channel,
|
||||||
|
senderPublicKeyPrefix: _prefix(22),
|
||||||
|
channelIdx: 0,
|
||||||
|
pathLen: 0,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: 1700000001,
|
||||||
|
text: 'Flood status',
|
||||||
|
receivedAt: DateTime.fromMillisecondsSinceEpoch(1700000001500),
|
||||||
|
deliveryStatus: MessageDeliveryStatus.sent,
|
||||||
|
echoCount: 2,
|
||||||
|
);
|
||||||
|
harness.messagesProvider.updateMessageRouteSelection(
|
||||||
|
'sent-channel-status',
|
||||||
|
PathSelection.flood(),
|
||||||
|
routerFallbackAttempted: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider.value(value: harness.messagesProvider),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) =>
|
||||||
|
Text(message.getLocalizedDeliveryStatus(context)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('2 nodes • Flood route'), findsOneWidget);
|
||||||
|
} finally {
|
||||||
|
await _disposeHarness(tester, harness);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('details show sent channel echo relay path', (tester) async {
|
||||||
|
final harness = await _TestHarness.create();
|
||||||
|
try {
|
||||||
|
final message = Message(
|
||||||
|
id: 'sent-channel-echo-details',
|
||||||
|
messageType: MessageType.channel,
|
||||||
|
senderPublicKeyPrefix: _prefix(23),
|
||||||
|
channelIdx: 0,
|
||||||
|
pathLen: 0,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: 1700000002,
|
||||||
|
text: 'Echo detail path',
|
||||||
|
receivedAt: DateTime.fromMillisecondsSinceEpoch(1700000002500),
|
||||||
|
deliveryStatus: MessageDeliveryStatus.sent,
|
||||||
|
echoCount: 1,
|
||||||
|
lastEchoRssiDbm: -88,
|
||||||
|
lastEchoSnrRaw: 6,
|
||||||
|
);
|
||||||
|
harness.messagesProvider.addSentMessage(message);
|
||||||
|
harness.messagesProvider.handleMessageEcho(
|
||||||
|
'sent-channel-echo-details',
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
-88,
|
||||||
|
pathBytes: Uint8List.fromList([0x10, 0x20, 0xAA]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(_buildApp(harness, message));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.longPress(find.text('Echo detail path'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Details'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Last echo relay'), findsOneWidget);
|
||||||
|
expect(find.text('AA'), findsOneWidget);
|
||||||
|
expect(find.text('Last echo path'), findsOneWidget);
|
||||||
|
expect(find.text('10:20:aa'), findsWidgets);
|
||||||
|
expect(find.text('Last echo bytes report'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.text('3 bytes [10 20 AA] • hops #1=10, #2=20, #3=AA'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await _disposeHarness(tester, harness);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('channel bubbles refresh to synced channel names', (
|
testWidgets('channel bubbles refresh to synced channel names', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@@ -403,7 +502,7 @@ void main() {
|
|||||||
await tester.longPress(find.text('Location details'));
|
await tester.longPress(find.text('Location details'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find.text('Technical details'));
|
await tester.tap(find.text('Details'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(flutter_map.FlutterMap), findsOneWidget);
|
expect(find.byType(flutter_map.FlutterMap), findsOneWidget);
|
||||||
@@ -451,7 +550,7 @@ void main() {
|
|||||||
await tester.longPress(find.text('Channel fallback'));
|
await tester.longPress(find.text('Channel fallback'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.tap(find.text('Technical details'));
|
await tester.tap(find.text('Details'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(flutter_map.FlutterMap), findsOneWidget);
|
expect(find.byType(flutter_map.FlutterMap), findsOneWidget);
|
||||||
|
|||||||
Reference in New Issue
Block a user