diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 53f4d65a..c16ff1d2 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2207,6 +2207,7 @@ class MeshCoreConnector extends ChangeNotifier { return; } _bleInitialSyncStarted = true; + _pendingInitialContactsSync = true; await _requestDeviceInfo(); _startBatteryPolling(); @@ -4184,7 +4185,9 @@ class MeshCoreConnector extends ChangeNotifier { if (_contacts.isEmpty) return 0; var latest = 0; for (final contact in _contacts) { - final seconds = contact.lastSeen.millisecondsSinceEpoch ~/ 1000; + // prefer lastmod per spec, fallback to lastseen + final source = contact.lastModified ?? contact.lastSeen; + final seconds = source.millisecondsSinceEpoch ~/ 1000; if (seconds > latest) { latest = seconds; } diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 02e75db3..c212acc3 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -735,10 +735,15 @@ Uint8List buildUpdateContactPathFrame( writer.writeInt32LE((longitude * 1e6).round()); } - if (lastModified != null) { - // Last modified - final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000; - writer.writeUInt32LE(lastModifiedTimestamp); + final hasLocation = lat != null && lon != null; + if (hasLocation || lastModified != null) { + writer.writeInt32LE(hasLocation ? (lat * 1e6).round() : 0); + writer.writeInt32LE(hasLocation ? (lon * 1e6).round() : 0); + if (lastModified != null) { + // Last modified + final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000; + writer.writeUInt32LE(lastModifiedTimestamp); + } } return writer.toBytes(); diff --git a/lib/models/contact.dart b/lib/models/contact.dart index b5df2af1..01d10a9c 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -17,6 +17,7 @@ class Contact { final double? longitude; final DateTime lastSeen; final DateTime lastMessageAt; + final DateTime? lastModified; final bool isActive; final bool wasPulled; final Uint8List? rawPacket; @@ -33,6 +34,7 @@ class Contact { this.latitude, this.longitude, required this.lastSeen, + this.lastModified, DateTime? lastMessageAt, this.isActive = true, this.wasPulled = false, @@ -86,6 +88,7 @@ class Contact { double? longitude, DateTime? lastSeen, DateTime? lastMessageAt, + DateTime? lastModified, bool? isActive, Uint8List? rawPacket, }) { @@ -106,6 +109,7 @@ class Contact { longitude: longitude ?? this.longitude, lastSeen: lastSeen ?? this.lastSeen, lastMessageAt: lastMessageAt ?? this.lastMessageAt, + lastModified: lastModified ?? this.lastModified, isActive: isActive ?? this.isActive, rawPacket: rawPacket ?? this.rawPacket, ); @@ -174,16 +178,34 @@ class Contact { return null; } - final lastMod = reader.readUInt32LE(); + // mandatory last_advert_timestamp + final lastAdvertTimestamp = reader.readUInt32LE(); double? lat, lon; - if (reader.remaining >= 8) { + DateTime? lastModified; + if (reader.remaining >= 12) { + final latRaw = reader.readInt32LE(); + final lonRaw = reader.readInt32LE(); + final lastModRaw = reader.readUInt32LE(); + // TODO: should this be &&? + if (latRaw != 0 || lonRaw != 0) { + lat = latRaw / 1e6; + lon = lonRaw / 1e6; + } + if (lastModRaw != 0) { + lastModified = DateTime.fromMillisecondsSinceEpoch(lastModRaw * 1000); + } + } else if (reader.remaining >= 8) { + // Old layout: gps without lastmod final latRaw = reader.readInt32LE(); final lonRaw = reader.readInt32LE(); if (latRaw != 0 || lonRaw != 0) { lat = latRaw / 1e6; lon = lonRaw / 1e6; } + appLogger.info( + 'Contact ${pubKeyToHex(pubKey).substring(0, 8)} has gps but no lastmod (legacy firmware layout)', + ); } return Contact( @@ -195,7 +217,10 @@ class Contact { path: pathBytes, latitude: lat, longitude: lon, - lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000), + lastSeen: DateTime.fromMillisecondsSinceEpoch( + lastAdvertTimestamp * 1000, + ), + lastModified: lastModified, isActive: true, rawPacket: null, ); diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart index 89ca0273..3f6f1718 100644 --- a/lib/storage/contact_discovery_store.dart +++ b/lib/storage/contact_discovery_store.dart @@ -43,6 +43,7 @@ class ContactDiscoveryStore { 'latitude': contact.latitude, 'longitude': contact.longitude, 'lastSeen': contact.lastSeen.millisecondsSinceEpoch, + 'lastModified': contact.lastModified?.millisecondsSinceEpoch, 'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch, 'rawPacket': contact.rawPacket != null ? base64Encode(contact.rawPacket!) @@ -53,6 +54,7 @@ class ContactDiscoveryStore { Contact _fromJson(Map json) { final lastSeenMs = json['lastSeen'] as int? ?? 0; final lastMessageMs = json['lastMessageAt'] as int?; + final lastModifiedMs = json['lastModified'] as int?; return Contact( publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), name: json['name'] as String? ?? 'Unknown', @@ -71,6 +73,9 @@ class ContactDiscoveryStore { latitude: (json['latitude'] as num?)?.toDouble(), longitude: (json['longitude'] as num?)?.toDouble(), lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs), + lastModified: lastModifiedMs == null + ? null + : DateTime.fromMillisecondsSinceEpoch(lastModifiedMs), lastMessageAt: DateTime.fromMillisecondsSinceEpoch( lastMessageMs ?? lastSeenMs, ), diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 0e2e3ad1..7883d885 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -75,6 +75,7 @@ class ContactStore { 'latitude': contact.latitude, 'longitude': contact.longitude, 'lastSeen': contact.lastSeen.millisecondsSinceEpoch, + 'lastModified': contact.lastModified?.millisecondsSinceEpoch, 'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch, 'isActive': contact.isActive, 'rawPacket': contact.rawPacket != null @@ -86,6 +87,7 @@ class ContactStore { Contact _fromJson(Map json) { final lastSeenMs = json['lastSeen'] as int? ?? 0; final lastMessageMs = json['lastMessageAt'] as int?; + final lastModifiedMs = json['lastModified'] as int?; return Contact( publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), name: json['name'] as String? ?? 'Unknown', @@ -104,6 +106,9 @@ class ContactStore { latitude: (json['latitude'] as num?)?.toDouble(), longitude: (json['longitude'] as num?)?.toDouble(), lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs), + lastModified: lastModifiedMs == null + ? null + : DateTime.fromMillisecondsSinceEpoch(lastModifiedMs), lastMessageAt: DateTime.fromMillisecondsSinceEpoch( lastMessageMs ?? lastSeenMs, ),