mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Retain rx path for adverts
This commit is contained in:
@@ -5,22 +5,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000256">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00029">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.924905">
|
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.339599">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="113.366384">
|
<testcase classname="fastlane.lanes" name="2: build_app" time="116.294416">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="755.542625">
|
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="441.763052">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -775,7 +775,6 @@ class AppProvider with ChangeNotifier {
|
|||||||
final receivedPathBytes = receptionDetailsSnapshot?.pathBytes;
|
final receivedPathBytes = receptionDetailsSnapshot?.pathBytes;
|
||||||
if (senderContact != null &&
|
if (senderContact != null &&
|
||||||
enrichedMessage.isChannelMessage &&
|
enrichedMessage.isChannelMessage &&
|
||||||
(enrichedMessage.channelIdx ?? 0) == 0 &&
|
|
||||||
receivedPathBytes != null &&
|
receivedPathBytes != null &&
|
||||||
receivedPathBytes.isNotEmpty) {
|
receivedPathBytes.isNotEmpty) {
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -1180,6 +1179,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'📡 [AppProvider] Advertisement received: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
|
'📡 [AppProvider] Advertisement received: ${publicKey.sublist(0, 6).map((b) => b.toRadixString(16).padLeft(2, '0')).join(':')}...',
|
||||||
);
|
);
|
||||||
|
unawaited(_retainAdvertRxPath(publicKey));
|
||||||
// Check if this is an existing contact that might have updated location
|
// Check if this is an existing contact that might have updated location
|
||||||
final contact = contactsProvider.findContactByKey(publicKey);
|
final contact = contactsProvider.findContactByKey(publicKey);
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
@@ -1583,6 +1583,29 @@ class AppProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _retainAdvertRxPath(Uint8List publicKey) async {
|
||||||
|
final decoded = _findBestMatchingAdvertRxRoute(publicKey);
|
||||||
|
if (decoded == null || decoded.pathBytes.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsedRoute = ContactRouteCodec.parse(
|
||||||
|
decoded.hopHashes.join(','),
|
||||||
|
expectedHashSize: decoded.hashSize,
|
||||||
|
);
|
||||||
|
contactsProvider.retainReceivedRoute(
|
||||||
|
publicKey,
|
||||||
|
signedEncodedPathLen: parsedRoute.signedEncodedPathLen,
|
||||||
|
paddedPathBytes: parsedRoute.paddedPathBytes,
|
||||||
|
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||||
|
);
|
||||||
|
await _pathHistoryService.recordReceivedBytePath(
|
||||||
|
publicKey.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(),
|
||||||
|
decoded.pathBytes,
|
||||||
|
decoded.hashSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
int _inferReceivedPathHashSize(
|
int _inferReceivedPathHashSize(
|
||||||
List<int> pathBytes, {
|
List<int> pathBytes, {
|
||||||
required int preferredHashSize,
|
required int preferredHashSize,
|
||||||
@@ -2629,6 +2652,37 @@ class AppProvider with ChangeNotifier {
|
|||||||
return bestLog;
|
return bestLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DecodedLogRxRoute? _findBestMatchingAdvertRxRoute(Uint8List publicKey) {
|
||||||
|
final publicKeyHex = publicKey
|
||||||
|
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
DecodedLogRxRoute? bestRoute;
|
||||||
|
var bestDeltaMs = 999999999;
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
for (final log in connectionProvider.bleService.packetLogs) {
|
||||||
|
if (log.responseCode != 0x88) continue;
|
||||||
|
if (log.rawData.length < 6) continue;
|
||||||
|
|
||||||
|
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||||
|
if (decoded == null) continue;
|
||||||
|
if (decoded.payloadType != 0x04) continue;
|
||||||
|
final senderHash = decoded.originalSenderHashHex;
|
||||||
|
if (senderHash == null || !publicKeyHex.startsWith(senderHash)) continue;
|
||||||
|
|
||||||
|
final deltaMs = (log.timestamp.difference(now).inMilliseconds).abs();
|
||||||
|
if (deltaMs < bestDeltaMs) {
|
||||||
|
bestDeltaMs = deltaMs;
|
||||||
|
bestRoute = decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestDeltaMs > 30000) return null;
|
||||||
|
return bestRoute;
|
||||||
|
}
|
||||||
|
|
||||||
List<int>? _extractPathBytesFromLog(BlePacketLog? log) {
|
List<int>? _extractPathBytesFromLog(BlePacketLog? log) {
|
||||||
if (log == null) return null;
|
if (log == null) return null;
|
||||||
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||||
|
|||||||
@@ -10,8 +10,15 @@ import '../utils/key_comparison.dart';
|
|||||||
class PendingAdvert {
|
class PendingAdvert {
|
||||||
final Uint8List publicKey;
|
final Uint8List publicKey;
|
||||||
final DateTime receivedAt;
|
final DateTime receivedAt;
|
||||||
|
final int? signedEncodedPathLen;
|
||||||
|
final Uint8List? paddedPathBytes;
|
||||||
|
|
||||||
const PendingAdvert({required this.publicKey, required this.receivedAt});
|
const PendingAdvert({
|
||||||
|
required this.publicKey,
|
||||||
|
required this.receivedAt,
|
||||||
|
this.signedEncodedPathLen,
|
||||||
|
this.paddedPathBytes,
|
||||||
|
});
|
||||||
|
|
||||||
String get publicKeyHex =>
|
String get publicKeyHex =>
|
||||||
publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||||||
@@ -21,14 +28,31 @@ class PendingAdvert {
|
|||||||
return prefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':');
|
return prefix.map((b) => b.toRadixString(16).padLeft(2, '0')).join(':');
|
||||||
}
|
}
|
||||||
|
|
||||||
PendingAdvert copyWith({Uint8List? publicKey, DateTime? receivedAt}) {
|
PendingAdvert copyWith({
|
||||||
|
Uint8List? publicKey,
|
||||||
|
DateTime? receivedAt,
|
||||||
|
int? signedEncodedPathLen,
|
||||||
|
Uint8List? paddedPathBytes,
|
||||||
|
}) {
|
||||||
return PendingAdvert(
|
return PendingAdvert(
|
||||||
publicKey: publicKey ?? this.publicKey,
|
publicKey: publicKey ?? this.publicKey,
|
||||||
receivedAt: receivedAt ?? this.receivedAt,
|
receivedAt: receivedAt ?? this.receivedAt,
|
||||||
|
signedEncodedPathLen: signedEncodedPathLen ?? this.signedEncodedPathLen,
|
||||||
|
paddedPathBytes: paddedPathBytes ?? this.paddedPathBytes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RetainedRoute {
|
||||||
|
final int signedEncodedPathLen;
|
||||||
|
final Uint8List paddedPathBytes;
|
||||||
|
|
||||||
|
const _RetainedRoute({
|
||||||
|
required this.signedEncodedPathLen,
|
||||||
|
required this.paddedPathBytes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Contacts Provider - manages contact list and telemetry
|
/// Contacts Provider - manages contact list and telemetry
|
||||||
class ContactsProvider with ChangeNotifier {
|
class ContactsProvider with ChangeNotifier {
|
||||||
final Map<String, Contact> _contacts = {};
|
final Map<String, Contact> _contacts = {};
|
||||||
@@ -338,8 +362,19 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
required Contact incomingContact,
|
required Contact incomingContact,
|
||||||
Contact? existingContact,
|
Contact? existingContact,
|
||||||
}) {
|
}) {
|
||||||
|
final retainedRoute = _retainedRouteForContact(
|
||||||
|
keyHex: incomingContact.publicKeyHex,
|
||||||
|
incomingContact: incomingContact,
|
||||||
|
existingContact: existingContact,
|
||||||
|
);
|
||||||
|
|
||||||
if (existingContact == null) {
|
if (existingContact == null) {
|
||||||
var newContact = incomingContact.copyWith(isNew: true);
|
var newContact = incomingContact.copyWith(
|
||||||
|
isNew: true,
|
||||||
|
outPathLen:
|
||||||
|
retainedRoute?.signedEncodedPathLen ?? incomingContact.outPathLen,
|
||||||
|
outPath: retainedRoute?.paddedPathBytes ?? incomingContact.outPath,
|
||||||
|
);
|
||||||
if (incomingContact.advertLocation != null) {
|
if (incomingContact.advertLocation != null) {
|
||||||
final timestamp = DateTime.fromMillisecondsSinceEpoch(
|
final timestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||||
incomingContact.lastAdvert * 1000,
|
incomingContact.lastAdvert * 1000,
|
||||||
@@ -363,6 +398,9 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
isNew: existingContact.isNew,
|
isNew: existingContact.isNew,
|
||||||
advertHistory: existingContact.advertHistory,
|
advertHistory: existingContact.advertHistory,
|
||||||
telemetry: mergedTelemetry,
|
telemetry: mergedTelemetry,
|
||||||
|
outPathLen:
|
||||||
|
retainedRoute?.signedEncodedPathLen ?? incomingContact.outPathLen,
|
||||||
|
outPath: retainedRoute?.paddedPathBytes ?? incomingContact.outPath,
|
||||||
advLat: incomingAdvertLocation != null
|
advLat: incomingAdvertLocation != null
|
||||||
? incomingContact.advLat
|
? incomingContact.advLat
|
||||||
: existingAdvertLocation != null
|
: existingAdvertLocation != null
|
||||||
@@ -388,6 +426,37 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
return updatedContact;
|
return updatedContact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_RetainedRoute? _retainedRouteForContact({
|
||||||
|
required String keyHex,
|
||||||
|
required Contact incomingContact,
|
||||||
|
required Contact? existingContact,
|
||||||
|
}) {
|
||||||
|
if (incomingContact.routeHasPath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pendingAdvert = _pendingAdverts[keyHex];
|
||||||
|
final pendingPathBytes = pendingAdvert?.paddedPathBytes;
|
||||||
|
final pendingPathLen = pendingAdvert?.signedEncodedPathLen;
|
||||||
|
if (pendingPathLen != null &&
|
||||||
|
pendingPathBytes != null &&
|
||||||
|
pendingPathBytes.isNotEmpty) {
|
||||||
|
return _RetainedRoute(
|
||||||
|
signedEncodedPathLen: pendingPathLen,
|
||||||
|
paddedPathBytes: Uint8List.fromList(pendingPathBytes),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingContact != null && existingContact.routeHasPath) {
|
||||||
|
return _RetainedRoute(
|
||||||
|
signedEncodedPathLen: existingContact.outPathLen,
|
||||||
|
paddedPathBytes: Uint8List.fromList(existingContact.outPath),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Update contact telemetry
|
/// Update contact telemetry
|
||||||
void updateTelemetry(Uint8List publicKeyPrefix, Uint8List lppData) {
|
void updateTelemetry(Uint8List publicKeyPrefix, Uint8List lppData) {
|
||||||
debugPrint('📊 [ContactsProvider] updateTelemetry() called');
|
debugPrint('📊 [ContactsProvider] updateTelemetry() called');
|
||||||
@@ -660,6 +729,46 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void retainReceivedRoute(
|
||||||
|
Uint8List publicKey, {
|
||||||
|
required int signedEncodedPathLen,
|
||||||
|
required Uint8List paddedPathBytes,
|
||||||
|
Uint8List? devicePublicKey,
|
||||||
|
}) {
|
||||||
|
if (devicePublicKey != null && publicKey.matches(devicePublicKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final keyHex = publicKey
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join('');
|
||||||
|
final contact = _contacts[keyHex];
|
||||||
|
if (contact != null) {
|
||||||
|
_contacts[keyHex] = contact.copyWith(
|
||||||
|
outPathLen: signedEncodedPathLen,
|
||||||
|
outPath: Uint8List.fromList(paddedPathBytes),
|
||||||
|
);
|
||||||
|
_persistContacts();
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final existing = _pendingAdverts[keyHex];
|
||||||
|
_pendingAdverts[keyHex] =
|
||||||
|
(existing ??
|
||||||
|
PendingAdvert(
|
||||||
|
publicKey: Uint8List.fromList(publicKey),
|
||||||
|
receivedAt: now,
|
||||||
|
))
|
||||||
|
.copyWith(
|
||||||
|
receivedAt: now,
|
||||||
|
signedEncodedPathLen: signedEncodedPathLen,
|
||||||
|
paddedPathBytes: Uint8List.fromList(paddedPathBytes),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void resetContactRouteLocal(Uint8List publicKey) {
|
void resetContactRouteLocal(Uint8List publicKey) {
|
||||||
final contact = findContactByKey(publicKey);
|
final contact = findContactByKey(publicKey);
|
||||||
if (contact == null) {
|
if (contact == null) {
|
||||||
|
|||||||
@@ -471,6 +471,57 @@ void main() {
|
|||||||
expect(updated.routeHasPath, isFalse);
|
expect(updated.routeHasPath, isFalse);
|
||||||
expect(updated.routeSummary, 'Flood/Unknown');
|
expect(updated.routeSummary, 'Flood/Unknown');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('retains existing route when contact refresh omits path', () {
|
||||||
|
final retainedRoute = ContactRouteCodec.parse('AABB,CCDD');
|
||||||
|
provider.setContactRouteLocal(
|
||||||
|
publicKey,
|
||||||
|
signedEncodedPathLen: retainedRoute.signedEncodedPathLen,
|
||||||
|
paddedPathBytes: retainedRoute.paddedPathBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
provider.addOrUpdateContact(
|
||||||
|
createContact(
|
||||||
|
key: publicKey,
|
||||||
|
type: ContactType.chat,
|
||||||
|
name: 'Routey',
|
||||||
|
).copyWith(outPathLen: -1, outPath: Uint8List(0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = provider.findContactByKey(publicKey)!;
|
||||||
|
expect(updated.routeHasPath, isTrue);
|
||||||
|
expect(updated.routeCanonicalText, 'AABB,CCDD');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applies retained pending advert route when contact is resolved', () {
|
||||||
|
final pendingKey = createPublicKey(96);
|
||||||
|
final retainedRoute = ContactRouteCodec.parse('1122,3344');
|
||||||
|
|
||||||
|
provider.retainReceivedRoute(
|
||||||
|
pendingKey,
|
||||||
|
signedEncodedPathLen: retainedRoute.signedEncodedPathLen,
|
||||||
|
paddedPathBytes: retainedRoute.paddedPathBytes,
|
||||||
|
);
|
||||||
|
provider.addPendingAdvert(pendingKey);
|
||||||
|
|
||||||
|
provider.addOrUpdateContact(
|
||||||
|
createContact(
|
||||||
|
key: pendingKey,
|
||||||
|
type: ContactType.chat,
|
||||||
|
name: 'Pending Routey',
|
||||||
|
).copyWith(outPathLen: -1, outPath: Uint8List(0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final updated = provider.findContactByKey(pendingKey)!;
|
||||||
|
expect(updated.routeHasPath, isTrue);
|
||||||
|
expect(updated.routeCanonicalText, '1122,3344');
|
||||||
|
expect(
|
||||||
|
provider.pendingAdverts.where(
|
||||||
|
(advert) => advert.publicKeyHex == updated.publicKeyHex,
|
||||||
|
),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('ContactsProvider.updateFastGps', () {
|
group('ContactsProvider.updateFastGps', () {
|
||||||
|
|||||||
Reference in New Issue
Block a user