mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
fix: Widen worker stats graph
This commit is contained in:
@@ -41,26 +41,22 @@ import '../utils/log_rx_route_decoder.dart';
|
||||
class _DirectMessageRouteSession {
|
||||
final PathSelection currentSelection;
|
||||
final ParsedContactRoute? originalRoute;
|
||||
final bool usedManualOverride;
|
||||
final bool routerFallbackAttempted;
|
||||
|
||||
const _DirectMessageRouteSession({
|
||||
required this.currentSelection,
|
||||
required this.originalRoute,
|
||||
required this.usedManualOverride,
|
||||
required this.routerFallbackAttempted,
|
||||
});
|
||||
|
||||
_DirectMessageRouteSession copyWith({
|
||||
PathSelection? currentSelection,
|
||||
ParsedContactRoute? originalRoute,
|
||||
bool? usedManualOverride,
|
||||
bool? routerFallbackAttempted,
|
||||
}) {
|
||||
return _DirectMessageRouteSession(
|
||||
currentSelection: currentSelection ?? this.currentSelection,
|
||||
originalRoute: originalRoute ?? this.originalRoute,
|
||||
usedManualOverride: usedManualOverride ?? this.usedManualOverride,
|
||||
routerFallbackAttempted:
|
||||
routerFallbackAttempted ?? this.routerFallbackAttempted,
|
||||
);
|
||||
@@ -199,9 +195,6 @@ class AppProvider with ChangeNotifier {
|
||||
bool get isVoiceNoiseSuppressionEnabled => _isVoiceNoiseSuppressionEnabled;
|
||||
double _messageFontScale = 1.0;
|
||||
double get messageFontScale => _messageFontScale;
|
||||
bool _autoRouteRotationEnabled =
|
||||
MessagingRoutePreferences.defaultAutoRouteRotationEnabled;
|
||||
bool get autoRouteRotationEnabled => _autoRouteRotationEnabled;
|
||||
bool _clearPathOnMaxRetry =
|
||||
MessagingRoutePreferences.defaultClearPathOnMaxRetry;
|
||||
bool get clearPathOnMaxRetry => _clearPathOnMaxRetry;
|
||||
@@ -213,6 +206,7 @@ class AppProvider with ChangeNotifier {
|
||||
const NearestRouterSelector();
|
||||
final Map<String, _DirectMessageRouteSession> _directMessageRouteSessions =
|
||||
{};
|
||||
final Set<String> _pendingDeliveredRouteRefreshContacts = <String>{};
|
||||
|
||||
static const Duration _packetRetryDelay = Duration(milliseconds: 1200);
|
||||
static const Duration _mediaSwarmResponseWindow = Duration(seconds: 10);
|
||||
@@ -873,8 +867,7 @@ class AppProvider with ChangeNotifier {
|
||||
|
||||
Future<void> _loadMessagingRouteSettings() async {
|
||||
try {
|
||||
_autoRouteRotationEnabled =
|
||||
await MessagingRoutePreferences.getAutoRouteRotationEnabled();
|
||||
await MessagingRoutePreferences.cleanupLegacySettings();
|
||||
_clearPathOnMaxRetry =
|
||||
await MessagingRoutePreferences.getClearPathOnMaxRetry();
|
||||
_nearestRelayFallbackEnabled =
|
||||
@@ -885,16 +878,6 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleAutoRouteRotationEnabled(bool enabled) async {
|
||||
try {
|
||||
_autoRouteRotationEnabled = enabled;
|
||||
await MessagingRoutePreferences.setAutoRouteRotationEnabled(enabled);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error saving auto route rotation setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleClearPathOnMaxRetry(bool enabled) async {
|
||||
try {
|
||||
_clearPathOnMaxRetry = enabled;
|
||||
@@ -1048,6 +1031,14 @@ class AppProvider with ChangeNotifier {
|
||||
contact,
|
||||
devicePublicKey: devicePublicKey,
|
||||
);
|
||||
|
||||
final updatedContact =
|
||||
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
|
||||
if (_pendingDeliveredRouteRefreshContacts.remove(
|
||||
updatedContact.publicKeyHex,
|
||||
)) {
|
||||
messagesProvider.applyDeliveredMessageRouteFromContact(updatedContact);
|
||||
}
|
||||
};
|
||||
|
||||
// When all contacts are received
|
||||
@@ -1254,18 +1245,6 @@ class AppProvider with ChangeNotifier {
|
||||
enrichedMessage,
|
||||
);
|
||||
final receivedPathBytes = receptionDetailsSnapshot?.pathBytes;
|
||||
if (senderContact != null &&
|
||||
enrichedMessage.isChannelMessage &&
|
||||
receivedPathBytes != null &&
|
||||
receivedPathBytes.isNotEmpty) {
|
||||
unawaited(
|
||||
_learnPathFromPublicMessage(
|
||||
contact: senderContact,
|
||||
pathBytes: receivedPathBytes,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Estimate location for contacts without GPS using received path
|
||||
if (senderContact != null &&
|
||||
senderContact.displayLocation == null &&
|
||||
@@ -1685,6 +1664,7 @@ class AppProvider with ChangeNotifier {
|
||||
|
||||
// When a contact's routing path is updated in the mesh network
|
||||
connectionProvider.onPathUpdated = (publicKey) {
|
||||
_pendingDeliveredRouteRefreshContacts.add(_publicKeyHex(publicKey));
|
||||
debugPrint(
|
||||
'🔄 [AppProvider] Path updated for contact: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
|
||||
);
|
||||
@@ -1850,28 +1830,18 @@ class AppProvider with ChangeNotifier {
|
||||
.getManualSelectionForContact(latestContact);
|
||||
final selection =
|
||||
manualSelection ??
|
||||
await _pathHistoryService.getSelectionForContact(
|
||||
latestContact,
|
||||
autoRouteRotationEnabled: _autoRouteRotationEnabled,
|
||||
);
|
||||
await _pathHistoryService.getSelectionForContact(latestContact);
|
||||
session = _DirectMessageRouteSession(
|
||||
currentSelection: selection,
|
||||
originalRoute: ContactRouteCodec.fromContact(latestContact),
|
||||
usedManualOverride: manualSelection != null,
|
||||
routerFallbackAttempted: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (!session.routerFallbackAttempted) {
|
||||
final currentSignature = session.currentSelection.hasDirectPath
|
||||
? session.currentSelection.pathBytes
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join()
|
||||
: null;
|
||||
final selection = await _resolveDirectMessageSelectionForRetry(
|
||||
latestContact,
|
||||
retryAttempt: retryAttempt,
|
||||
currentSignature: currentSignature,
|
||||
fallbackSelection: session.currentSelection,
|
||||
);
|
||||
session = session.copyWith(currentSelection: selection);
|
||||
@@ -1891,26 +1861,17 @@ class AppProvider with ChangeNotifier {
|
||||
Future<PathSelection> _resolveDirectMessageSelectionForRetry(
|
||||
Contact contact, {
|
||||
required int retryAttempt,
|
||||
required String? currentSignature,
|
||||
required PathSelection fallbackSelection,
|
||||
}) async {
|
||||
if (retryAttempt == 2) {
|
||||
return PathSelection.flood();
|
||||
}
|
||||
|
||||
if (retryAttempt >= 3) {
|
||||
final historicalSelection = await _pathHistoryService
|
||||
.getLastSuccessfulDirectSelection(
|
||||
contact,
|
||||
excludeSignature: currentSignature,
|
||||
senderLatitude: locationTrackingService.currentPosition?.latitude,
|
||||
senderLongitude: locationTrackingService.currentPosition?.longitude,
|
||||
recipientLatitude: contact.displayLocation?.latitude,
|
||||
recipientLongitude: contact.displayLocation?.longitude,
|
||||
);
|
||||
if (historicalSelection != null) {
|
||||
return historicalSelection;
|
||||
}
|
||||
if (retryAttempt < 2 &&
|
||||
!fallbackSelection.hasDirectPath &&
|
||||
contact.routeHasPath &&
|
||||
contact.routeHopCount > 0) {
|
||||
return _pathHistoryService.getSelectionForContact(contact);
|
||||
}
|
||||
|
||||
return fallbackSelection;
|
||||
@@ -2020,32 +1981,19 @@ class AppProvider with ChangeNotifier {
|
||||
|
||||
final latestContact =
|
||||
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
|
||||
final manualSelection = await _pathHistoryService.getManualSelectionForContact(
|
||||
latestContact,
|
||||
);
|
||||
final session =
|
||||
_directMessageRouteSessions[messageId] ??
|
||||
_DirectMessageRouteSession(
|
||||
currentSelection:
|
||||
await _pathHistoryService.getManualSelectionForContact(
|
||||
latestContact,
|
||||
) ??
|
||||
await _pathHistoryService.getSelectionForContact(
|
||||
latestContact,
|
||||
autoRouteRotationEnabled: _autoRouteRotationEnabled,
|
||||
),
|
||||
manualSelection ??
|
||||
await _pathHistoryService.getSelectionForContact(latestContact),
|
||||
originalRoute: ContactRouteCodec.fromContact(latestContact),
|
||||
usedManualOverride:
|
||||
await _pathHistoryService.getManualSelectionForContact(
|
||||
latestContact,
|
||||
) !=
|
||||
null,
|
||||
routerFallbackAttempted: false,
|
||||
);
|
||||
|
||||
await _pathHistoryService.recordPathResult(
|
||||
latestContact.publicKeyHex,
|
||||
session.currentSelection,
|
||||
success: false,
|
||||
);
|
||||
|
||||
final repeater = _nearestRouterSelector.select(
|
||||
senderPosition: locationTrackingService.currentPosition,
|
||||
repeaters: contactsProvider.repeaters,
|
||||
@@ -2089,33 +2037,10 @@ class AppProvider with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(
|
||||
() async {
|
||||
await _pathHistoryService.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
session.currentSelection,
|
||||
success: true,
|
||||
roundTripTimeMs: roundTripTimeMs,
|
||||
senderLatitude: locationTrackingService.currentPosition?.latitude,
|
||||
senderLongitude: locationTrackingService.currentPosition?.longitude,
|
||||
recipientLatitude: contact.displayLocation?.latitude,
|
||||
recipientLongitude: contact.displayLocation?.longitude,
|
||||
);
|
||||
if (!session.usedManualOverride) {
|
||||
return;
|
||||
}
|
||||
if (session.currentSelection.mode == PathSelectionMode.directCurrent ||
|
||||
session.currentSelection.mode ==
|
||||
PathSelectionMode.directHistorical) {
|
||||
await _pathHistoryService.setManualSelectionFor(
|
||||
contact.publicKeyHex,
|
||||
session.currentSelection,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _pathHistoryService.clearManualRouteFor(contact.publicKeyHex);
|
||||
}(),
|
||||
);
|
||||
if (session.currentSelection.usesFlood ||
|
||||
session.currentSelection.mode == PathSelectionMode.nearestRouter) {
|
||||
messagesProvider.queueDeliveredMessageRouteRefresh(messageId, contact);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleDirectMessageFinalFailure({
|
||||
@@ -2126,16 +2051,6 @@ class AppProvider with ChangeNotifier {
|
||||
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
|
||||
final session = _directMessageRouteSessions.remove(messageId);
|
||||
if (session != null) {
|
||||
await _pathHistoryService.recordPathResult(
|
||||
latestContact.publicKeyHex,
|
||||
session.currentSelection,
|
||||
success: false,
|
||||
);
|
||||
if (session.usedManualOverride) {
|
||||
await _pathHistoryService.clearManualRouteFor(
|
||||
latestContact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
if (session.routerFallbackAttempted) {
|
||||
await _restoreRouteOnDevice(latestContact, session.originalRoute);
|
||||
}
|
||||
@@ -2157,6 +2072,12 @@ class AppProvider with ChangeNotifier {
|
||||
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
String _publicKeyHex(Uint8List publicKey) {
|
||||
return publicKey
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
}
|
||||
|
||||
/// Estimate contact location from the received message path.
|
||||
///
|
||||
/// When we receive a message, the path bytes describe how it traveled:
|
||||
@@ -2207,22 +2128,6 @@ class AppProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _learnPathFromPublicMessage({
|
||||
required Contact contact,
|
||||
required List<int> pathBytes,
|
||||
}) async {
|
||||
final preferred = await RouteHashPreferences.getHashSize();
|
||||
final inferredHashSize = _inferReceivedPathHashSize(
|
||||
pathBytes,
|
||||
preferredHashSize: preferred,
|
||||
);
|
||||
await _pathHistoryService.recordReceivedBytePath(
|
||||
contact.publicKeyHex,
|
||||
pathBytes,
|
||||
inferredHashSize,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _retainAdvertRxPath(Uint8List publicKey) async {
|
||||
final decoded = _findBestMatchingAdvertRxRoute(publicKey);
|
||||
if (decoded == null || decoded.pathBytes.isEmpty) {
|
||||
@@ -2248,11 +2153,6 @@ class AppProvider with ChangeNotifier {
|
||||
paddedPathBytes: parsedRoute.paddedPathBytes,
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
await _pathHistoryService.recordReceivedBytePath(
|
||||
publicKey.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(),
|
||||
decoded.pathBytes,
|
||||
decoded.hashSize,
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle pushAdvert (0x80) — matches the official MeshCore app flow:
|
||||
@@ -2593,22 +2493,6 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
int _inferReceivedPathHashSize(
|
||||
List<int> pathBytes, {
|
||||
required int preferredHashSize,
|
||||
}) {
|
||||
final preferred = preferredHashSize;
|
||||
final candidates = {preferred, 3, 2, 1}.toList();
|
||||
for (final candidate in candidates) {
|
||||
if (candidate >= 1 &&
|
||||
candidate <= 3 &&
|
||||
pathBytes.length % candidate == 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// Initialize the app (load contacts, sync time, etc.)
|
||||
Future<void> initialize() async {
|
||||
if (!connectionProvider.deviceInfo.isConnected) return;
|
||||
|
||||
@@ -24,6 +24,7 @@ typedef DisplayMessageEntry = ({Message message, int occurrenceCount});
|
||||
class MessagesProvider with ChangeNotifier {
|
||||
static const Duration _channelEchoWarningDelay = Duration(seconds: 12);
|
||||
static const Duration _receivedDuplicateWindow = Duration(seconds: 5);
|
||||
static const Duration _deliveredRouteRefreshWindow = Duration(seconds: 30);
|
||||
|
||||
final List<Message> _messages = [];
|
||||
final Map<String, SarMarker> _sarMarkers = {};
|
||||
@@ -38,6 +39,9 @@ class MessagesProvider with ChangeNotifier {
|
||||
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
|
||||
final Map<String, MessageTransferDetails> _messageTransferDetails = {};
|
||||
final Map<String, MessageRouteMetadata> _messageRouteMetadata = {};
|
||||
final Map<String, List<_PendingDeliveredRouteRefresh>>
|
||||
_pendingDeliveredRouteRefreshByContact =
|
||||
<String, List<_PendingDeliveredRouteRefresh>>{};
|
||||
String? _storageNamespace;
|
||||
|
||||
// Track pending sent messages by expected ACK/TAG
|
||||
@@ -199,6 +203,83 @@ class MessagesProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void queueDeliveredMessageRouteRefresh(String messageId, Contact contact) {
|
||||
if (messageId.isEmpty || contact.publicKeyHex.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_prunePendingDeliveredRouteRefresh();
|
||||
final queue = _pendingDeliveredRouteRefreshByContact.putIfAbsent(
|
||||
contact.publicKeyHex,
|
||||
() => <_PendingDeliveredRouteRefresh>[],
|
||||
);
|
||||
queue.removeWhere((entry) => entry.messageId == messageId);
|
||||
queue.add(
|
||||
_PendingDeliveredRouteRefresh(
|
||||
messageId: messageId,
|
||||
queuedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool applyDeliveredMessageRouteFromContact(Contact contact) {
|
||||
if (!contact.routeHasPath ||
|
||||
contact.routeHopCount <= 0 ||
|
||||
contact.publicKeyHex.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_prunePendingDeliveredRouteRefresh();
|
||||
final queue = _pendingDeliveredRouteRefreshByContact[contact.publicKeyHex];
|
||||
if (queue == null || queue.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (queue.isNotEmpty) {
|
||||
final pending = queue.removeAt(0);
|
||||
final index = _messages.indexWhere(
|
||||
(message) => message.id == pending.messageId,
|
||||
);
|
||||
if (index == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final message = _messages[index];
|
||||
if (!message.isContactMessage ||
|
||||
message.deliveryStatus != MessageDeliveryStatus.delivered) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final existingMetadata = _messageRouteMetadata[pending.messageId];
|
||||
_messageRouteMetadata[pending.messageId] = MessageRouteMetadata(
|
||||
mode: PathSelectionMode.directCurrent,
|
||||
routerFallbackAttempted:
|
||||
existingMetadata?.routerFallbackAttempted ?? false,
|
||||
canonicalPath: contact.routeCanonicalText.isEmpty
|
||||
? null
|
||||
: contact.routeCanonicalText,
|
||||
hopCount: contact.routeHopCount,
|
||||
);
|
||||
_messages[index] = message.copyWith(
|
||||
pathLen: contact.routeHopCount,
|
||||
usedFloodFallback: false,
|
||||
);
|
||||
|
||||
if (queue.isEmpty) {
|
||||
_pendingDeliveredRouteRefreshByContact.remove(contact.publicKeyHex);
|
||||
}
|
||||
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (queue.isEmpty) {
|
||||
_pendingDeliveredRouteRefreshByContact.remove(contact.publicKeyHex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Set localizations for notifications
|
||||
void setLocalizations(AppLocalizations localizations) {
|
||||
_localizations = localizations;
|
||||
@@ -2555,6 +2636,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
_pendingSentMessages.remove(message.expectedAckTag);
|
||||
}
|
||||
_clearAckHistoryForMessage(messageId);
|
||||
_removePendingDeliveredRouteRefresh(messageId);
|
||||
|
||||
// Clear retry tracking
|
||||
_retryManager.clearRetry(messageId);
|
||||
@@ -2595,6 +2677,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
_pendingSentMessages.remove(message.expectedAckTag);
|
||||
}
|
||||
_clearAckHistoryForMessage(messageId);
|
||||
_removePendingDeliveredRouteRefresh(messageId);
|
||||
_retryManager.clearRetry(messageId);
|
||||
_messageRouteMetadata.remove(messageId);
|
||||
onManualRetryPreparedCallback?.call(messageId);
|
||||
@@ -2767,4 +2850,51 @@ class MessagesProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _prunePendingDeliveredRouteRefresh() {
|
||||
if (_pendingDeliveredRouteRefreshByContact.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final cutoff = DateTime.now().subtract(_deliveredRouteRefreshWindow);
|
||||
final emptyKeys = <String>[];
|
||||
for (final entry in _pendingDeliveredRouteRefreshByContact.entries) {
|
||||
entry.value.removeWhere(
|
||||
(pending) => pending.queuedAt.isBefore(cutoff),
|
||||
);
|
||||
if (entry.value.isEmpty) {
|
||||
emptyKeys.add(entry.key);
|
||||
}
|
||||
}
|
||||
for (final key in emptyKeys) {
|
||||
_pendingDeliveredRouteRefreshByContact.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
void _removePendingDeliveredRouteRefresh(String messageId) {
|
||||
if (_pendingDeliveredRouteRefreshByContact.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final emptyKeys = <String>[];
|
||||
for (final entry in _pendingDeliveredRouteRefreshByContact.entries) {
|
||||
entry.value.removeWhere((pending) => pending.messageId == messageId);
|
||||
if (entry.value.isEmpty) {
|
||||
emptyKeys.add(entry.key);
|
||||
}
|
||||
}
|
||||
for (final key in emptyKeys) {
|
||||
_pendingDeliveredRouteRefreshByContact.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PendingDeliveredRouteRefresh {
|
||||
final String messageId;
|
||||
final DateTime queuedAt;
|
||||
|
||||
const _PendingDeliveredRouteRefresh({
|
||||
required this.messageId,
|
||||
required this.queuedAt,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user