fix: Use first hop of received path (near sender), not outbound path

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.
This commit is contained in:
Janez T
2026-03-17 11:19:37 +01:00
parent b48139964a
commit 251932bb47

View File

@@ -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 && if (senderContact != null &&
senderContact.displayLocation == null && senderContact.displayLocation == null &&
receptionDetailsSnapshot?.rssiDbm != null && receivedPathBytes != null &&
senderContact.routeHasPath && receivedPathBytes.isNotEmpty) {
senderContact.routeHopCount > 0) { _estimateContactLocationFromReceivedPath(
_estimateContactLocationFromRssi(
contact: senderContact, 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(); return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
} }
/// Record RSSI observation from last-hop repeater for trilateration. /// Estimate contact location from the received message path.
void _estimateContactLocationFromRssi({ ///
/// 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 Contact contact,
required int rssiDbm, required List<int> receivedPathBytes,
}) { }) {
final pathHopCount = contact.routeHopCount; if (receivedPathBytes.isEmpty) return;
final pathHashSize = contact.routeHashSize;
if (pathHopCount <= 0 || pathHashSize <= 0) return;
final pathBytes = contact.routePathBytes; // Infer hash size from the contact's known path encoding, or default to 1
if (pathBytes.length < pathHashSize) return; final hashSize = contact.routeHasPath ? contact.routeHashSize : 1;
final firstHopHash = pathBytes if (receivedPathBytes.length < hashSize) return;
.sublist(0, pathHashSize)
// First hop in received path = repeater nearest to sender
final firstHopHash = receivedPathBytes
.sublist(0, hashSize)
.map((b) => b.toRadixString(16).padLeft(2, '0')) .map((b) => b.toRadixString(16).padLeft(2, '0'))
.join() .join()
.toLowerCase(); .toLowerCase();
Contact? lastHopRepeater; Contact? nearSenderRepeater;
for (final c in contactsProvider.contacts) { for (final c in contactsProvider.contacts) {
if (c.publicKeyHex.toLowerCase().startsWith(firstHopHash) && if (c.publicKeyHex.toLowerCase().startsWith(firstHopHash) &&
c.displayLocation != null) { c.displayLocation != null) {
lastHopRepeater = c; nearSenderRepeater = c;
break; 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( contactsProvider.addRssiObservation(
contactPublicKeyHex: contact.publicKeyHex, contactPublicKeyHex: contact.publicKeyHex,
contactPublicKey: contact.publicKey, contactPublicKey: contact.publicKey,
observation: RssiObservation( observation: RssiObservation(
repeaterLocation: lastHopRepeater.displayLocation!, repeaterLocation: nearSenderRepeater.displayLocation!,
rssiDbm: rssiDbm, rssiDbm: estimatedRssi,
observedAt: DateTime.now(), observedAt: DateTime.now(),
), ),
); );