From e31bfac2ac17fe521d56ccb7856cbebafcb03669 Mon Sep 17 00:00:00 2001 From: Janez T Date: Wed, 11 Mar 2026 20:21:17 +0100 Subject: [PATCH] Retain rx path for adverts --- ios/fastlane/report.xml | 8 +- lib/providers/app_provider.dart | 56 +++++++++- lib/providers/contacts_provider.dart | 115 ++++++++++++++++++++- test/providers/contacts_provider_test.dart | 51 +++++++++ 4 files changed, 222 insertions(+), 8 deletions(-) diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index f33b0e2..098041f 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,22 +5,22 @@ - + - + - + - + diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index db7ae9b..69c0a73 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -775,7 +775,6 @@ class AppProvider with ChangeNotifier { final receivedPathBytes = receptionDetailsSnapshot?.pathBytes; if (senderContact != null && enrichedMessage.isChannelMessage && - (enrichedMessage.channelIdx ?? 0) == 0 && receivedPathBytes != null && receivedPathBytes.isNotEmpty) { unawaited( @@ -1180,6 +1179,7 @@ class AppProvider with ChangeNotifier { debugPrint( '📡 [AppProvider] Advertisement received: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...', ); + unawaited(_retainAdvertRxPath(publicKey)); // Check if this is an existing contact that might have updated location final contact = contactsProvider.findContactByKey(publicKey); if (contact != null) { @@ -1583,6 +1583,29 @@ class AppProvider with ChangeNotifier { ); } + Future _retainAdvertRxPath(Uint8List publicKey) async { + final decoded = _findBestMatchingAdvertRxRoute(publicKey); + if (decoded == null || decoded.pathBytes.isEmpty) { + return; + } + + final parsedRoute = ContactRouteCodec.parse( + decoded.hopHashes.join(','), + expectedHashSize: decoded.hashSize, + ); + contactsProvider.retainReceivedRoute( + publicKey, + signedEncodedPathLen: parsedRoute.signedEncodedPathLen, + paddedPathBytes: parsedRoute.paddedPathBytes, + devicePublicKey: connectionProvider.deviceInfo.publicKey, + ); + await _pathHistoryService.recordReceivedBytePath( + publicKey.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(), + decoded.pathBytes, + decoded.hashSize, + ); + } + int _inferReceivedPathHashSize( List pathBytes, { required int preferredHashSize, @@ -2629,6 +2652,37 @@ class AppProvider with ChangeNotifier { return bestLog; } + DecodedLogRxRoute? _findBestMatchingAdvertRxRoute(Uint8List publicKey) { + final publicKeyHex = publicKey + .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + .join() + .toLowerCase(); + + DecodedLogRxRoute? bestRoute; + var bestDeltaMs = 999999999; + final now = DateTime.now(); + + for (final log in connectionProvider.bleService.packetLogs) { + if (log.responseCode != 0x88) continue; + if (log.rawData.length < 6) continue; + + final decoded = LogRxRouteDecoder.decode(log.rawData); + if (decoded == null) continue; + if (decoded.payloadType != 0x04) continue; + final senderHash = decoded.originalSenderHashHex; + if (senderHash == null || !publicKeyHex.startsWith(senderHash)) continue; + + final deltaMs = (log.timestamp.difference(now).inMilliseconds).abs(); + if (deltaMs < bestDeltaMs) { + bestDeltaMs = deltaMs; + bestRoute = decoded; + } + } + + if (bestDeltaMs > 30000) return null; + return bestRoute; + } + List? _extractPathBytesFromLog(BlePacketLog? log) { if (log == null) return null; final decoded = LogRxRouteDecoder.decode(log.rawData); diff --git a/lib/providers/contacts_provider.dart b/lib/providers/contacts_provider.dart index 5f9c366..53f56db 100644 --- a/lib/providers/contacts_provider.dart +++ b/lib/providers/contacts_provider.dart @@ -10,8 +10,15 @@ import '../utils/key_comparison.dart'; class PendingAdvert { final Uint8List publicKey; final DateTime receivedAt; + final int? signedEncodedPathLen; + final Uint8List? paddedPathBytes; - const PendingAdvert({required this.publicKey, required this.receivedAt}); + const PendingAdvert({ + required this.publicKey, + required this.receivedAt, + this.signedEncodedPathLen, + this.paddedPathBytes, + }); String get publicKeyHex => publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join(''); @@ -21,14 +28,31 @@ class PendingAdvert { return prefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':'); } - PendingAdvert copyWith({Uint8List? publicKey, DateTime? receivedAt}) { + PendingAdvert copyWith({ + Uint8List? publicKey, + DateTime? receivedAt, + int? signedEncodedPathLen, + Uint8List? paddedPathBytes, + }) { return PendingAdvert( publicKey: publicKey ?? this.publicKey, receivedAt: receivedAt ?? this.receivedAt, + signedEncodedPathLen: signedEncodedPathLen ?? this.signedEncodedPathLen, + paddedPathBytes: paddedPathBytes ?? this.paddedPathBytes, ); } } +class _RetainedRoute { + final int signedEncodedPathLen; + final Uint8List paddedPathBytes; + + const _RetainedRoute({ + required this.signedEncodedPathLen, + required this.paddedPathBytes, + }); +} + /// Contacts Provider - manages contact list and telemetry class ContactsProvider with ChangeNotifier { final Map _contacts = {}; @@ -338,8 +362,19 @@ class ContactsProvider with ChangeNotifier { required Contact incomingContact, Contact? existingContact, }) { + final retainedRoute = _retainedRouteForContact( + keyHex: incomingContact.publicKeyHex, + incomingContact: incomingContact, + existingContact: existingContact, + ); + if (existingContact == null) { - var newContact = incomingContact.copyWith(isNew: true); + var newContact = incomingContact.copyWith( + isNew: true, + outPathLen: + retainedRoute?.signedEncodedPathLen ?? incomingContact.outPathLen, + outPath: retainedRoute?.paddedPathBytes ?? incomingContact.outPath, + ); if (incomingContact.advertLocation != null) { final timestamp = DateTime.fromMillisecondsSinceEpoch( incomingContact.lastAdvert * 1000, @@ -363,6 +398,9 @@ class ContactsProvider with ChangeNotifier { isNew: existingContact.isNew, advertHistory: existingContact.advertHistory, telemetry: mergedTelemetry, + outPathLen: + retainedRoute?.signedEncodedPathLen ?? incomingContact.outPathLen, + outPath: retainedRoute?.paddedPathBytes ?? incomingContact.outPath, advLat: incomingAdvertLocation != null ? incomingContact.advLat : existingAdvertLocation != null @@ -388,6 +426,37 @@ class ContactsProvider with ChangeNotifier { return updatedContact; } + _RetainedRoute? _retainedRouteForContact({ + required String keyHex, + required Contact incomingContact, + required Contact? existingContact, + }) { + if (incomingContact.routeHasPath) { + return null; + } + + final pendingAdvert = _pendingAdverts[keyHex]; + final pendingPathBytes = pendingAdvert?.paddedPathBytes; + final pendingPathLen = pendingAdvert?.signedEncodedPathLen; + if (pendingPathLen != null && + pendingPathBytes != null && + pendingPathBytes.isNotEmpty) { + return _RetainedRoute( + signedEncodedPathLen: pendingPathLen, + paddedPathBytes: Uint8List.fromList(pendingPathBytes), + ); + } + + if (existingContact != null && existingContact.routeHasPath) { + return _RetainedRoute( + signedEncodedPathLen: existingContact.outPathLen, + paddedPathBytes: Uint8List.fromList(existingContact.outPath), + ); + } + + return null; + } + /// Update contact telemetry void updateTelemetry(Uint8List publicKeyPrefix, Uint8List lppData) { debugPrint('📊 [ContactsProvider] updateTelemetry() called'); @@ -660,6 +729,46 @@ class ContactsProvider with ChangeNotifier { notifyListeners(); } + void retainReceivedRoute( + Uint8List publicKey, { + required int signedEncodedPathLen, + required Uint8List paddedPathBytes, + Uint8List? devicePublicKey, + }) { + if (devicePublicKey != null && publicKey.matches(devicePublicKey)) { + return; + } + + final keyHex = publicKey + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(''); + final contact = _contacts[keyHex]; + if (contact != null) { + _contacts[keyHex] = contact.copyWith( + outPathLen: signedEncodedPathLen, + outPath: Uint8List.fromList(paddedPathBytes), + ); + _persistContacts(); + notifyListeners(); + return; + } + + final now = DateTime.now(); + final existing = _pendingAdverts[keyHex]; + _pendingAdverts[keyHex] = + (existing ?? + PendingAdvert( + publicKey: Uint8List.fromList(publicKey), + receivedAt: now, + )) + .copyWith( + receivedAt: now, + signedEncodedPathLen: signedEncodedPathLen, + paddedPathBytes: Uint8List.fromList(paddedPathBytes), + ); + notifyListeners(); + } + void resetContactRouteLocal(Uint8List publicKey) { final contact = findContactByKey(publicKey); if (contact == null) { diff --git a/test/providers/contacts_provider_test.dart b/test/providers/contacts_provider_test.dart index 08e6f53..b62e814 100644 --- a/test/providers/contacts_provider_test.dart +++ b/test/providers/contacts_provider_test.dart @@ -471,6 +471,57 @@ void main() { expect(updated.routeHasPath, isFalse); expect(updated.routeSummary, 'Flood/Unknown'); }); + + test('retains existing route when contact refresh omits path', () { + final retainedRoute = ContactRouteCodec.parse('AABB,CCDD'); + provider.setContactRouteLocal( + publicKey, + signedEncodedPathLen: retainedRoute.signedEncodedPathLen, + paddedPathBytes: retainedRoute.paddedPathBytes, + ); + + provider.addOrUpdateContact( + createContact( + key: publicKey, + type: ContactType.chat, + name: 'Routey', + ).copyWith(outPathLen: -1, outPath: Uint8List(0)), + ); + + final updated = provider.findContactByKey(publicKey)!; + expect(updated.routeHasPath, isTrue); + expect(updated.routeCanonicalText, 'AABB,CCDD'); + }); + + test('applies retained pending advert route when contact is resolved', () { + final pendingKey = createPublicKey(96); + final retainedRoute = ContactRouteCodec.parse('1122,3344'); + + provider.retainReceivedRoute( + pendingKey, + signedEncodedPathLen: retainedRoute.signedEncodedPathLen, + paddedPathBytes: retainedRoute.paddedPathBytes, + ); + provider.addPendingAdvert(pendingKey); + + provider.addOrUpdateContact( + createContact( + key: pendingKey, + type: ContactType.chat, + name: 'Pending Routey', + ).copyWith(outPathLen: -1, outPath: Uint8List(0)), + ); + + final updated = provider.findContactByKey(pendingKey)!; + expect(updated.routeHasPath, isTrue); + expect(updated.routeCanonicalText, '1122,3344'); + expect( + provider.pendingAdverts.where( + (advert) => advert.publicKeyHex == updated.publicKeyHex, + ), + isEmpty, + ); + }); }); group('ContactsProvider.updateFastGps', () {