From 251932bb47c9780ff9c9f382a1eec9bc73a2e5ed Mon Sep 17 00:00:00 2001 From: Janez T Date: Tue, 17 Mar 2026 11:19:37 +0100 Subject: [PATCH] fix: Use first hop of received path (near sender), not outbound path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Received message path: Sender → hop1 (near sender) → ... → Us Outbound path: Us → hop1 (near us) → ... → Contact Was using outbound path which placed contacts near OUR repeater. Now uses first hop of the received message path — the repeater nearest to the sender. Only triggers on received messages, not sends. --- lib/providers/app_provider.dart | 54 +++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 0daae79..6e536b6 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -1176,15 +1176,14 @@ class AppProvider with ChangeNotifier { ); } - // Estimate location for contacts without GPS using RSSI + last-hop repeater + // Estimate location for contacts without GPS using received path if (senderContact != null && senderContact.displayLocation == null && - receptionDetailsSnapshot?.rssiDbm != null && - senderContact.routeHasPath && - senderContact.routeHopCount > 0) { - _estimateContactLocationFromRssi( + receivedPathBytes != null && + receivedPathBytes.isNotEmpty) { + _estimateContactLocationFromReceivedPath( contact: senderContact, - rssiDbm: receptionDetailsSnapshot!.rssiDbm!, + receivedPathBytes: receivedPathBytes, ); } @@ -2016,40 +2015,51 @@ class AppProvider with ChangeNotifier { return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); } - /// Record RSSI observation from last-hop repeater for trilateration. - void _estimateContactLocationFromRssi({ + /// Estimate contact location from the received message path. + /// + /// When we receive a message, the path bytes describe how it traveled: + /// Sender → first_hop (near sender) → ... → last_hop (near us) → Us + /// + /// The FIRST hop in the received path is the repeater nearest to the sender. + /// We don't have the actual RSSI between sender and that repeater, so we + /// use a conservative estimate to place them within typical LoRa range. + void _estimateContactLocationFromReceivedPath({ required Contact contact, - required int rssiDbm, + required List receivedPathBytes, }) { - final pathHopCount = contact.routeHopCount; - final pathHashSize = contact.routeHashSize; - if (pathHopCount <= 0 || pathHashSize <= 0) return; + if (receivedPathBytes.isEmpty) return; - final pathBytes = contact.routePathBytes; - if (pathBytes.length < pathHashSize) return; - final firstHopHash = pathBytes - .sublist(0, pathHashSize) + // Infer hash size from the contact's known path encoding, or default to 1 + final hashSize = contact.routeHasPath ? contact.routeHashSize : 1; + if (receivedPathBytes.length < hashSize) return; + + // First hop in received path = repeater nearest to sender + final firstHopHash = receivedPathBytes + .sublist(0, hashSize) .map((b) => b.toRadixString(16).padLeft(2, '0')) .join() .toLowerCase(); - Contact? lastHopRepeater; + Contact? nearSenderRepeater; for (final c in contactsProvider.contacts) { if (c.publicKeyHex.toLowerCase().startsWith(firstHopHash) && c.displayLocation != null) { - lastHopRepeater = c; + nearSenderRepeater = c; break; } } - if (lastHopRepeater == null) return; + if (nearSenderRepeater == null) return; + + // We don't have the RSSI between sender and their repeater. + // Use -70 dBm as conservative estimate (~300m in open terrain). + const int estimatedRssi = -70; - // Record observation for trilateration contactsProvider.addRssiObservation( contactPublicKeyHex: contact.publicKeyHex, contactPublicKey: contact.publicKey, observation: RssiObservation( - repeaterLocation: lastHopRepeater.displayLocation!, - rssiDbm: rssiDbm, + repeaterLocation: nearSenderRepeater.displayLocation!, + rssiDbm: estimatedRssi, observedAt: DateTime.now(), ), );