Refactor contact handling and other improvments (#317)

* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
This commit is contained in:
Winston Lowe
2026-03-26 22:28:01 -07:00
committed by GitHub
parent 411cd3f8d2
commit d0e3767db6
51 changed files with 440 additions and 105 deletions
+41 -4
View File
@@ -193,6 +193,7 @@ class MeshCoreConnector extends ChangeNotifier {
static const int _contactMsgBackoffFallbackMs = 5000; static const int _contactMsgBackoffFallbackMs = 5000;
static const int _contactMsgBackoffMinMs = 500; static const int _contactMsgBackoffMinMs = 500;
static const int _contactMsgBackoffMaxMs = 15000; static const int _contactMsgBackoffMaxMs = 15000;
int _pollingInterval = 30;
bool _batteryRequested = false; bool _batteryRequested = false;
bool _awaitingSelfInfo = false; bool _awaitingSelfInfo = false;
bool _hasReceivedDeviceInfo = false; bool _hasReceivedDeviceInfo = false;
@@ -323,8 +324,14 @@ class MeshCoreConnector extends ChangeNotifier {
List<Contact> get allContacts => List.unmodifiable([ List<Contact> get allContacts => List.unmodifiable([
..._contacts, ..._contacts,
..._discoveredContacts.where((c) => !c.isActive), ..._discoveredContacts.where(
(c) => !c.isActive && c.publicKeyHex != selfPublicKeyHex,
),
]); ]);
List<Contact> get allContactsUnfiltered =>
List.unmodifiable([..._contacts, ..._discoveredContacts]);
List<Contact> get discoveredContacts { List<Contact> get discoveredContacts {
return List.unmodifiable(_discoveredContacts); return List.unmodifiable(_discoveredContacts);
} }
@@ -2242,9 +2249,18 @@ class MeshCoreConnector extends ChangeNotifier {
_batteryPollTimer = null; _batteryPollTimer = null;
} }
void setPollingInterval(int i) {
_pollingInterval = i.clamp(1, 60);
if (isConnected) {
_startRadioStatsPolling();
}
}
void _startRadioStatsPolling() { void _startRadioStatsPolling() {
_radioStatsPollTimer?.cancel(); _radioStatsPollTimer?.cancel();
_radioStatsPollTimer = Timer.periodic(const Duration(seconds: 1), (_) { _radioStatsPollTimer = Timer.periodic(Duration(seconds: _pollingInterval), (
_,
) {
if (!isConnected) { if (!isConnected) {
_stopRadioStatsPolling(); _stopRadioStatsPolling();
return; return;
@@ -2369,6 +2385,18 @@ class MeshCoreConnector extends ChangeNotifier {
}); });
} }
Contact getFromDiscovered(Contact contact) {
final tmp = _discoveredContacts.firstWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
orElse: () => contact,
);
return contact.copyWith(
rawPacket: tmp.rawPacket,
latitude: tmp.latitude,
longitude: tmp.longitude,
);
}
Future<void> getContacts({int? since, bool preserveExisting = false}) async { Future<void> getContacts({int? since, bool preserveExisting = false}) async {
if (!isConnected) return; if (!isConnected) return;
@@ -3735,8 +3763,17 @@ class MeshCoreConnector extends ChangeNotifier {
} }
void _handleContact(Uint8List frame, {bool isContact = true}) { void _handleContact(Uint8List frame, {bool isContact = true}) {
final contact = Contact.fromFrame(frame); final contactTmp = Contact.fromFrame(frame);
if (contact != null) { if (contactTmp != null) {
if (listEquals(contactTmp.publicKey, _selfPublicKey)) {
appLogger.info(
'Ignoring contact with self public key: ${contactTmp.name}',
tag: 'Connector',
);
removeContact(contactTmp);
return;
}
final contact = getFromDiscovered(contactTmp);
_handleDiscovery(contact, frame, noNotify: true, addActive: true); _handleDiscovery(contact, frame, noNotify: true, addActive: true);
if (contact.type == advTypeRepeater) { if (contact.type == advTypeRepeater) {
+2 -2
View File
@@ -202,15 +202,15 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32; const int cmdSetChannel = 32;
const int cmdSendTracePath = 36; const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38; const int cmdSetOtherParams = 38;
const int cmdSendAnonReq = 57;
const int cmdSendTelemetryReq = 39; const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40; const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41; const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50; const int cmdSendBinaryReq = 50;
const int cmdGetStats = 56;
const int cmdSendAnonReq = 57;
const int cmdSetAutoAddConfig = 58; const int cmdSetAutoAddConfig = 58;
const int cmdGetAutoAddConfig = 59; const int cmdGetAutoAddConfig = 59;
const int cmdSetPathHashMode = 61; const int cmdSetPathHashMode = 61;
const int cmdGetStats = 56;
// Text message types // Text message types
const int txtTypePlain = 0; const int txtTypePlain = 0;
+4 -2
View File
@@ -2017,5 +2017,7 @@
"scanner_linuxPairingHidePin": "Скрий ПИН", "scanner_linuxPairingHidePin": "Скрий ПИН",
"scanner_linuxPairingShowPin": "Покажи PIN", "scanner_linuxPairingShowPin": "Покажи PIN",
"scanner_linuxPairingPinTitle": "PIN код за сдвояване на Bluetooth", "scanner_linuxPairingPinTitle": "PIN код за сдвояване на Bluetooth",
"scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма)." "scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма).",
} "repeater_cliQuickClockSync": "Синхронизация на часовника",
"repeater_cliQuickDiscovery": "Открий Съседи"
}
+4 -2
View File
@@ -2045,5 +2045,7 @@
"scanner_linuxPairingShowPin": "PIN anzeigen", "scanner_linuxPairingShowPin": "PIN anzeigen",
"scanner_linuxPairingHidePin": "PIN ausblenden", "scanner_linuxPairingHidePin": "PIN ausblenden",
"scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN", "scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN",
"scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine)." "scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).",
} "repeater_cliQuickClockSync": "Uhr Synchronisieren",
"repeater_cliQuickDiscovery": "Entdecke Nachbarn"
}
+8 -2
View File
@@ -303,8 +303,12 @@
"path_routeWeight": "{weight}/{max}", "path_routeWeight": "{weight}/{max}",
"@path_routeWeight": { "@path_routeWeight": {
"placeholders": { "placeholders": {
"weight": { "type": "String" }, "weight": {
"max": { "type": "String" } "type": "String"
},
"max": {
"type": "String"
}
} }
}, },
"appSettings_battery": "Battery", "appSettings_battery": "Battery",
@@ -1333,6 +1337,8 @@
"repeater_cliQuickVersion": "Version", "repeater_cliQuickVersion": "Version",
"repeater_cliQuickAdvertise": "Advertise", "repeater_cliQuickAdvertise": "Advertise",
"repeater_cliQuickClock": "Clock", "repeater_cliQuickClock": "Clock",
"repeater_cliQuickClockSync": "Clock Sync",
"repeater_cliQuickDiscovery": "Discover Neighbors",
"repeater_cliHelpAdvert": "Sends an advertisement packet", "repeater_cliHelpAdvert": "Sends an advertisement packet",
"repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)", "repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)",
"repeater_cliHelpClock": "Displays current time per device's clock.", "repeater_cliHelpClock": "Displays current time per device's clock.",
+4 -2
View File
@@ -2045,5 +2045,7 @@
"scanner_linuxPairingShowPin": "Mostrar PIN", "scanner_linuxPairingShowPin": "Mostrar PIN",
"scanner_linuxPairingPinTitle": "PIN de emparejamiento Bluetooth", "scanner_linuxPairingPinTitle": "PIN de emparejamiento Bluetooth",
"scanner_linuxPairingHidePin": "Ocultar PIN", "scanner_linuxPairingHidePin": "Ocultar PIN",
"scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno)." "scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno).",
} "repeater_cliQuickDiscovery": "Descubrir Vecinos",
"repeater_cliQuickClockSync": "Sincronización del reloj"
}
+4 -2
View File
@@ -2017,5 +2017,7 @@
"scanner_linuxPairingShowPin": "Afficher le code PIN", "scanner_linuxPairingShowPin": "Afficher le code PIN",
"scanner_linuxPairingHidePin": "Masquer le code PIN", "scanner_linuxPairingHidePin": "Masquer le code PIN",
"scanner_linuxPairingPinTitle": "Code PIN dappairage Bluetooth", "scanner_linuxPairingPinTitle": "Code PIN dappairage Bluetooth",
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si aucun)." "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si aucun).",
} "repeater_cliQuickClockSync": "Synchronisation de l'horloge",
"repeater_cliQuickDiscovery": "Découvrir les voisins"
}
+3 -1
View File
@@ -2055,5 +2055,7 @@
"scanner_linuxPairingHidePin": "PIN elrejtése", "scanner_linuxPairingHidePin": "PIN elrejtése",
"scanner_linuxPairingShowPin": "PIN megjelenítése", "scanner_linuxPairingShowPin": "PIN megjelenítése",
"scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN", "scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN",
"scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs)." "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).",
"repeater_cliQuickClockSync": "Óra szinkronizálás",
"repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat"
} }
+4 -2
View File
@@ -2017,5 +2017,7 @@
"scanner_linuxPairingShowPin": "Mostra PIN", "scanner_linuxPairingShowPin": "Mostra PIN",
"scanner_linuxPairingHidePin": "Nascondi PIN", "scanner_linuxPairingHidePin": "Nascondi PIN",
"scanner_linuxPairingPinTitle": "PIN di associazione Bluetooth", "scanner_linuxPairingPinTitle": "PIN di associazione Bluetooth",
"scanner_linuxPairingPinPrompt": "Inserisci il PIN per {deviceName} (lascia vuoto se non ce n'è)." "scanner_linuxPairingPinPrompt": "Inserisci il PIN per {deviceName} (lascia vuoto se non ce n'è).",
} "repeater_cliQuickClockSync": "Sincronizzazione dell'orologio",
"repeater_cliQuickDiscovery": "Scopri i Vicini"
}
+3 -1
View File
@@ -2055,5 +2055,7 @@
"scanner_linuxPairingShowPin": "PINを表示", "scanner_linuxPairingShowPin": "PINを表示",
"scanner_linuxPairingHidePin": "PINを非表示", "scanner_linuxPairingHidePin": "PINを非表示",
"scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN", "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN",
"scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。" "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。",
"repeater_cliQuickClockSync": "クロック同期",
"repeater_cliQuickDiscovery": "近隣を発見する"
} }
+3 -1
View File
@@ -2055,5 +2055,7 @@
"scanner_linuxPairingShowPin": "PIN 표시", "scanner_linuxPairingShowPin": "PIN 표시",
"scanner_linuxPairingPinTitle": "블루투스 페어링 PIN", "scanner_linuxPairingPinTitle": "블루투스 페어링 PIN",
"scanner_linuxPairingHidePin": "PIN 숨기기", "scanner_linuxPairingHidePin": "PIN 숨기기",
"scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요)." "scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요).",
"repeater_cliQuickClockSync": "시계 동기화",
"repeater_cliQuickDiscovery": "이웃 발견하기"
} }
+12
View File
@@ -4322,6 +4322,18 @@ abstract class AppLocalizations {
/// **'Clock'** /// **'Clock'**
String get repeater_cliQuickClock; String get repeater_cliQuickClock;
/// No description provided for @repeater_cliQuickClockSync.
///
/// In en, this message translates to:
/// **'Clock Sync'**
String get repeater_cliQuickClockSync;
/// No description provided for @repeater_cliQuickDiscovery.
///
/// In en, this message translates to:
/// **'Discover Neighbors'**
String get repeater_cliQuickDiscovery;
/// No description provided for @repeater_cliHelpAdvert. /// No description provided for @repeater_cliHelpAdvert.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+6
View File
@@ -2429,6 +2429,12 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Часовник'; String get repeater_cliQuickClock => 'Часовник';
@override
String get repeater_cliQuickClockSync => 'Синхронизация на часовника';
@override
String get repeater_cliQuickDiscovery => 'Открий Съседи';
@override @override
String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет'; String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет';
+6
View File
@@ -2429,6 +2429,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Uhr'; String get repeater_cliQuickClock => 'Uhr';
@override
String get repeater_cliQuickClockSync => 'Uhr Synchronisieren';
@override
String get repeater_cliQuickDiscovery => 'Entdecke Nachbarn';
@override @override
String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung'; String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung';
+6
View File
@@ -2379,6 +2379,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Clock'; String get repeater_cliQuickClock => 'Clock';
@override
String get repeater_cliQuickClockSync => 'Clock Sync';
@override
String get repeater_cliQuickDiscovery => 'Discover Neighbors';
@override @override
String get repeater_cliHelpAdvert => 'Sends an advertisement packet'; String get repeater_cliHelpAdvert => 'Sends an advertisement packet';
+6
View File
@@ -2423,6 +2423,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Reloj'; String get repeater_cliQuickClock => 'Reloj';
@override
String get repeater_cliQuickClockSync => 'Sincronización del reloj';
@override
String get repeater_cliQuickDiscovery => 'Descubrir Vecinos';
@override @override
String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad'; String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad';
+6
View File
@@ -2442,6 +2442,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Horloge'; String get repeater_cliQuickClock => 'Horloge';
@override
String get repeater_cliQuickClockSync => 'Synchronisation de l\'horloge';
@override
String get repeater_cliQuickDiscovery => 'Découvrir les voisins';
@override @override
String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce'; String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce';
+6
View File
@@ -2437,6 +2437,12 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'óra'; String get repeater_cliQuickClock => 'óra';
@override
String get repeater_cliQuickClockSync => 'Óra szinkronizálás';
@override
String get repeater_cliQuickDiscovery => 'Fedezd fel a szomszédokat';
@override @override
String get repeater_cliHelpAdvert => 'Elküldi egy hirdetési csomagot'; String get repeater_cliHelpAdvert => 'Elküldi egy hirdetési csomagot';
+6
View File
@@ -2426,6 +2426,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Orologio'; String get repeater_cliQuickClock => 'Orologio';
@override
String get repeater_cliQuickClockSync => 'Sincronizzazione dell\'orologio';
@override
String get repeater_cliQuickDiscovery => 'Scopri i Vicini';
@override @override
String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario'; String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario';
+6
View File
@@ -2322,6 +2322,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => '時計'; String get repeater_cliQuickClock => '時計';
@override
String get repeater_cliQuickClockSync => 'クロック同期';
@override
String get repeater_cliQuickDiscovery => '近隣を発見する';
@override @override
String get repeater_cliHelpAdvert => '広告用資料を送る'; String get repeater_cliHelpAdvert => '広告用資料を送る';
+6
View File
@@ -2319,6 +2319,12 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => '시계'; String get repeater_cliQuickClock => '시계';
@override
String get repeater_cliQuickClockSync => '시계 동기화';
@override
String get repeater_cliQuickDiscovery => '이웃 발견하기';
@override @override
String get repeater_cliHelpAdvert => '광고 패킷을 발송'; String get repeater_cliHelpAdvert => '광고 패킷을 발송';
+6
View File
@@ -2409,6 +2409,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Tijd'; String get repeater_cliQuickClock => 'Tijd';
@override
String get repeater_cliQuickClockSync => 'Kloksynchronisatie';
@override
String get repeater_cliQuickDiscovery => 'Ontdek Buren';
@override @override
String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket'; String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket';
+6
View File
@@ -2436,6 +2436,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Godzina'; String get repeater_cliQuickClock => 'Godzina';
@override
String get repeater_cliQuickClockSync => 'Synchronizacja zegara';
@override
String get repeater_cliQuickDiscovery => 'Odkryj Sąsiadów';
@override @override
String get repeater_cliHelpAdvert => 'Wysyła pakiet rozgłoszeniowy'; String get repeater_cliHelpAdvert => 'Wysyła pakiet rozgłoszeniowy';
+6
View File
@@ -2423,6 +2423,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Relógio'; String get repeater_cliQuickClock => 'Relógio';
@override
String get repeater_cliQuickClockSync => 'Sincronização do Relógio';
@override
String get repeater_cliQuickDiscovery => 'Descobrir Vizinhos';
@override @override
String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios'; String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios';
+6
View File
@@ -2427,6 +2427,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Время'; String get repeater_cliQuickClock => 'Время';
@override
String get repeater_cliQuickClockSync => 'Синхронизация часов';
@override
String get repeater_cliQuickDiscovery => 'Обнаружить Соседей';
@override @override
String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования';
+6
View File
@@ -2406,6 +2406,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Hodiny'; String get repeater_cliQuickClock => 'Hodiny';
@override
String get repeater_cliQuickClockSync => 'Synchronizácia hodin';
@override
String get repeater_cliQuickDiscovery => 'Objaviť susedov';
@override @override
String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.'; String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.';
+6
View File
@@ -2409,6 +2409,12 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Ura'; String get repeater_cliQuickClock => 'Ura';
@override
String get repeater_cliQuickClockSync => 'Usklajevanje ure';
@override
String get repeater_cliQuickDiscovery => 'Odkrijte sosede';
@override @override
String get repeater_cliHelpAdvert => 'Pošlje paket oglasov'; String get repeater_cliHelpAdvert => 'Pošlje paket oglasov';
+6
View File
@@ -2394,6 +2394,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Klocka'; String get repeater_cliQuickClock => 'Klocka';
@override
String get repeater_cliQuickClockSync => 'Synkronisera klocka';
@override
String get repeater_cliQuickDiscovery => 'Upptäck grannar';
@override @override
String get repeater_cliHelpAdvert => 'Skickar ett annonspaket'; String get repeater_cliHelpAdvert => 'Skickar ett annonspaket';
+6
View File
@@ -2427,6 +2427,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => 'Годинник'; String get repeater_cliQuickClock => 'Годинник';
@override
String get repeater_cliQuickClockSync => 'Синхронізація годинника';
@override
String get repeater_cliQuickDiscovery => 'Відкрити сусідів';
@override @override
String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення'; String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення';
+6
View File
@@ -2277,6 +2277,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get repeater_cliQuickClock => '时钟'; String get repeater_cliQuickClock => '时钟';
@override
String get repeater_cliQuickClockSync => '同步时钟';
@override
String get repeater_cliQuickDiscovery => '发现邻居';
@override @override
String get repeater_cliHelpAdvert => '发送广播包'; String get repeater_cliHelpAdvert => '发送广播包';
+4 -2
View File
@@ -2017,5 +2017,7 @@
"scanner_linuxPairingShowPin": "Toon PIN", "scanner_linuxPairingShowPin": "Toon PIN",
"scanner_linuxPairingHidePin": "PIN verbergen", "scanner_linuxPairingHidePin": "PIN verbergen",
"scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).", "scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).",
"scanner_linuxPairingPinTitle": "BluetoothkoppelingsPIN" "scanner_linuxPairingPinTitle": "BluetoothkoppelingsPIN",
} "repeater_cliQuickDiscovery": "Ontdek Buren",
"repeater_cliQuickClockSync": "Kloksynchronisatie"
}
+4 -2
View File
@@ -2055,5 +2055,7 @@
"scanner_linuxPairingShowPin": "Pokaż PIN", "scanner_linuxPairingShowPin": "Pokaż PIN",
"scanner_linuxPairingHidePin": "Ukryj PIN", "scanner_linuxPairingHidePin": "Ukryj PIN",
"scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).", "scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).",
"scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth" "scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth",
} "repeater_cliQuickClockSync": "Synchronizacja zegara",
"repeater_cliQuickDiscovery": "Odkryj Sąsiadów"
}
+4 -2
View File
@@ -2017,5 +2017,7 @@
"scanner_linuxPairingShowPin": "Mostrar PIN", "scanner_linuxPairingShowPin": "Mostrar PIN",
"scanner_linuxPairingHidePin": "Ocultar PIN", "scanner_linuxPairingHidePin": "Ocultar PIN",
"scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).", "scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).",
"scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth" "scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth",
} "repeater_cliQuickClockSync": "Sincronização do Relógio",
"repeater_cliQuickDiscovery": "Descobrir Vizinhos"
}
+4 -2
View File
@@ -1257,5 +1257,7 @@
"scanner_linuxPairingShowPin": "Показать PIN", "scanner_linuxPairingShowPin": "Показать PIN",
"scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).", "scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).",
"scanner_linuxPairingHidePin": "Скрыть PIN", "scanner_linuxPairingHidePin": "Скрыть PIN",
"scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth" "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth",
} "repeater_cliQuickDiscovery": "Обнаружить Соседей",
"repeater_cliQuickClockSync": "Синхронизация часов"
}
+4 -2
View File
@@ -2017,5 +2017,7 @@
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak nie je, nechajte prázdne).", "scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak nie je, nechajte prázdne).",
"scanner_linuxPairingShowPin": "Zobraziť PIN", "scanner_linuxPairingShowPin": "Zobraziť PIN",
"scanner_linuxPairingHidePin": "Skryť PIN", "scanner_linuxPairingHidePin": "Skryť PIN",
"scanner_linuxPairingPinTitle": "Bluetooth párovací PIN" "scanner_linuxPairingPinTitle": "Bluetooth párovací PIN",
} "repeater_cliQuickClockSync": "Synchronizácia hodin",
"repeater_cliQuickDiscovery": "Objaviť susedov"
}
+4 -2
View File
@@ -2017,5 +2017,7 @@
"scanner_linuxPairingShowPin": "Prikaži PIN", "scanner_linuxPairingShowPin": "Prikaži PIN",
"scanner_linuxPairingHidePin": "Skrij PIN", "scanner_linuxPairingHidePin": "Skrij PIN",
"scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).", "scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).",
"scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje" "scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje",
} "repeater_cliQuickDiscovery": "Odkrijte sosede",
"repeater_cliQuickClockSync": "Usklajevanje ure"
}
+4 -2
View File
@@ -2017,5 +2017,7 @@
"scanner_linuxPairingShowPin": "Visa PIN", "scanner_linuxPairingShowPin": "Visa PIN",
"scanner_linuxPairingPinTitle": "BluetoothparningsPIN", "scanner_linuxPairingPinTitle": "BluetoothparningsPIN",
"scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).", "scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
"scanner_linuxPairingHidePin": "Dölj PIN" "scanner_linuxPairingHidePin": "Dölj PIN",
} "repeater_cliQuickDiscovery": "Upptäck grannar",
"repeater_cliQuickClockSync": "Synkronisera klocka"
}
+4 -2
View File
@@ -2017,5 +2017,7 @@
"scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth", "scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth",
"scanner_linuxPairingShowPin": "Показати PIN", "scanner_linuxPairingShowPin": "Показати PIN",
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
"scanner_linuxPairingHidePin": "Приховати PIN" "scanner_linuxPairingHidePin": "Приховати PIN",
} "repeater_cliQuickClockSync": "Синхронізація годинника",
"repeater_cliQuickDiscovery": "Відкрити сусідів"
}
+4 -2
View File
@@ -2022,5 +2022,7 @@
"scanner_linuxPairingShowPin": "显示 PIN码", "scanner_linuxPairingShowPin": "显示 PIN码",
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN(如果没有,请留空)。", "scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN(如果没有,请留空)。",
"scanner_linuxPairingPinTitle": "蓝牙配对 PIN", "scanner_linuxPairingPinTitle": "蓝牙配对 PIN",
"scanner_linuxPairingHidePin": "隐藏 PIN" "scanner_linuxPairingHidePin": "隐藏 PIN",
} "repeater_cliQuickDiscovery": "发现邻居",
"repeater_cliQuickClockSync": "同步时钟"
}
+15 -3
View File
@@ -822,7 +822,8 @@ List<_PathHop> _buildPathHops(
) { ) {
if (pathBytes.isEmpty) return const []; if (pathBytes.isEmpty) return const [];
final candidatesByPrefix = <int, List<Contact>>{}; final candidatesByPrefix = <int, List<Contact>>{};
for (final contact in connector.allContacts) { final allContacts = connector.allContacts;
for (final contact in allContacts) {
if (contact.publicKey.isEmpty) continue; if (contact.publicKey.isEmpty) continue;
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) { if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
continue; continue;
@@ -839,7 +840,8 @@ List<_PathHop> _buildPathHops(
: null; : null;
var previousPosition = startPoint; var previousPosition = startPoint;
final distance = Distance(); final distance = Distance();
var lastDistance = 0.0;
var bestDistance = 0.0;
final hops = <_PathHop>[]; final hops = <_PathHop>[];
for (var i = 0; i < pathBytes.length; i++) { for (var i = 0; i < pathBytes.length; i++) {
final searchPoint = i == 0 ? startPoint : previousPosition; final searchPoint = i == 0 ? startPoint : previousPosition;
@@ -848,7 +850,7 @@ List<_PathHop> _buildPathHops(
if (candidates != null && candidates.isNotEmpty) { if (candidates != null && candidates.isNotEmpty) {
var bestIndex = 0; var bestIndex = 0;
if (searchPoint != null) { if (searchPoint != null) {
var bestDistance = double.infinity; bestDistance = double.infinity;
for (var j = 0; j < candidates.length; j++) { for (var j = 0; j < candidates.length; j++) {
final candidate = candidates[j]; final candidate = candidates[j];
if (!candidate.hasLocation || if (!candidate.hasLocation ||
@@ -876,6 +878,16 @@ List<_PathHop> _buildPathHops(
if (resolvedPosition != null) { if (resolvedPosition != null) {
previousPosition = resolvedPosition; previousPosition = resolvedPosition;
} }
// If the best candidate is much farther than the previous hop, it's likely not the correct match.
if (lastDistance + bestDistance > 70000 &&
candidates != null &&
candidates.isNotEmpty) {
i--;
lastDistance = bestDistance;
continue;
}
lastDistance = bestDistance;
hops.add( hops.add(
_PathHop( _PathHop(
index: i + 1, index: i + 1,
+1 -1
View File
@@ -127,7 +127,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
canPop: allowBack, canPop: allowBack,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: AppBarTitle(context.l10n.channels_title, indicators: false), title: AppBarTitle(context.l10n.channels_title),
centerTitle: true, centerTitle: true,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
actions: [ actions: [
+1 -1
View File
@@ -290,6 +290,7 @@ class _ChatScreenState extends State<ChatScreen> {
tooltip: context.l10n.chat_pathManagement, tooltip: context.l10n.chat_pathManagement,
onPressed: () => _showPathHistory(context), onPressed: () => _showPathHistory(context),
), ),
const RadioStatsIconButton(),
Consumer<MeshCoreConnector>( Consumer<MeshCoreConnector>(
builder: (context, connector, _) { builder: (context, connector, _) {
return PopupMenuButton<String>( return PopupMenuButton<String>(
@@ -362,7 +363,6 @@ class _ChatScreenState extends State<ChatScreen> {
); );
}, },
), ),
const RadioStatsIconButton(),
], ],
), ),
body: Consumer<MeshCoreConnector>( body: Consumer<MeshCoreConnector>(
@@ -24,6 +24,7 @@ class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
final c = context.read<MeshCoreConnector>(); final c = context.read<MeshCoreConnector>();
_connector = c; _connector = c;
c.acquireRadioStatsPolling(); c.acquireRadioStatsPolling();
c.setPollingInterval(1);
c.radioStatsNotifier.addListener(_onStatsUpdate); c.radioStatsNotifier.addListener(_onStatsUpdate);
} }
@@ -44,6 +45,7 @@ class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
void dispose() { void dispose() {
_connector?.radioStatsNotifier.removeListener(_onStatsUpdate); _connector?.radioStatsNotifier.removeListener(_onStatsUpdate);
_connector?.releaseRadioStatsPolling(); _connector?.releaseRadioStatsPolling();
_connector?.setPollingInterval(30);
super.dispose(); super.dispose();
} }
+7 -12
View File
@@ -1240,9 +1240,7 @@ class _ContactsScreenState extends State<ContactsScreen>
if (isRepeater) ...[ if (isRepeater) ...[
ListTile( ListTile(
leading: const Icon(Icons.radar, color: Colors.green), leading: const Icon(Icons.radar, color: Colors.green),
title: contact.pathBytesForDisplay.isNotEmpty title: Text(context.l10n.contacts_ping),
? Text(context.l10n.contacts_pathTrace)
: Text(context.l10n.contacts_ping),
onTap: () { onTap: () {
final hw = context final hw = context
.read<MeshCoreConnector>() .read<MeshCoreConnector>()
@@ -1251,11 +1249,8 @@ class _ContactsScreenState extends State<ContactsScreen>
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => PathTraceMapScreen( builder: (context) => PathTraceMapScreen(
title: contact.pathBytesForDisplay.isNotEmpty title: context.l10n.contacts_repeaterPing,
? context.l10n.contacts_repeaterPathTrace path: Uint8List.fromList([contact.publicKey.first]),
: context.l10n.contacts_repeaterPing,
path: contact.pathBytesForDisplay,
flipPathAround: true,
targetContact: contact, targetContact: contact,
pathHashByteWidth: hw, pathHashByteWidth: hw,
), ),
@@ -1274,9 +1269,7 @@ class _ContactsScreenState extends State<ContactsScreen>
] else if (isRoom) ...[ ] else if (isRoom) ...[
ListTile( ListTile(
leading: const Icon(Icons.radar, color: Colors.green), leading: const Icon(Icons.radar, color: Colors.green),
title: contact.pathLength > 0 title: Text(context.l10n.contacts_pathTrace),
? Text(context.l10n.contacts_pathTrace)
: Text(context.l10n.contacts_ping),
onTap: () { onTap: () {
final hw = context final hw = context
.read<MeshCoreConnector>() .read<MeshCoreConnector>()
@@ -1288,7 +1281,9 @@ class _ContactsScreenState extends State<ContactsScreen>
title: contact.pathBytesForDisplay.isNotEmpty title: contact.pathBytesForDisplay.isNotEmpty
? context.l10n.contacts_roomPathTrace ? context.l10n.contacts_roomPathTrace
: context.l10n.contacts_roomPing, : context.l10n.contacts_roomPing,
path: contact.pathBytesForDisplay, path: contact.pathBytesForDisplay.isNotEmpty
? contact.pathBytesForDisplay
: Uint8List.fromList([contact.publicKey.first]),
flipPathAround: contact.pathBytesForDisplay.isNotEmpty, flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
targetContact: contact, targetContact: contact,
pathHashByteWidth: hw, pathHashByteWidth: hw,
+57 -5
View File
@@ -38,6 +38,13 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
super.dispose(); super.dispose();
} }
DateTime _resolveLastSeen(Contact contact) {
if (contact.type != advTypeChat) return contact.lastSeen;
return contact.lastMessageAt.isAfter(contact.lastSeen)
? contact.lastMessageAt
: contact.lastSeen;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
@@ -108,11 +115,56 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
trailing: Text( // Clamp text scaling in trailing section to prevent overflow while
_formatLastSeen(context, contact.lastSeen), // maintaining accessibility. Primary content (title/subtitle) scales normally.
style: TextStyle( trailing: MediaQuery(
fontSize: 12, data: MediaQuery.of(context).copyWith(
color: Colors.grey[600], textScaler: TextScaler.linear(
MediaQuery.textScalerOf(
context,
).scale(1.0).clamp(1.0, 1.3),
),
),
child: SizedBox(
width: 120,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
_formatLastSeen(
context,
_resolveLastSeen(contact),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (contact.hasLocation)
Icon(
Icons.location_on,
size: 14,
color: Colors.grey[400],
),
if (contact.rawPacket != null)
const SizedBox(width: 2),
if (contact.rawPacket != null)
Icon(
Icons.cell_tower,
size: 14,
color: Colors.grey[400],
),
],
),
],
),
), ),
), ),
onTap: () { onTap: () {
+47 -22
View File
@@ -64,6 +64,7 @@ class _MapScreenState extends State<MapScreen> {
bool _hasInitializedMap = false; bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false; bool _removedMarkersLoaded = false;
final List<int> _pathTrace = []; final List<int> _pathTrace = [];
final List<Contact> _pathTraceContacts = [];
final List<LatLng> _points = []; final List<LatLng> _points = [];
final List<Polyline> _polylines = []; final List<Polyline> _polylines = [];
bool _legendExpanded = false; bool _legendExpanded = false;
@@ -488,7 +489,7 @@ class _MapScreenState extends State<MapScreen> {
), ),
), ),
), ),
if (!_isBuildingPathTrace) if (!settings.mapShowOverlaps)
..._buildGuessedMarker( ..._buildGuessedMarker(
guessedLocations, guessedLocations,
showLabels: _showNodeLabels, showLabels: _showNodeLabels,
@@ -788,17 +789,26 @@ class _MapScreenState extends State<MapScreen> {
final markers = <Marker>[]; final markers = <Marker>[];
for (final guess in guessed) { for (final guess in guessed) {
if (guess.contact.type == advTypeChat && _isBuildingPathTrace) {
continue;
}
final color = _getNodeColor(guess.contact.type); final color = _getNodeColor(guess.contact.type);
final marker = Marker( final marker = Marker(
point: guess.position, point: guess.position,
width: 35, width: 35,
height: 35, height: 35,
child: GestureDetector( child: GestureDetector(
onTap: () => _showNodeInfo( onLongPress: () => _isBuildingPathTrace
context, ? _showNodeInfo(context, guess.contact)
guess.contact, : null,
guessedPosition: guess.position, onTap: () => _isBuildingPathTrace
), ? _addToPath(context, guess.contact, position: guess.position)
: _showNodeInfo(
context,
guess.contact,
guessedPosition: guess.position,
),
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -870,23 +880,29 @@ class _MapScreenState extends State<MapScreen> {
addContact = true; addContact = true;
} }
final hasOverlap = contacts if (contact.type == advTypeChat && _isBuildingPathTrace) {
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;
if (hasOverlap == null &&
settings.mapShowOverlaps &&
!_isBuildingPathTrace) {
addContact = false; addContact = false;
} }
if (settings.mapShowOverlaps) {
final hasOverlap = contacts
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;
if (hasOverlap == null &&
settings.mapShowOverlaps &&
!_isBuildingPathTrace) {
addContact = false;
}
}
if (addContact) { if (addContact) {
filtered.add(contact); filtered.add(contact);
} }
@@ -2121,12 +2137,18 @@ class _MapScreenState extends State<MapScreen> {
} }
} }
void _addToPath(BuildContext context, Contact contact) { void _addToPath(BuildContext context, Contact contact, {LatLng? position}) {
setState(() { setState(() {
_pathTrace.add( _pathTrace.add(
contact.publicKey[0], contact.publicKey[0],
); // Add first 16 bytes of public key to path trace ); // Add first 16 bytes of public key to path trace
_points.add(LatLng(contact.latitude!, contact.longitude!)); _pathTraceContacts.add(
contact.copyWith(
latitude: position?.latitude ?? contact.latitude,
longitude: position?.longitude ?? contact.longitude,
),
); // Add contact to path trace contacts
_points.add(position ?? LatLng(contact.latitude!, contact.longitude!));
}); });
} }
@@ -2134,6 +2156,7 @@ class _MapScreenState extends State<MapScreen> {
setState(() { setState(() {
_isBuildingPathTrace = true; _isBuildingPathTrace = true;
_pathTrace.clear(); _pathTrace.clear();
_pathTraceContacts.clear();
_points.clear(); _points.clear();
_polylines.clear(); _polylines.clear();
_points.add(position); _points.add(position);
@@ -2142,6 +2165,7 @@ class _MapScreenState extends State<MapScreen> {
void _removePath() { void _removePath() {
setState(() { setState(() {
_pathTraceContacts.removeLast();
_pathTrace.removeLast(); // Remove last node from path trace _pathTrace.removeLast(); // Remove last node from path trace
_points.removeLast(); // Remove last point from points list _points.removeLast(); // Remove last point from points list
_polylines.clear(); // Clear polylines _polylines.clear(); // Clear polylines
@@ -2201,6 +2225,7 @@ class _MapScreenState extends State<MapScreen> {
title: l10n.contacts_pathTrace, title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace), path: Uint8List.fromList(_pathTrace),
pathHashByteWidth: hashW, pathHashByteWidth: hashW,
pathContacts: _pathTraceContacts,
), ),
), ),
); );
+40 -10
View File
@@ -56,6 +56,7 @@ class PathTraceMapScreen extends StatefulWidget {
final bool reversePathAround; final bool reversePathAround;
final Contact? targetContact; final Contact? targetContact;
final int pathHashByteWidth; final int pathHashByteWidth;
final List<Contact>? pathContacts;
const PathTraceMapScreen({ const PathTraceMapScreen({
super.key, super.key,
@@ -66,6 +67,7 @@ class PathTraceMapScreen extends StatefulWidget {
this.reversePathAround = false, this.reversePathAround = false,
this.targetContact, this.targetContact,
this.pathHashByteWidth = pathHashSize, this.pathHashByteWidth = pathHashSize,
this.pathContacts,
}); });
@override @override
@@ -74,6 +76,8 @@ class PathTraceMapScreen extends StatefulWidget {
class _PathTraceMapScreenState extends State<PathTraceMapScreen> { class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
static const double _labelZoomThreshold = 8.5; static const double _labelZoomThreshold = 8.5;
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
StreamSubscription<Uint8List>? _frameSubscription; StreamSubscription<Uint8List>? _frameSubscription;
Timer? _timeoutTimer; Timer? _timeoutTimer;
@@ -266,17 +270,43 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toList(); .toList();
Map<int, Contact> pathContacts = {}; Map<int, Contact> pathContacts = {};
final contacts = connector.allContacts; Contact lastContact = Contact(
contacts.where((c) => c.type != advTypeChat).forEach((repeater) { path: Uint8List(0),
for (var repeaterData in pathData) { pathLength: 0,
if (listEquals( publicKey: connector.selfPublicKey ?? Uint8List(0),
repeater.publicKey.sublist(0, 1), name: context.l10n.pathTrace_you,
Uint8List.fromList([repeaterData]), type: advTypeChat,
)) { latitude: connector.selfLatitude,
pathContacts[repeaterData] = repeater; longitude: connector.selfLongitude,
lastSeen: DateTime.now(),
);
if (widget.pathContacts != null) {
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
} else {
final contacts = connector.allContactsUnfiltered;
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
if (lastContact.latitude != null &&
lastContact.longitude != null &&
repeater.hasLocation &&
lastContact.hasLocation &&
Distance().distance(
LatLng(lastContact.latitude!, lastContact.longitude!),
LatLng(repeater.latitude!, repeater.longitude!),
) >
_maxRepeaterMatchDistanceMeters) {
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
} }
} for (var repeaterData in pathData) {
}); if (listEquals(
repeater.publicKey.sublist(0, 1),
Uint8List.fromList([repeaterData]),
)) {
pathContacts[repeaterData] = repeater;
lastContact = repeater;
}
}
});
}
// For hops with no GPS contact, infer position from other contacts // For hops with no GPS contact, infer position from other contacts
// with known GPS that share the same last-hop byte. // with known GPS that share the same last-hop byte.
+7 -1
View File
@@ -35,13 +35,15 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
// Common commands for quick access // Common commands for quick access
late final List<Map<String, String>> _quickCommands = [ late final List<Map<String, String>> _quickCommands = [
{'labelKey': 'advertise', 'command': 'advert'},
{'labelKey': 'getName', 'command': 'get name'}, {'labelKey': 'getName', 'command': 'get name'},
{'labelKey': 'getRadio', 'command': 'get radio'}, {'labelKey': 'getRadio', 'command': 'get radio'},
{'labelKey': 'getTx', 'command': 'get tx'}, {'labelKey': 'getTx', 'command': 'get tx'},
{'labelKey': 'discovery', 'command': 'discover.neighbors'},
{'labelKey': 'neighbors', 'command': 'neighbors'}, {'labelKey': 'neighbors', 'command': 'neighbors'},
{'labelKey': 'version', 'command': 'ver'}, {'labelKey': 'version', 'command': 'ver'},
{'labelKey': 'advertise', 'command': 'advert'},
{'labelKey': 'clock', 'command': 'clock'}, {'labelKey': 'clock', 'command': 'clock'},
{'labelKey': 'clock sync', 'command': 'clock sync'},
]; ];
@override @override
@@ -407,6 +409,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
return l10n.repeater_cliQuickAdvertise; return l10n.repeater_cliQuickAdvertise;
case 'clock': case 'clock':
return l10n.repeater_cliQuickClock; return l10n.repeater_cliQuickClock;
case 'clock sync':
return l10n.repeater_cliQuickClockSync;
case 'discovery':
return l10n.repeater_cliQuickDiscovery;
default: default:
return key; return key;
} }
+25 -9
View File
@@ -14,12 +14,13 @@ class ContactExport {
final double lon; final double lon;
final String desc; final String desc;
final double? ele; final double? ele;
final String url;
ContactExport({ ContactExport({
required this.name, required this.name,
required this.lat, required this.lat,
required this.lon, required this.lon,
required this.desc, required this.desc,
required this.url,
this.ele, this.ele,
}); });
} }
@@ -40,6 +41,7 @@ class GpxExport {
String name, String name,
double lat, double lat,
double lon, double lon,
String url,
String desc, [ String desc, [
double? ele, double? ele,
]) { ]) {
@@ -50,55 +52,66 @@ class GpxExport {
lon: lon, lon: lon,
desc: desc.trim(), desc: desc.trim(),
ele: ele, ele: ele,
url: url,
), ),
); );
} }
void addRepeaters() { void addRepeaters() {
final contacts = _connector.contacts final contacts = _connector.allContacts.where(
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) (c) => c.type == advTypeRepeater || c.type == advTypeRoom,
.toList(); );
for (var contact in contacts) { for (var contact in contacts) {
if (contact.latitude == null || contact.longitude == null) { if (contact.latitude == null || contact.longitude == null) {
continue; continue;
} }
final url = contact.rawPacket != null
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
: "";
_addContact( _addContact(
contact.name, contact.name,
contact.latitude!, contact.latitude!,
contact.longitude!, contact.longitude!,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url,
); );
} }
} }
void addContacts() { void addContacts() {
final contacts = _connector.contacts final contacts = _connector.allContacts.where((c) => c.type == advTypeChat);
.where((c) => c.type == advTypeChat)
.toList();
for (var contact in contacts) { for (var contact in contacts) {
if (contact.latitude == null || contact.longitude == null) { if (contact.latitude == null || contact.longitude == null) {
continue; continue;
} }
final url = contact.rawPacket != null
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
: "";
_addContact( _addContact(
contact.name, contact.name,
contact.latitude!, contact.latitude!,
contact.longitude!, contact.longitude!,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url,
); );
} }
} }
void addAll() { void addAll() {
final contacts = _connector.contacts; final contacts = _connector.allContacts;
for (var contact in contacts.toList()) { for (var contact in contacts) {
if (contact.latitude == null || contact.longitude == null) { if (contact.latitude == null || contact.longitude == null) {
continue; continue;
} }
final url = contact.rawPacket != null
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
: "";
_addContact( _addContact(
contact.name, contact.name,
contact.latitude ?? 0.0, contact.latitude ?? 0.0,
contact.longitude ?? 0.0, contact.longitude ?? 0.0,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url,
); );
} }
} }
@@ -138,6 +151,9 @@ class GpxExport {
ele: c.ele, ele: c.ele,
name: c.name, name: c.name,
desc: c.desc, desc: c.desc,
extensions: {
"meshcore": {"url": c.url},
},
), ),
) )
.toList(); .toList();
+1 -1
View File
@@ -113,7 +113,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
messageBytes: responseBytes, messageBytes: responseBytes,
); );
final timeoutSeconds = (timeoutMs / 1000).ceil(); final timeoutSeconds = (timeoutMs / 1000).ceil();
final timeout = Duration(milliseconds: timeoutMs); final timeout = Duration(milliseconds: timeoutMs + 2000);
final selectionLabel = selection.useFlood final selectionLabel = selection.useFlood
? 'flood' ? 'flood'
: '${selection.hopCount} hops'; : '${selection.hopCount} hops';
+1 -1
View File
@@ -108,7 +108,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
messageBytes: responseBytes, messageBytes: responseBytes,
); );
final timeoutSeconds = (timeoutMs / 1000).ceil(); final timeoutSeconds = (timeoutMs / 1000).ceil();
final timeout = Duration(milliseconds: timeoutMs); final timeout = Duration(milliseconds: timeoutMs + 2000);
final selectionLabel = selection.useFlood final selectionLabel = selection.useFlood
? 'flood' ? 'flood'
: '${selection.hopCount} hops'; : '${selection.hopCount} hops';