diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 7dd550c4..ab581863 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -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 sendSelfAdvert({bool flood = true}) async { if (!isConnected) return; await sendFrame(buildSendSelfAdvertFrame(flood: flood)); + if (!flood) { + _lastZeroHopAdvertAt = DateTime.now(); + _lastZeroHopAdvertLatitude = _selfLatitude; + _lastZeroHopAdvertLongitude = _selfLongitude; + } } Future 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 && diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index c9923e75..4d1b0538 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -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": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index dc4c9dab..c005c712 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -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.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bcbe3005..637332fd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 303f8cb5..a24fc034 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -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.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2c90f639..705b279a 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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.", diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 044f1490..20ef8197 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -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", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2df230ae..71f8c0ab 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -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.", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 1c8b728f..ba3dc29a 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -167,6 +167,8 @@ "settings_locationIntervalInvalid": "間隔は少なくとも60秒で、86400秒未満でなければなりません。", "settings_latitude": "緯度", "settings_longitude": "経度", + "settings_autoZeroHopAdvertOnGpsUpdate": "GPS更新時にゼロホップ広告を自動送信", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。", "settings_contactSettings": "連絡先設定", "settings_contactSettingsSubtitle": "連絡先の追加方法に関する設定", "settings_privacyMode": "プライバシーモード", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 6ed6f9a4..809b19bb 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -161,6 +161,8 @@ "settings_locationIntervalInvalid": "간격은 최소 60초 이상, 86400초 미만이어야 합니다.", "settings_latitude": "위도", "settings_longitude": "경도", + "settings_autoZeroHopAdvertOnGpsUpdate": "GPS 업데이트 시 제로 홉 광고 자동 전송", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).", "settings_contactSettings": "연락처 설정", "settings_contactSettingsSubtitle": "연락처 추가 방식 설정", "settings_privacyMode": "개인 정보 보호 모드", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 08d7a0b6..e692313b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 1404277c..ee22d67b 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -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'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9e061241..432838c8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -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'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 08f45442..a0463e45 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -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'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index da9decb1..8bd9eb64 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -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'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 2f9205cf..5dacff6f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index d64268ed..480c8b0a 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -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'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 350e7b78..4b796fd6 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 53851ba3..bdcd9f58 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 3ddff59a..3fb3fa54 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -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'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index ade730a2..c925be1b 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 627b332b..4f4e55cb 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 0c9f67e2..cec4f35d 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 1ddc9c79..62fa5442 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -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 => 'Несколько подтверждений'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index bd16dc0f..9c2dffca 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -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'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 064391b7..42af93e9 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 1d1ec527..2621192c 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -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'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index ab02382b..39a4f3a7 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -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 => 'Багато підтверджень'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 6e0704bb..95f0c658 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -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'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 8eb7f331..f6f574de 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -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.", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d68df31d..7f03d14b 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -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.", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 43187cc9..8eb79a9d 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -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.", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 8a8f85aa..677339c5 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -77,6 +77,8 @@ "settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.", "settings_latitude": "Широта", "settings_longitude": "Долгота", + "settings_autoZeroHopAdvertOnGpsUpdate": "Авто-объявление без хопов при обновлении GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).", "settings_privacyMode": "Режим конфиденциальности", "settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании", "settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index f6852078..f2e8cb72 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -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.", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index e423ab67..6b2ea8cf 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -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.", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 5366d0e0..a25b9ec0 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -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.", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 8f82d3d6..0de853a7 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -101,6 +101,8 @@ "settings_locationInvalid": "Некоректна широта або довгота.", "settings_latitude": "Широта", "settings_longitude": "Довгота", + "settings_autoZeroHopAdvertOnGpsUpdate": "Автооголошення без хопів при оновленні GPS", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).", "settings_privacyMode": "Режим приватності", "settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях", "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index e7129d19..3f3a9d51 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -105,6 +105,8 @@ "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", "settings_latitude": "纬度", "settings_longitude": "经度", + "settings_autoZeroHopAdvertOnGpsUpdate": "GPS 更新时自动发送零跳广告", + "settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。", "settings_privacyMode": "隐私模式", "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", "settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。", diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 3fd0d05f..aa435f13 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -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, diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index f8ff3692..4e6ab594 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -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 { void _editLocation(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; + final settingsService = context.read(); final latController = TextEditingController(); final lonController = TextEditingController(); final intervalController = TextEditingController(); @@ -772,9 +774,9 @@ class _SettingsScreenState extends State { 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 { 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 { void _privacySettings(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; + final settingsService = context.read(); 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( diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index 88ff0f47..da1d5c6b 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -13,6 +13,14 @@ class AppSettingsService extends ChangeNotifier { AppSettings get settings => _settings; + int resolvedGpsIntervalSeconds(Map? 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 setAutoSendZeroHopAdvertOnGpsUpdate(bool value) async { + await updateSettings( + _settings.copyWith(autoSendZeroHopAdvertOnGpsUpdate: value), + ); + } + + Future setGpsIntervalSeconds( + int value, { + Future 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 setAutoRouteRotationEnabled(bool value) async { await updateSettings(_settings.copyWith(autoRouteRotationEnabled: value)); } diff --git a/lib/widgets/elements_ui.dart b/lib/widgets/elements_ui.dart index a4995c73..f77b90a6 100644 --- a/lib/widgets/elements_ui.dart +++ b/lib/widgets/elements_ui.dart @@ -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? 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 { 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 { 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 { Row( mainAxisSize: MainAxisSize.min, children: [ - Switch(value: widget.value, onChanged: widget.onChanged), + Switch( + value: widget.value, + onChanged: isEnabled ? widget.onChanged : null, + thumbColor: !isEnabled + ? WidgetStatePropertyAll(scheme.onSurfaceVariant) + : null, + trackColor: !isEnabled + ? WidgetStatePropertyAll( + scheme.onSurfaceVariant.withValues(alpha: 0.35), + ) + : null, + ), if (widget.hasRefreshing) ...[ const SizedBox(width: 4), widget.isRefreshing @@ -74,7 +93,7 @@ class _FeatureToggleRow extends State { ) : 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, diff --git a/test/models/model_changes_test.dart b/test/models/model_changes_test.dart index a80c7944..edf069ff 100644 --- a/test/models/model_changes_test.dart +++ b/test/models/model_changes_test.dart @@ -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)); + }); + }); }