Implement meshcore-open route reload

This commit is contained in:
Janez T
2026-03-08 14:26:05 +01:00
parent e026c1dbbd
commit 17d7c43745
25 changed files with 1927 additions and 278 deletions

View File

@@ -0,0 +1,31 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:meshcore_sar_app/services/messaging_route_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
test('route preference defaults are disabled', () async {
expect(
await MessagingRoutePreferences.getAutoRouteRotationEnabled(),
isFalse,
);
expect(await MessagingRoutePreferences.getClearPathOnMaxRetry(), isFalse);
});
test('route preferences persist changes', () async {
await MessagingRoutePreferences.setAutoRouteRotationEnabled(true);
await MessagingRoutePreferences.setClearPathOnMaxRetry(true);
expect(
await MessagingRoutePreferences.getAutoRouteRotationEnabled(),
isTrue,
);
expect(await MessagingRoutePreferences.getClearPathOnMaxRetry(), isTrue);
});
}

View File

@@ -0,0 +1,115 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:geolocator/geolocator.dart';
import 'package:meshcore_sar_app/models/contact.dart';
import 'package:meshcore_sar_app/services/nearest_router_selector.dart';
Contact _buildRepeater({
required int seed,
required String name,
required double latitude,
required double longitude,
required int lastAdvert,
int outPathLen = -1,
}) {
return Contact(
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i + seed)),
type: ContactType.repeater,
flags: 0,
outPathLen: outPathLen,
outPath: Uint8List(0),
advName: name,
lastAdvert: lastAdvert,
advLat: (latitude * 1e6).round(),
advLon: (longitude * 1e6).round(),
lastMod: lastAdvert,
);
}
Position _position(double latitude, double longitude) {
return Position(
latitude: latitude,
longitude: longitude,
timestamp: DateTime.now(),
accuracy: 1,
altitude: 0,
altitudeAccuracy: 1,
heading: 0,
headingAccuracy: 1,
speed: 0,
speedAccuracy: 0,
);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('selector chooses nearest eligible repeater', () {
final selector = NearestRouterSelector();
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final recipient = _buildRepeater(
seed: 90,
name: 'Recipient',
latitude: 46.05,
longitude: 14.50,
lastAdvert: now,
).copyWith(type: ContactType.chat);
final selected = selector.select(
senderPosition: _position(46.0569, 14.5058),
repeaters: [
_buildRepeater(
seed: 1,
name: 'Far',
latitude: 46.10,
longitude: 14.60,
lastAdvert: now,
outPathLen: 1,
),
_buildRepeater(
seed: 2,
name: 'Near',
latitude: 46.0570,
longitude: 14.5060,
lastAdvert: now - 5,
outPathLen: 1,
),
],
recipient: recipient,
);
expect(selected?.advName, 'Near');
});
test('selector skips stale repeaters', () {
final selector = NearestRouterSelector();
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final staleAdvert = now - (11 * 60);
final recipient = _buildRepeater(
seed: 91,
name: 'Recipient',
latitude: 46.05,
longitude: 14.50,
lastAdvert: now,
).copyWith(type: ContactType.chat);
final selected = selector.select(
senderPosition: _position(46.0569, 14.5058),
repeaters: [
_buildRepeater(
seed: 3,
name: 'Stale',
latitude: 46.0570,
longitude: 14.5060,
lastAdvert: staleAdvert,
outPathLen: 1,
),
],
recipient: recipient,
);
expect(selected, isNull);
});
}

View File

@@ -0,0 +1,130 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:meshcore_sar_app/models/contact.dart';
import 'package:meshcore_sar_app/models/path_selection.dart';
import 'package:meshcore_sar_app/services/path_history_service.dart';
Contact _buildContact({
required int seed,
required List<int> pathBytes,
required int hopCount,
required int hashSize,
}) {
final encoded = ((hashSize - 1) << 6) | (hopCount & 0x3F);
final outPath = Uint8List(ContactRouteCodec.maxPathBytes)
..setRange(0, pathBytes.length, pathBytes);
return Contact(
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i + seed)),
type: ContactType.chat,
flags: 0,
outPathLen: ContactRouteCodec.toSignedDescriptor(encoded),
outPath: outPath,
advName: 'Contact $seed',
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
advLat: 0,
advLon: 0,
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SharedPreferences.setMockInitialValues({});
});
test('auto rotation ranks best paths before flood', () async {
final service = PathHistoryService();
final contact = _buildContact(
seed: 0,
pathBytes: [0xAA, 0xBB],
hopCount: 2,
hashSize: 1,
);
final best = PathSelection(
mode: PathSelectionMode.directHistorical,
pathBytes: Uint8List.fromList([0xAA, 0xBB]),
hopCount: 2,
hashSize: 1,
);
final second = PathSelection(
mode: PathSelectionMode.directHistorical,
pathBytes: Uint8List.fromList([0xCC, 0xDD]),
hopCount: 2,
hashSize: 1,
);
await service.initialize();
await service.recordLearnedPath(contact);
await service.recordPathResult(
contact.publicKeyHex,
best,
success: true,
roundTripTimeMs: 120,
);
await service.recordPathResult(
contact.publicKeyHex,
best,
success: true,
roundTripTimeMs: 110,
);
await service.recordPathResult(
contact.publicKeyHex,
second,
success: true,
roundTripTimeMs: 200,
);
await service.recordPathResult(
contact.publicKeyHex,
second,
success: false,
);
final first = await service.getSelectionForContact(
contact,
autoRouteRotationEnabled: true,
);
final third = await service.getSelectionForContact(
contact,
autoRouteRotationEnabled: true,
);
final secondPick = await service.getSelectionForContact(
contact,
autoRouteRotationEnabled: true,
);
expect(first.mode, PathSelectionMode.directHistorical);
expect(first.canonicalPath, 'AA,BB');
expect(third.mode, PathSelectionMode.directHistorical);
expect(third.canonicalPath, 'CC,DD');
expect(secondPick.mode, PathSelectionMode.flood);
});
test('no history falls back to flood', () async {
final service = PathHistoryService();
final contact = Contact(
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i)),
type: ContactType.chat,
flags: 0,
outPathLen: -1,
outPath: Uint8List(0),
advName: 'No Route',
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
advLat: 0,
advLon: 0,
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
);
final selection = await service.getSelectionForContact(
contact,
autoRouteRotationEnabled: true,
);
expect(selection.mode, PathSelectionMode.flood);
});
}