diff --git a/lib/providers/contacts_provider.dart b/lib/providers/contacts_provider.dart index 710f1ee..ce52b79 100644 --- a/lib/providers/contacts_provider.dart +++ b/lib/providers/contacts_provider.dart @@ -335,12 +335,9 @@ class ContactsProvider with ChangeNotifier { debugPrint(' ✅ Parsed new telemetry'); debugPrint(' New telemetry timestamp: ${telemetry.timestamp}'); - // Ignore placeholder GPS coordinates (0,0) so we don't overwrite a - // previously known/saved position with invalid telemetry data. - if (_isInvalidTelemetryGps(telemetry.gpsLocation)) { - debugPrint( - ' ⚠️ Ignoring invalid telemetry GPS coordinates: ${telemetry.gpsLocation}', - ); + final incomingGps = telemetry.gpsLocation; + if (_isInvalidTelemetryGps(incomingGps)) { + // Always sanitize invalid placeholder GPS to null first. telemetry = ContactTelemetry( gpsLocation: null, batteryPercentage: telemetry.batteryPercentage, @@ -353,6 +350,25 @@ class ContactsProvider with ChangeNotifier { ); } + // Keep last valid GPS for router/chat/room contacts when current + // telemetry does not provide a valid GPS fix. + if (_shouldRetainLastValidGps(contact, telemetry.gpsLocation)) { + debugPrint( + ' ⚠️ Retaining last valid GPS. Incoming telemetry GPS is invalid/missing: $incomingGps', + ); + final previousGps = _getValidGpsOrNull(contact.telemetry?.gpsLocation); + telemetry = ContactTelemetry( + gpsLocation: previousGps, + batteryPercentage: telemetry.batteryPercentage, + batteryMilliVolts: telemetry.batteryMilliVolts, + temperature: telemetry.temperature, + timestamp: telemetry.timestamp, + humidity: telemetry.humidity, + pressure: telemetry.pressure, + extraSensorData: telemetry.extraSensorData, + ); + } + // Update contact with new telemetry AND last seen time // lastAdvert is Unix timestamp in seconds final currentTimestamp = (DateTime.now().millisecondsSinceEpoch / 1000) @@ -390,6 +406,29 @@ class ContactsProvider with ChangeNotifier { return lat.abs() < epsilon && lon.abs() < epsilon; } + LatLng? _getValidGpsOrNull(LatLng? location) { + if (location == null || _isInvalidTelemetryGps(location)) { + return null; + } + return location; + } + + bool _shouldRetainLastValidGps(Contact contact, LatLng? incomingGps) { + final isSupportedType = + contact.isChat || contact.isRepeater || contact.isRoom; + if (!isSupportedType) { + return false; + } + + final hasPreviousValidGps = + _getValidGpsOrNull(contact.telemetry?.gpsLocation) != null; + if (!hasPreviousValidGps) { + return false; + } + + return incomingGps == null; + } + /// Find contact by public key prefix (6 bytes) Contact? _findContactByPrefix(Uint8List prefix) { if (prefix.length < 6) return null; diff --git a/test/providers/contacts_provider_test.dart b/test/providers/contacts_provider_test.dart index 0253e68..0fa2e92 100644 --- a/test/providers/contacts_provider_test.dart +++ b/test/providers/contacts_provider_test.dart @@ -9,6 +9,29 @@ import 'package:shared_preferences/shared_preferences.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); + Uint8List createPublicKey(int seed) { + return Uint8List.fromList(List.generate(32, (index) => seed + index)); + } + + Contact createContact({ + required Uint8List key, + required ContactType type, + String? name, + }) { + return Contact( + publicKey: key, + type: type, + flags: 0, + outPathLen: 0, + outPath: Uint8List(64), + advName: name ?? 'Test Contact', + lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000, + advLat: (46.0569 * 1e6).toInt(), + advLon: (14.5058 * 1e6).toInt(), + lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000, + ); + } + group('ContactsProvider.updateTelemetry', () { late ContactsProvider provider; late Uint8List publicKey; @@ -106,5 +129,70 @@ void main() { expect(updated.displayLocation!.latitude, closeTo(45.0001, 0.0001)); expect(updated.displayLocation!.longitude, closeTo(13.9999, 0.0001)); }); + + test( + 'retains last valid gps for chat/repeater/room when telemetry gps is invalid or missing', + () { + final contactTypes = [ + ContactType.chat, + ContactType.repeater, + ContactType.room, + ]; + + for (var i = 0; i < contactTypes.length; i++) { + final scopedProvider = ContactsProvider(); + final scopedKey = createPublicKey(16 + i); + final contactType = contactTypes[i]; + scopedProvider.addOrUpdateContact( + createContact( + key: scopedKey, + type: contactType, + name: 'Contact ${contactType.name}', + ), + ); + + final validGps = CayenneLppParser.createGpsData( + latitude: 45.1234, + longitude: 13.8765, + ); + scopedProvider.updateTelemetry(scopedKey.sublist(0, 6), validGps); + + // No GPS frame should keep previous valid GPS. + final batteryOnly = CayenneLppParser.createBatteryData(3.8); + scopedProvider.updateTelemetry(scopedKey.sublist(0, 6), batteryOnly); + + var updated = scopedProvider.findContactByKey(scopedKey)!; + expect(updated.telemetry, isNotNull); + expect(updated.telemetry!.gpsLocation, isNotNull); + expect( + updated.telemetry!.gpsLocation!.latitude, + closeTo(45.1234, 0.0001), + ); + expect( + updated.telemetry!.gpsLocation!.longitude, + closeTo(13.8765, 0.0001), + ); + + // Invalid 0,0 GPS frame should also keep previous valid GPS. + final invalidGps = CayenneLppParser.createGpsData( + latitude: 0.0, + longitude: 0.0, + ); + scopedProvider.updateTelemetry(scopedKey.sublist(0, 6), invalidGps); + + updated = scopedProvider.findContactByKey(scopedKey)!; + expect(updated.telemetry, isNotNull); + expect(updated.telemetry!.gpsLocation, isNotNull); + expect( + updated.telemetry!.gpsLocation!.latitude, + closeTo(45.1234, 0.0001), + ); + expect( + updated.telemetry!.gpsLocation!.longitude, + closeTo(13.8765, 0.0001), + ); + } + }, + ); }); }