Implement ranking system for direct repeaters based on SNR and recency; update related UI components to reflect changes

This commit is contained in:
Winston Lowe
2026-02-16 11:58:44 -08:00
parent 36401210ce
commit 42eb293d1c
4 changed files with 74 additions and 21 deletions
+24 -4
View File
@@ -54,6 +54,19 @@ class DirectRepeater {
lastUpdated = DateTime.now(); lastUpdated = DateTime.now();
} }
int get ranking {
if (isStale()) {
return -1; // Stale repeaters get lowest rank
}
// Higher SNR gets higher rank and recency within maxAgeMinutes breaks ties.
final ageMs =
DateTime.now().millisecondsSinceEpoch -
lastUpdated.millisecondsSinceEpoch;
final maxAgeMs = maxAgeMinutes * 60 * 1000;
final recencyScore = (maxAgeMs - ageMs).clamp(0, maxAgeMs);
return (snr * 1000).round() + recencyScore;
}
bool isStale() { bool isStale() {
return DateTime.now().difference(lastUpdated) > return DateTime.now().difference(lastUpdated) >
const Duration(minutes: maxAgeMinutes); const Duration(minutes: maxAgeMinutes);
@@ -3466,8 +3479,7 @@ class MeshCoreConnector extends ChangeNotifier {
return; return;
} }
if (publicKey == _selfPublicKey) { if (listEquals(publicKey, _selfPublicKey)) {
appLogger.info('Ignoring advert from self', tag: 'Connector');
return; return;
} }
@@ -3480,7 +3492,9 @@ class MeshCoreConnector extends ChangeNotifier {
name: name, name: name,
type: type, type: type,
pathLength: path.length, pathLength: path.length,
path: path, path: Uint8List.fromList(
path.reversed.toList(),
), // Store path in reverse for easier use in outgoing messages
latitude: latitude, latitude: latitude,
longitude: longitude, longitude: longitude,
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
@@ -3510,7 +3524,7 @@ class MeshCoreConnector extends ChangeNotifier {
latitude: hasLocation ? latitude : existing.latitude, latitude: hasLocation ? latitude : existing.latitude,
longitude: hasLocation ? longitude : existing.longitude, longitude: hasLocation ? longitude : existing.longitude,
name: hasName ? name : existing.name, name: hasName ? name : existing.name,
path: path, path: Uint8List.fromList(path.reversed.toList()),
pathLength: path.length, pathLength: path.length,
lastMessageAt: mergedLastMessageAt, lastMessageAt: mergedLastMessageAt,
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
@@ -3518,6 +3532,12 @@ class MeshCoreConnector extends ChangeNotifier {
pathOverrideBytes: existing.pathOverrideBytes, pathOverrideBytes: existing.pathOverrideBytes,
); );
// Add path to history if we have a valid path
if (_pathHistoryService != null &&
_contacts[existingIndex].pathLength >= 0) {
_pathHistoryService!.handlePathUpdated(_contacts[existingIndex]);
}
_updateDirectRepeater(_contacts[existingIndex], snr, path); _updateDirectRepeater(_contacts[existingIndex], snr, path);
appLogger.info( appLogger.info(
+40 -3
View File
@@ -437,6 +437,20 @@ class _ChatScreenState extends State<ChatScreen> {
builder: (context) => Consumer<PathHistoryService>( builder: (context) => Consumer<PathHistoryService>(
builder: (context, pathService, _) { builder: (context, pathService, _) {
final paths = pathService.getRecentPaths(widget.contact.publicKeyHex); final paths = pathService.getRecentPaths(widget.contact.publicKeyHex);
final repeatersList = List.of(connector.directRepeaters)
..sort((a, b) => b.ranking.compareTo(a.ranking));
final directRepeater = repeatersList.isEmpty
? null
: repeatersList.first;
final secondDirectRepeater = repeatersList.length < 2
? null
: repeatersList.elementAt(1);
final thirdDirectRepeater = repeatersList.length < 3
? null
: repeatersList.elementAt(2);
return AlertDialog( return AlertDialog(
title: Row( title: Row(
children: [ children: [
@@ -478,15 +492,38 @@ class _ChatScreenState extends State<ChatScreen> {
], ],
const SizedBox(height: 8), const SizedBox(height: 8),
...paths.map((path) { ...paths.map((path) {
final isDirectRepeater =
directRepeater != null &&
path.pathBytes.isNotEmpty &&
directRepeater.pubkeyFirstByte ==
path.pathBytes.first;
final isSecoundDirectRepeater =
secondDirectRepeater != null &&
path.pathBytes.isNotEmpty &&
secondDirectRepeater.pubkeyFirstByte ==
path.pathBytes.first;
final isThirdDirectRepeater =
thirdDirectRepeater != null &&
path.pathBytes.isNotEmpty &&
thirdDirectRepeater.pubkeyFirstByte ==
path.pathBytes.first;
Color color = Colors.grey;
if (isDirectRepeater) {
color = Colors.green;
} else if (isSecoundDirectRepeater) {
color = Colors.yellow;
} else if (isThirdDirectRepeater) {
color = Colors.red;
} else if (path.wasFloodDiscovery) {
color = Colors.blue;
}
return Card( return Card(
margin: const EdgeInsets.symmetric(vertical: 4), margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile( child: ListTile(
dense: true, dense: true,
leading: CircleAvatar( leading: CircleAvatar(
radius: 16, radius: 16,
backgroundColor: path.wasFloodDiscovery backgroundColor: color,
? Colors.blue
: Colors.green,
child: Text( child: Text(
'${path.hopCount}', '${path.hopCount}',
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
+9 -13
View File
@@ -134,23 +134,18 @@ class _PathManagementDialog extends StatelessWidget {
final currentContact = _resolveContact(connector); final currentContact = _resolveContact(connector);
final paths = pathService.getRecentPaths(currentContact.publicKeyHex); final paths = pathService.getRecentPaths(currentContact.publicKeyHex);
final RepeatersList = List.of(connector.directRepeaters) final repeatersList = List.of(connector.directRepeaters)
..sort((a, b) => b.lastUpdated.compareTo(a.lastUpdated)); ..sort((a, b) => b.ranking.compareTo(a.ranking));
final topSNRRepeaters = List.of(RepeatersList) final directRepeater = repeatersList.isEmpty
..sort((a, b) => b.snr.compareTo(a.snr));
final topThreeRepeaters = topSNRRepeaters.take(3).toList();
final directRepeater = topThreeRepeaters.isEmpty
? null ? null
: topThreeRepeaters.first; : repeatersList.first;
final secondDirectRepeater = topThreeRepeaters.length < 2 final secondDirectRepeater = repeatersList.length < 2
? null ? null
: topThreeRepeaters.elementAt(1); : repeatersList.elementAt(1);
final thirdDirectRepeater = topThreeRepeaters.length < 3 final thirdDirectRepeater = repeatersList.length < 3
? null ? null
: topThreeRepeaters.elementAt(2); : repeatersList.elementAt(2);
return AlertDialog( return AlertDialog(
title: Text(l10n.chat_pathManagement), title: Text(l10n.chat_pathManagement),
@@ -206,6 +201,7 @@ class _PathManagementDialog extends StatelessWidget {
path.pathBytes.isNotEmpty && path.pathBytes.isNotEmpty &&
thirdDirectRepeater.pubkeyFirstByte == thirdDirectRepeater.pubkeyFirstByte ==
path.pathBytes.first; path.pathBytes.first;
Color color = Colors.grey; Color color = Colors.grey;
if (isDirectRepeater) { if (isDirectRepeater) {
color = Colors.green; color = Colors.green;
+1 -1
View File
@@ -76,7 +76,7 @@ class _SNRIndicatorState extends State<SNRIndicator> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final directRepeaters = widget.connector.directRepeaters; final directRepeaters = widget.connector.directRepeaters;
final directBestRepeaters = List.of(directRepeaters) final directBestRepeaters = List.of(directRepeaters)
..sort((a, b) => (b.snr).compareTo(a.snr)); ..sort((a, b) => (b.ranking).compareTo(a.ranking));
final directRepeater = directBestRepeaters.isEmpty final directRepeater = directBestRepeaters.isEmpty
? null ? null
: directBestRepeaters.first; : directBestRepeaters.first;