Merge pull request #497 from ericszimmermann/ez_zerohop_location_advert_PR

Zero-Hop Location advert
This commit is contained in:
zjs81
2026-07-04 00:31:52 -07:00
committed by GitHub
43 changed files with 390 additions and 9 deletions
+48
View File
@@ -216,6 +216,9 @@ class MeshCoreConnector extends ChangeNotifier {
DateTime _lastRadioRxTime = DateTime.fromMillisecondsSinceEpoch(0);
DateTime _lastContactMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0);
DateTime _lastChannelMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0);
DateTime _lastZeroHopAdvertAt = DateTime.fromMillisecondsSinceEpoch(0);
double? _lastZeroHopAdvertLatitude;
double? _lastZeroHopAdvertLongitude;
static const int _radioQuietMs = 3000;
static const int _radioQuietMaxWaitMs = 3000;
@@ -2523,6 +2526,8 @@ class MeshCoreConnector extends ChangeNotifier {
_selfName = null;
_selfLatitude = null;
_selfLongitude = null;
_lastZeroHopAdvertLatitude = null;
_lastZeroHopAdvertLongitude = null;
_awaitingSelfInfo = false;
_webInitialHandshakeRequestSent = false;
_selfInfoRetryTimer?.cancel();
@@ -2689,6 +2694,8 @@ class MeshCoreConnector extends ChangeNotifier {
_selfName = null;
_selfLatitude = null;
_selfLongitude = null;
_lastZeroHopAdvertLatitude = null;
_lastZeroHopAdvertLongitude = null;
_clientRepeat = null;
_rememberedNonRepeatRadioState = null;
_firmwareVerCode = null;
@@ -3797,6 +3804,11 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> sendSelfAdvert({bool flood = true}) async {
if (!isConnected) return;
await sendFrame(buildSendSelfAdvertFrame(flood: flood));
if (!flood) {
_lastZeroHopAdvertAt = DateTime.now();
_lastZeroHopAdvertLatitude = _selfLatitude;
_lastZeroHopAdvertLongitude = _selfLongitude;
}
}
Future<void> rebootDevice() async {
@@ -4260,6 +4272,42 @@ class MeshCoreConnector extends ChangeNotifier {
tag: 'Connector',
);
}
const locationChangeEpsilon = 2.25e-4; // ~25 meters in degrees.
final lastAdvertLatitude = _lastZeroHopAdvertLatitude;
final lastAdvertLongitude = _lastZeroHopAdvertLongitude;
final currentLatitude = _selfLatitude;
final currentLongitude = _selfLongitude;
final latChanged =
lastAdvertLatitude != null &&
currentLatitude != null &&
(currentLatitude - lastAdvertLatitude).abs() >= locationChangeEpsilon;
final lonChanged =
lastAdvertLongitude != null &&
currentLongitude != null &&
(currentLongitude - lastAdvertLongitude).abs() >= locationChangeEpsilon;
final gpsSampleChanged =
hasValidLocation(currentLatitude, currentLongitude) &&
(!hasValidLocation(lastAdvertLatitude, lastAdvertLongitude) ||
latChanged ||
lonChanged);
final effectiveGpsIntervalSeconds =
_appSettingsService?.resolvedGpsIntervalSeconds(_currentCustomVars) ??
0;
final timeSinceLastZeroHopAdvert = DateTime.now().difference(
_lastZeroHopAdvertAt,
);
final shouldAutoSendZeroHopAdvert =
(gpsSampleChanged || (_clientRepeat ?? false)) &&
_advertLocPolicy == 1 &&
(_appSettingsService?.settings.autoSendZeroHopAdvertOnGpsUpdate ??
false) &&
effectiveGpsIntervalSeconds > 0 &&
timeSinceLastZeroHopAdvert.inSeconds >= effectiveGpsIntervalSeconds;
if (shouldAutoSendZeroHopAdvert) {
unawaited(sendSelfAdvert(flood: false));
}
final selfName = _selfName?.trim();
if (_activeTransport == MeshCoreTransportType.usb &&
selfName != null &&
+2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Невалидна ширина или дължина.",
"settings_latitude": "Широчина",
"settings_longitude": "Дължина",
"settings_autoZeroHopAdvertOnGpsUpdate": "Автоматична zero-hop обява при GPS обновяване",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когато GPS местоположението се промени, изпрати zero-hop обява (изисква местоположение в обявата).",
"settings_privacyMode": "Режим на поверителност",
"settings_privacyModeSubtitle": "Скриване на име/местоположение в рекламите",
"settings_privacyModeToggle": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.",
+2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Ungültiger Breiten- oder Längengrad.",
"settings_latitude": "Breitengrad",
"settings_longitude": "Längengrad",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatische Zero-Hop-Ankündigung bei GPS-Update",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Wenn sich der GPS-Standort ändert, eine Zero-Hop-Ankündigung senden (erfordert Standort in der Ankündigung).",
"settings_privacyMode": "Privatsphärenmodus",
"settings_privacyModeSubtitle": "Name und Standort in Ankündigungen verbergen",
"settings_privacyModeToggle": "Privatsphärenmodus aktivieren, um Namen und Standort in Ankündigungen zu verbergen.",
+2
View File
@@ -206,6 +206,8 @@
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
"settings_advertLocation": "Advert Location",
"settings_advertLocationSubtitle": "Include location in advert.",
"settings_autoZeroHopAdvertOnGpsUpdate": "Auto Zero-Hop Advert On GPS Update",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "When GPS location changes, send a zero-hop advert (requires Advert Location).",
"settings_multiAck": "Multi-ACKs",
"settings_telemetryModeUpdated": "Telemetry mode updated",
"settings_actions": "Actions",
+2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Latitud o longitud inválidos.",
"settings_latitude": "Latitud",
"settings_longitude": "Longitud",
"settings_autoZeroHopAdvertOnGpsUpdate": "Anuncio de cero saltos automático al actualizar GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Cuando cambie la ubicación GPS, enviar un anuncio de cero saltos (requiere ubicación en anuncio).",
"settings_privacyMode": "Modo de privacidad",
"settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios",
"settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.",
+2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Latitude ou longitude invalide.",
"settings_latitude": "Latitude",
"settings_longitude": "Longitude",
"settings_autoZeroHopAdvertOnGpsUpdate": "Annonce zéro saut automatique lors de la mise à jour GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Lorsque la position GPS change, envoyer une annonce zéro saut (nécessite la position dans l'annonce).",
"settings_privacyMode": "Mode de confidentialité",
"settings_privacyModeSubtitle": "Masquer le nom et la localisation dans les annonces",
"settings_privacyModeToggle": "Activez le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.",
+2
View File
@@ -161,6 +161,8 @@
"settings_locationIntervalInvalid": "Az intervallumnak legalább 60 másodpercnek kell lennie, és kevesebbnek kell lennie 86 400 másodpercnél.",
"settings_latitude": "Szélesség",
"settings_longitude": "Hosszúság",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatikus zero-hop hirdetés GPS-frissítéskor",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Ha a GPS-helyzet megváltozik, küldjön zero-hop hirdetést (a hirdetésben helymegadás szükséges).",
"settings_contactSettings": "Kapcsolati beállítások",
"settings_contactSettingsSubtitle": "A névjegyek hozzáadásának beállításai.",
"settings_privacyMode": "Adatvédelmi mód",
+2
View File
@@ -2207,6 +2207,8 @@
"settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
"settings_advertLocation": "Posizione dell'annuncio",
"settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
"settings_autoZeroHopAdvertOnGpsUpdate": "Annuncio zero-hop automatico all'aggiornamento GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Quando la posizione GPS cambia, invia un annuncio zero-hop (richiede la posizione nell'annuncio).",
"settings_privacy": "Impostazioni sulla privacy",
"settings_denyAll": "Negare tutto",
"settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
+2
View File
@@ -167,6 +167,8 @@
"settings_locationIntervalInvalid": "間隔は少なくとも60秒で、86400秒未満でなければなりません。",
"settings_latitude": "緯度",
"settings_longitude": "経度",
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS更新時にゼロホップ広告を自動送信",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。",
"settings_contactSettings": "連絡先設定",
"settings_contactSettingsSubtitle": "連絡先の追加方法に関する設定",
"settings_privacyMode": "プライバシーモード",
+2
View File
@@ -161,6 +161,8 @@
"settings_locationIntervalInvalid": "간격은 최소 60초 이상, 86400초 미만이어야 합니다.",
"settings_latitude": "위도",
"settings_longitude": "경도",
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS 업데이트 시 제로 홉 광고 자동 전송",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).",
"settings_contactSettings": "연락처 설정",
"settings_contactSettingsSubtitle": "연락처 추가 방식 설정",
"settings_privacyMode": "개인 정보 보호 모드",
+12
View File
@@ -1030,6 +1030,18 @@ abstract class AppLocalizations {
/// **'Include location in advert.'**
String get settings_advertLocationSubtitle;
/// No description provided for @settings_autoZeroHopAdvertOnGpsUpdate.
///
/// In en, this message translates to:
/// **'Auto Zero-Hop Advert On GPS Update'**
String get settings_autoZeroHopAdvertOnGpsUpdate;
/// No description provided for @settings_autoZeroHopAdvertOnGpsUpdateSubtitle.
///
/// In en, this message translates to:
/// **'When GPS location changes, send a zero-hop advert (requires Advert Location).'**
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle;
/// No description provided for @settings_multiAck.
///
/// In en, this message translates to:
+8
View File
@@ -506,6 +506,14 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_advertLocationSubtitle =>
'Включи местоположение в обявата';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Автоматична zero-hop обява при GPS обновяване';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Когато GPS местоположението се промени, изпрати zero-hop обява (изисква местоположение в обявата).';
@override
String get settings_multiAck => 'Множество ACK';
+8
View File
@@ -506,6 +506,14 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_advertLocationSubtitle =>
'Standort in die Ankündigung einschließen.';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Automatische Zero-Hop-Ankündigung bei GPS-Update';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Wenn sich der GPS-Standort ändert, eine Zero-Hop-Ankündigung senden (erfordert Standort in der Ankündigung).';
@override
String get settings_multiAck => 'Mehrfach-ACKs';
+8
View File
@@ -496,6 +496,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_advertLocationSubtitle => 'Include location in advert.';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Auto Zero-Hop Advert On GPS Update';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'When GPS location changes, send a zero-hop advert (requires Advert Location).';
@override
String get settings_multiAck => 'Multi-ACKs';
+8
View File
@@ -503,6 +503,14 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Anuncio de cero saltos automático al actualizar GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Cuando cambie la ubicación GPS, enviar un anuncio de cero saltos (requiere ubicación en anuncio).';
@override
String get settings_multiAck => 'Múltiples respuestas de confirmación';
+8
View File
@@ -507,6 +507,14 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_advertLocationSubtitle =>
'Inclure la localisation dans l\'annonce';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Annonce zéro saut automatique lors de la mise à jour GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Lorsque la position GPS change, envoyer une annonce zéro saut (nécessite la position dans l\'annonce).';
@override
String get settings_multiAck => 'Plusieurs accusés de réception';
+8
View File
@@ -502,6 +502,14 @@ class AppLocalizationsHu extends AppLocalizations {
String get settings_advertLocationSubtitle =>
'A hirdetésben szerepeljen a helyszín.';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Automatikus zero-hop hirdetés GPS-frissítéskor';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Ha a GPS-helyzet megváltozik, küldjön zero-hop hirdetést (a hirdetésben helymegadás szükséges).';
@override
String get settings_multiAck => 'Multi-ACK';
+8
View File
@@ -507,6 +507,14 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_advertLocationSubtitle =>
'Includi la posizione nell\'annuncio';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Annuncio zero-hop automatico all\'aggiornamento GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Quando la posizione GPS cambia, invia un annuncio zero-hop (richiede la posizione nell\'annuncio).';
@override
String get settings_multiAck => 'ACK multipli';
+7
View File
@@ -482,6 +482,13 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get settings_advertLocationSubtitle => '広告に場所を記載してください。';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate => 'GPS更新時にゼロホップ広告を自動送信';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。';
@override
String get settings_multiAck => 'マルチ ACK';
+8
View File
@@ -483,6 +483,14 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'GPS 업데이트 시 제로 홉 광고 자동 전송';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).';
@override
String get settings_multiAck => '다중 ACK';
+8
View File
@@ -501,6 +501,14 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_advertLocationSubtitle =>
'Locatie opnemen in advertentie';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Automatische zero-hop-advertentie bij GPS-update';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Wanneer de GPS-locatie verandert, een zero-hop-advertentie verzenden (vereist locatie in advertentie).';
@override
String get settings_multiAck => 'Meerdere bevestigingen';
+8
View File
@@ -508,6 +508,14 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_advertLocationSubtitle =>
'Uwzględnij lokalizację w ogłoszeniu';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Automatyczne ogłoszenie zero-hop przy aktualizacji GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Gdy lokalizacja GPS się zmieni, wyślij ogłoszenie zero-hop (wymaga lokalizacji w ogłoszeniu).';
@override
String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru';
+8
View File
@@ -505,6 +505,14 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_advertLocationSubtitle =>
'Incluir localização no anúncio';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Anúncio zero-hop automático na atualização do GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Quando a localização GPS mudar, enviar um anúncio zero-hop (requer localização no anúncio).';
@override
String get settings_multiAck => 'Multi-ACKs';
+8
View File
@@ -505,6 +505,14 @@ class AppLocalizationsRu extends AppLocalizations {
String get settings_advertLocationSubtitle =>
'Включить местоположение в объявление';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Авто-объявление без хопов при обновлении GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).';
@override
String get settings_multiAck => 'Несколько подтверждений';
+8
View File
@@ -499,6 +499,14 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Automatický zero-hop inzerát pri aktualizácii GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Keď sa GPS poloha zmení, odoslať zero-hop inzerát (vyžaduje polohu v inzeráte).';
@override
String get settings_multiAck => 'Viaceré ACK';
+8
View File
@@ -500,6 +500,14 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Samodejni zero-hop oglas ob posodobitvi GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Ko se GPS lokacija spremeni, pošlji zero-hop oglas (zahteva lokacijo v oglasu).';
@override
String get settings_multiAck => 'Več potrdil';
+8
View File
@@ -497,6 +497,14 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Automatisk zero-hop-annons vid GPS-uppdatering';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'När GPS-positionen ändras, skicka en zero-hop-annons (kräver plats i annonsen).';
@override
String get settings_multiAck => 'Flera bekräftelser';
+8
View File
@@ -501,6 +501,14 @@ class AppLocalizationsUk extends AppLocalizations {
String get settings_advertLocationSubtitle =>
'Включити геопозицію в оголошення';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Автооголошення без хопів при оновленні GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).';
@override
String get settings_multiAck => 'Багато підтверджень';
+7
View File
@@ -476,6 +476,13 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settings_advertLocationSubtitle => '在广告中包含位置';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate => 'GPS 更新时自动发送零跳广告';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。';
@override
String get settings_multiAck => '多重ACK';
+2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.",
"settings_latitude": "Breedtegraad",
"settings_longitude": "Lengtegraad",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatische zero-hop-advertentie bij GPS-update",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Wanneer de GPS-locatie verandert, een zero-hop-advertentie verzenden (vereist locatie in advertentie).",
"settings_privacyMode": "Privacy-modus",
"settings_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
"settings_privacyModeToggle": "Schakel privacy modus in om je naam en locatie in advertenties te verbergen.",
+2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.",
"settings_latitude": "Szerokość",
"settings_longitude": "Długość",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatyczne ogłoszenie zero-hop przy aktualizacji GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Gdy lokalizacja GPS się zmieni, wyślij ogłoszenie zero-hop (wymaga lokalizacji w ogłoszeniu).",
"settings_privacyMode": "Tryb prywatności",
"settings_privacyModeSubtitle": "Ukryj imię/lokalizację w rozgłoszeniach",
"settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w rozgłoszeniach.",
+2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Latitude ou longitude inválidos.",
"settings_latitude": "Latitude",
"settings_longitude": "Longitude",
"settings_autoZeroHopAdvertOnGpsUpdate": "Anúncio zero-hop automático na atualização do GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Quando a localização GPS mudar, enviar um anúncio zero-hop (requer localização no anúncio).",
"settings_privacyMode": "Modo de Privacidade",
"settings_privacyModeSubtitle": "Esconder nome/localização em anúncios",
"settings_privacyModeToggle": "Ative o modo de privacidade para ocultar seu nome e localização em anúncios.",
+2
View File
@@ -77,6 +77,8 @@
"settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.",
"settings_latitude": "Широта",
"settings_longitude": "Долгота",
"settings_autoZeroHopAdvertOnGpsUpdate": "Авто-объявление без хопов при обновлении GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).",
"settings_privacyMode": "Режим конфиденциальности",
"settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании",
"settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.",
+2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Neplatná šírka alebo dĺžka.",
"settings_latitude": "Súradnica",
"settings_longitude": "Dĺžka",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatický zero-hop inzerát pri aktualizácii GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Keď sa GPS poloha zmení, odoslať zero-hop inzerát (vyžaduje polohu v inzeráte).",
"settings_privacyMode": "Režim ochrany súkromia",
"settings_privacyModeSubtitle": "Skryť meno/poloha v reklamách",
"settings_privacyModeToggle": "Prepínač súkromného režimu skryje vaše meno a polohu v reklamách.",
+2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.",
"settings_latitude": "Širina",
"settings_longitude": "Dolžina",
"settings_autoZeroHopAdvertOnGpsUpdate": "Samodejni zero-hop oglas ob posodobitvi GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Ko se GPS lokacija spremeni, pošlji zero-hop oglas (zahteva lokacijo v oglasu).",
"settings_privacyMode": "Zasebnost",
"settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih",
"settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.",
+2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Ogiltig latitud eller longitud.",
"settings_latitude": "Latitud",
"settings_longitude": "Längdgrad",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatisk zero-hop-annons vid GPS-uppdatering",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "När GPS-positionen ändras, skicka en zero-hop-annons (kräver plats i annonsen).",
"settings_privacyMode": "Privatläge",
"settings_privacyModeSubtitle": "Dölj namn/plats i annonser",
"settings_privacyModeToggle": "Aktivera privatläge för att dölja ditt namn och din plats i annonser.",
+2
View File
@@ -101,6 +101,8 @@
"settings_locationInvalid": "Некоректна широта або довгота.",
"settings_latitude": "Широта",
"settings_longitude": "Довгота",
"settings_autoZeroHopAdvertOnGpsUpdate": "Автооголошення без хопів при оновленні GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).",
"settings_privacyMode": "Режим приватності",
"settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
"settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.",
+2
View File
@@ -105,6 +105,8 @@
"settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。",
"settings_latitude": "纬度",
"settings_longitude": "经度",
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS 更新时自动发送零跳广告",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。",
"settings_privacyMode": "隐私模式",
"settings_privacyModeSubtitle": "在广告中隐藏姓名/位置",
"settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。",
+17
View File
@@ -95,6 +95,8 @@ class AppSettings {
final bool notifyOnNewMessage;
final bool notifyOnNewChannelMessage;
final bool notifyOnNewAdvert;
final bool autoSendZeroHopAdvertOnGpsUpdate;
final int gpsIntervalSeconds;
final bool autoRouteRotationEnabled;
final double maxRouteWeight;
final double initialRouteWeight;
@@ -149,6 +151,8 @@ class AppSettings {
this.notifyOnNewMessage = true,
this.notifyOnNewChannelMessage = true,
this.notifyOnNewAdvert = true,
this.autoSendZeroHopAdvertOnGpsUpdate = false,
this.gpsIntervalSeconds = 900,
this.autoRouteRotationEnabled = true,
this.maxRouteWeight = 5.0,
this.initialRouteWeight = 3.0,
@@ -210,6 +214,9 @@ class AppSettings {
'notify_on_new_message': notifyOnNewMessage,
'notify_on_new_channel_message': notifyOnNewChannelMessage,
'notify_on_new_advert': notifyOnNewAdvert,
'auto_send_zero_hop_advert_on_gps_update':
autoSendZeroHopAdvertOnGpsUpdate,
'gps_interval_seconds': gpsIntervalSeconds,
'auto_route_rotation_enabled': autoRouteRotationEnabled,
'max_route_weight': maxRouteWeight,
'initial_route_weight': initialRouteWeight,
@@ -275,6 +282,10 @@ class AppSettings {
notifyOnNewChannelMessage:
json['notify_on_new_channel_message'] as bool? ?? true,
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
autoSendZeroHopAdvertOnGpsUpdate:
json['auto_send_zero_hop_advert_on_gps_update'] as bool? ?? false,
gpsIntervalSeconds:
(json['gps_interval_seconds'] as num?)?.toInt() ?? 900,
autoRouteRotationEnabled:
json['auto_route_rotation_enabled'] as bool? ?? true,
maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
@@ -383,6 +394,8 @@ class AppSettings {
bool? notifyOnNewMessage,
bool? notifyOnNewChannelMessage,
bool? notifyOnNewAdvert,
bool? autoSendZeroHopAdvertOnGpsUpdate,
int? gpsIntervalSeconds,
bool? autoRouteRotationEnabled,
double? maxRouteWeight,
double? initialRouteWeight,
@@ -433,6 +446,10 @@ class AppSettings {
notifyOnNewChannelMessage:
notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage,
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
autoSendZeroHopAdvertOnGpsUpdate:
autoSendZeroHopAdvertOnGpsUpdate ??
this.autoSendZeroHopAdvertOnGpsUpdate,
gpsIntervalSeconds: gpsIntervalSeconds ?? this.gpsIntervalSeconds,
autoRouteRotationEnabled:
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight,
+30 -4
View File
@@ -9,6 +9,7 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/radio_settings.dart';
import '../services/app_settings_service.dart';
import '../services/app_debug_log_service.dart';
import '../theme/mesh_theme.dart';
import '../widgets/app_bar.dart';
@@ -761,6 +762,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _editLocation(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
final settingsService = context.read<AppSettingsService>();
final latController = TextEditingController();
final lonController = TextEditingController();
final intervalController = TextEditingController();
@@ -772,9 +774,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
final bool hasGPS = customVars.containsKey("gps");
bool isGPSEnabled = customVars["gps"] == "1";
// Read current interval or default to 900 (15 minutes)
final currentInterval =
int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
final currentInterval = settingsService.resolvedGpsIntervalSeconds(
customVars,
);
intervalController.text = currentInterval.toString();
String? intervalError;
@@ -872,7 +874,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Navigator.pop(context);
if (interval != null) {
await connector.setCustomVar("gps_interval:$interval");
await settingsService.setGpsIntervalSeconds(
interval,
writeToDevice: (value) =>
connector.setCustomVar("gps_interval:$value"),
);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
showDismissibleSnackBar(
@@ -1172,12 +1178,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _privacySettings(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
final settingsService = context.read<AppSettingsService>();
int telemetryMode = connector.telemetryModeBase;
int telemetryLocMode = connector.telemetryModeLoc;
int telemetryEnvMode = connector.telemetryModeEnv;
bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true;
int multiAcks = connector.multiAcks;
bool autoZeroHopAdvertOnGpsUpdate =
settingsService.settings.autoSendZeroHopAdvertOnGpsUpdate;
final telemModeBase = [
DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)),
@@ -1212,6 +1221,20 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
},
),
const SizedBox(height: 8),
FeatureToggleRow(
title: l10n.settings_autoZeroHopAdvertOnGpsUpdate,
subtitle: l10n.settings_autoZeroHopAdvertOnGpsUpdateSubtitle,
value: autoZeroHopAdvertOnGpsUpdate,
enabled: advertLocPolicy,
onChanged: advertLocPolicy
? (value) {
setDialogState(
() => autoZeroHopAdvertOnGpsUpdate = value,
);
}
: null,
),
const SizedBox(height: 8),
SwitchListTile(
title: Text(l10n.settings_multiAck),
value: multiAcks == 1,
@@ -1280,6 +1303,9 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
advertLocPolicy ? 1 : 0,
multiAcks,
);
await settingsService.setAutoSendZeroHopAdvertOnGpsUpdate(
autoZeroHopAdvertOnGpsUpdate,
);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
showDismissibleSnackBar(
+30
View File
@@ -13,6 +13,14 @@ class AppSettingsService extends ChangeNotifier {
AppSettings get settings => _settings;
int resolvedGpsIntervalSeconds(Map<String, String>? deviceCustomVars) {
final deviceValue = int.tryParse(deviceCustomVars?['gps_interval'] ?? '');
if (deviceValue != null && deviceValue >= 0) {
return deviceValue;
}
return _settings.gpsIntervalSeconds;
}
String batteryChemistryForDevice(String deviceId) {
final stored = _settings.batteryChemistryByDeviceId[deviceId];
if (stored == 'liion') return 'nmc';
@@ -128,6 +136,28 @@ class AppSettingsService extends ChangeNotifier {
await updateSettings(_settings.copyWith(notifyOnNewAdvert: value));
}
Future<void> setAutoSendZeroHopAdvertOnGpsUpdate(bool value) async {
await updateSettings(
_settings.copyWith(autoSendZeroHopAdvertOnGpsUpdate: value),
);
}
Future<void> setGpsIntervalSeconds(
int value, {
Future<void> Function(int value)? writeToDevice,
}) async {
await updateSettings(_settings.copyWith(gpsIntervalSeconds: value));
if (writeToDevice == null) return;
try {
await writeToDevice(value);
} catch (e) {
appLogger.warn(
'Failed to write GPS interval to device: $e',
tag: 'AppSettings',
);
}
}
Future<void> setAutoRouteRotationEnabled(bool value) async {
await updateSettings(_settings.copyWith(autoRouteRotationEnabled: value));
}
+24 -5
View File
@@ -4,6 +4,7 @@ class FeatureToggleRow extends StatefulWidget {
final String title;
final String subtitle;
final bool value;
final bool enabled;
final bool hasRefreshing;
final bool isRefreshing;
final ValueChanged<bool>? onChanged;
@@ -15,6 +16,7 @@ class FeatureToggleRow extends StatefulWidget {
required this.title,
required this.subtitle,
required this.value,
this.enabled = true,
this.hasRefreshing = false,
this.isRefreshing = false,
this.onChanged,
@@ -31,6 +33,13 @@ class _FeatureToggleRow extends State<FeatureToggleRow> {
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final isEnabled = widget.enabled;
final titleColor = isEnabled
? scheme.onSurface
: scheme.onSurfaceVariant.withValues(alpha: 0.7);
final subtitleColor = isEnabled
? scheme.onSurfaceVariant
: scheme.onSurfaceVariant.withValues(alpha: 0.55);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
@@ -44,14 +53,13 @@ class _FeatureToggleRow extends State<FeatureToggleRow> {
widget.title,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: titleColor,
),
),
const SizedBox(height: 2),
Text(
widget.subtitle,
style: textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
style: textTheme.bodySmall?.copyWith(color: subtitleColor),
),
],
),
@@ -60,7 +68,18 @@ class _FeatureToggleRow extends State<FeatureToggleRow> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Switch(value: widget.value, onChanged: widget.onChanged),
Switch(
value: widget.value,
onChanged: isEnabled ? widget.onChanged : null,
thumbColor: !isEnabled
? WidgetStatePropertyAll<Color?>(scheme.onSurfaceVariant)
: null,
trackColor: !isEnabled
? WidgetStatePropertyAll<Color?>(
scheme.onSurfaceVariant.withValues(alpha: 0.35),
)
: null,
),
if (widget.hasRefreshing) ...[
const SizedBox(width: 4),
widget.isRefreshing
@@ -74,7 +93,7 @@ class _FeatureToggleRow extends State<FeatureToggleRow> {
)
: IconButton(
icon: const Icon(Icons.refresh, size: 18),
onPressed: widget.onRefresh,
onPressed: isEnabled ? widget.onRefresh : null,
tooltip: widget.refreshTooltip,
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
+51
View File
@@ -5,6 +5,9 @@ import 'package:meshcore_open/models/contact.dart';
import 'package:meshcore_open/models/path_history.dart';
import 'package:meshcore_open/models/app_settings.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/services/app_settings_service.dart';
import 'package:meshcore_open/storage/prefs_manager.dart';
import 'package:shared_preferences/shared_preferences.dart';
// Builds a valid contact frame with the given pathLen and optional overrides.
// Frame layout: [respCode(1)][pubKey(32)][type(1)][flags(1)][pathLen(1)][path(64)][name(32)][timestamp(4)][lat(4)][lon(4)]
@@ -38,6 +41,14 @@ Uint8List _buildContactFrame({
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() async {
SharedPreferences.setMockInitialValues({});
PrefsManager.reset();
await PrefsManager.initialize();
});
group('Contact.fromFrame — pathLen mapping', () {
test('pathLen == 0 → pathLength == 0 (direct, NOT flood)', () {
final frame = _buildContactFrame(pathLen: 0);
@@ -358,4 +369,44 @@ void main() {
expect(updated.maxRouteWeight, equals(settings.maxRouteWeight));
});
});
group('AppSettingsService — gps interval fallback', () {
test('resolvedGpsIntervalSeconds prefers device custom var', () {
final service = AppSettingsService();
expect(
service.resolvedGpsIntervalSeconds(const {'gps_interval': '120'}),
equals(120),
);
});
test('resolvedGpsIntervalSeconds falls back to stored value', () async {
final service = AppSettingsService();
await service.updateSettings(AppSettings(gpsIntervalSeconds: 900));
expect(service.resolvedGpsIntervalSeconds(null), equals(900));
expect(
service.resolvedGpsIntervalSeconds(const {'gps_interval': 'bad'}),
equals(900),
);
});
test('resolvedGpsIntervalSeconds keeps an explicit device zero', () async {
final service = AppSettingsService();
await service.updateSettings(AppSettings(gpsIntervalSeconds: 900));
expect(
service.resolvedGpsIntervalSeconds(const {'gps_interval': '0'}),
equals(0),
);
});
test('toJson/fromJson preserves gpsIntervalSeconds', () {
final settings = AppSettings(gpsIntervalSeconds: 321);
final restored = AppSettings.fromJson(settings.toJson());
expect(restored.gpsIntervalSeconds, equals(321));
});
});
}