mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: RSSI trilateration from multiple repeaters
When a contact has no GPS, estimate their position from RSSI observations through multiple repeaters: - 1 repeater: offset by RSSI distance in deterministic direction - 2 repeaters: weighted midpoint between circle intersections - 3+ repeaters: weighted centroid with inverse-square weighting (closer repeater observations dominate) Observations stored per-repeater (latest wins), max 8 per contact, expire after 30 minutes. Each incoming message updates the estimate.
This commit is contained in:
@@ -2016,17 +2016,15 @@ class AppProvider with ChangeNotifier {
|
||||
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
/// Estimate a contact's location from the last-hop repeater + RSSI distance.
|
||||
/// Record RSSI observation from last-hop repeater for trilateration.
|
||||
void _estimateContactLocationFromRssi({
|
||||
required Contact contact,
|
||||
required int rssiDbm,
|
||||
}) {
|
||||
// Find the first-hop repeater from the contact's path
|
||||
final pathHopCount = contact.routeHopCount;
|
||||
final pathHashSize = contact.routeHashSize;
|
||||
if (pathHopCount <= 0 || pathHashSize <= 0) return;
|
||||
|
||||
// Get first hop hash from path
|
||||
final pathBytes = contact.routePathBytes;
|
||||
if (pathBytes.length < pathHashSize) return;
|
||||
final firstHopHash = pathBytes
|
||||
@@ -2035,7 +2033,6 @@ class AppProvider with ChangeNotifier {
|
||||
.join()
|
||||
.toLowerCase();
|
||||
|
||||
// Find a repeater whose public key starts with this hash
|
||||
Contact? lastHopRepeater;
|
||||
for (final c in contactsProvider.contacts) {
|
||||
if (c.publicKeyHex.toLowerCase().startsWith(firstHopHash) &&
|
||||
@@ -2046,14 +2043,16 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
if (lastHopRepeater == null) return;
|
||||
|
||||
final estimated = RssiLocationEstimator.estimateFromRepeater(
|
||||
repeaterLocation: lastHopRepeater.displayLocation!,
|
||||
rssiDbm: rssiDbm,
|
||||
// Record observation for trilateration
|
||||
contactsProvider.addRssiObservation(
|
||||
contactPublicKeyHex: contact.publicKeyHex,
|
||||
contactPublicKey: contact.publicKey,
|
||||
observation: RssiObservation(
|
||||
repeaterLocation: lastHopRepeater.displayLocation!,
|
||||
rssiDbm: rssiDbm,
|
||||
observedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
if (estimated != null) {
|
||||
contactsProvider.setEstimatedLocation(contact.publicKeyHex, estimated);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _learnPathFromPublicMessage({
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../models/message_contact_location.dart';
|
||||
import '../services/cayenne_lpp_parser.dart';
|
||||
import '../services/contact_storage_service.dart';
|
||||
import '../utils/fast_gps_packet.dart';
|
||||
import '../utils/rssi_location_estimator.dart';
|
||||
import '../utils/key_comparison.dart';
|
||||
|
||||
class PendingAdvert {
|
||||
@@ -129,6 +130,7 @@ class ContactsProvider with ChangeNotifier {
|
||||
final List<SavedContactGroup> _savedContactGroups = <SavedContactGroup>[];
|
||||
final Map<String, PendingAdvert> _pendingAdverts = {};
|
||||
final Map<String, LatLng> _estimatedLocations = {};
|
||||
final Map<String, List<RssiObservation>> _rssiObservations = {};
|
||||
final ContactStorageService _storageService = ContactStorageService();
|
||||
bool _isInitialized = false;
|
||||
bool _isPersisting = false;
|
||||
@@ -516,6 +518,44 @@ class ContactsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Record an RSSI observation and re-trilaterate the contact's position.
|
||||
///
|
||||
/// Keeps up to 5 observations per unique repeater (latest wins).
|
||||
/// With multiple repeaters, trilateration produces a better estimate.
|
||||
void addRssiObservation({
|
||||
required String contactPublicKeyHex,
|
||||
required List<int> contactPublicKey,
|
||||
required RssiObservation observation,
|
||||
}) {
|
||||
final observations = _rssiObservations.putIfAbsent(
|
||||
contactPublicKeyHex,
|
||||
() => [],
|
||||
);
|
||||
|
||||
// Replace existing observation from the same repeater, or add new
|
||||
final repeaterKey =
|
||||
'${observation.repeaterLocation.latitude},${observation.repeaterLocation.longitude}';
|
||||
observations.removeWhere((o) =>
|
||||
'${o.repeaterLocation.latitude},${o.repeaterLocation.longitude}' ==
|
||||
repeaterKey);
|
||||
observations.add(observation);
|
||||
|
||||
// Keep at most 8 observations (most recent per repeater)
|
||||
if (observations.length > 8) {
|
||||
observations.sort((a, b) => b.observedAt.compareTo(a.observedAt));
|
||||
observations.removeRange(8, observations.length);
|
||||
}
|
||||
|
||||
final estimated = RssiLocationEstimator.trilaterate(
|
||||
observations: observations,
|
||||
contactPublicKey: contactPublicKey,
|
||||
);
|
||||
if (estimated != null) {
|
||||
_estimatedLocations[contactPublicKeyHex] = estimated;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Effective location: own GPS/advert > estimated from RSSI.
|
||||
LatLng? effectiveLocationFor(Contact contact) {
|
||||
return contact.displayLocation ?? _estimatedLocations[contact.publicKeyHex];
|
||||
|
||||
Reference in New Issue
Block a user