diff --git a/lib/helpers/remote_radio_commands.dart b/lib/helpers/remote_radio_commands.dart new file mode 100644 index 00000000..06059e2e --- /dev/null +++ b/lib/helpers/remote_radio_commands.dart @@ -0,0 +1,102 @@ +import '../models/radio_preset.dart'; + +// Radio presets on remote nodes (repeater, room server, sensor) through the +// stock admin CLI (#733). Kept out of the screen so it can be tested. + +/// Default `tempradio` duration (owner decision on #662). +const int kTempRadioDefaultMinutes = 1440; + +/// Longest `tempradio` duration that works. Firmware schedules the revert at +/// `futureMillis(2000 + timeout_mins * 60 * 1000)` in a 32-bit int +/// (simple_repeater/MyMesh.cpp), which overflows past this. +const int kTempRadioMaxMinutes = 35791; + +String _bandwidthKHz(int bandwidthHz) => '${bandwidthHz / 1000}'; + +/// Stock CLI: persist radio params (applied after a reboot). +String setRadioCommand( + String frequencyText, + int bandwidthHz, + int spreadingFactor, + int codingRate, +) => + 'set radio $frequencyText,${_bandwidthKHz(bandwidthHz)},' + '$spreadingFactor,$codingRate'; + +/// Stock CLI: apply radio params now, reverting after [minutes]. Nothing is +/// saved on the node. Coding rate is the 4/x denominator (4/5 is 5), which +/// is what firmware range-checks (5-8). +String tempRadioCommand( + String frequencyText, + int bandwidthHz, + int spreadingFactor, + int codingRate, + int minutes, +) => + 'tempradio $frequencyText,${_bandwidthKHz(bandwidthHz)},' + '$spreadingFactor,$codingRate,$minutes'; + +/// A valid `tempradio` duration in minutes (1 to [kTempRadioMaxMinutes]), +/// or null. +int? parseTempRadioMinutes(String text) { + final minutes = int.tryParse(text.trim()); + if (minutes == null || minutes < 1 || minutes > kTempRadioMaxMinutes) { + return null; + } + return minutes; +} + +/// `tempradio` retunes the node 2 s after it replies, so anything sent after +/// it in the same save would go out on the old frequency. Keep the original +/// order otherwise, with retuning commands moved to the end. +List withRetuneLast(List commands, bool Function(T) retunes) => [ + ...commands.where((c) => !retunes(c)), + ...commands.where(retunes), +]; + +bool isRetuneCommand(String command) => command.startsWith('tempradio '); + +/// Stock CLI: path hash mode, zero-based (2 bytes = mode 1). +String setPathHashModeCommand(int mode) => 'set path.hash.mode $mode'; + +/// Whether a path hash that came from a preset goes out in this save (#735). +/// Firmware has no temporary path hash, so with a temporary radio change it +/// would outlive the revert; it stays pending for a normal save instead. A +/// path hash the user set by hand is always sent. +bool sendsPresetPathHash({ + required bool temporaryRadio, + required bool pathHashFromPreset, +}) => !(temporaryRadio && pathHashFromPreset); + +/// The preset whose radio values match the form's, or null for custom values. +/// The form holds frequency as text (the repeater reports e.g. "908.205017"), +/// so frequency matches to the nearest kHz. TX power is not compared: the +/// remote screen sets it separately. A preset with a path hash only matches +/// a node on that mode (#747): MeshCore's "Canada" and "USA" differ only there. +String? matchRemotePresetId( + List presets, { + required String frequencyText, + required int? bandwidthHz, + required int? spreadingFactor, + required int? codingRate, + required int pathHashMode, +}) { + final mhz = double.tryParse(frequencyText.trim()); + if (mhz == null || + bandwidthHz == null || + spreadingFactor == null || + codingRate == null) { + return null; + } + final khz = (mhz * 1000).round(); + for (final p in presets) { + if (p.frequencyKHz == khz && + p.bandwidth.hz == bandwidthHz && + p.spreadingFactor.value == spreadingFactor && + p.codingRate.value == codingRate && + presetPathHashMatches(p, pathHashMode + 1)) { + return p.id; + } + } + return null; +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ae9d4130..1134a965 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -541,6 +541,7 @@ } }, "contacts_manageRepeater": "Manage Repeater", + "contacts_manageSensor": "Manage Sensor", "contacts_manageRoom": "Manage Room Server", "contacts_roomLogin": "Room Server Login", "contacts_openChat": "Open Chat", @@ -1459,6 +1460,18 @@ "repeater_bandwidth": "Bandwidth", "repeater_spreadingFactor": "Spreading Factor", "repeater_codingRate": "Coding Rate", + "repeater_tempRadio": "Apply temporarily", + "repeater_tempRadioSubtitle": "Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.", + "repeater_tempRadioMinutes": "Duration (minutes)", + "repeater_tempRadioPathHashNote": "This preset's path hash size isn't applied with a temporary change: the node can't revert it on its own. It stays pending for a normal save.", + "repeater_tempRadioMinutesInvalid": "1 to {max} minutes", + "@repeater_tempRadioMinutesInvalid": { + "placeholders": { + "max": { + "type": "int" + } + } + }, "repeater_locationSettings": "Location Settings", "repeater_latitude": "Latitude", "repeater_latitudeHelper": "Decimal degrees (e.g., 37.7749)", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 57001df4..986178b4 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2182,6 +2182,12 @@ abstract class AppLocalizations { /// **'Manage Repeater'** String get contacts_manageRepeater; + /// No description provided for @contacts_manageSensor. + /// + /// In en, this message translates to: + /// **'Manage Sensor'** + String get contacts_manageSensor; + /// No description provided for @contacts_manageRoom. /// /// In en, this message translates to: @@ -4688,6 +4694,36 @@ abstract class AppLocalizations { /// **'Coding Rate'** String get repeater_codingRate; + /// No description provided for @repeater_tempRadio. + /// + /// In en, this message translates to: + /// **'Apply temporarily'** + String get repeater_tempRadio; + + /// No description provided for @repeater_tempRadioSubtitle. + /// + /// In en, this message translates to: + /// **'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'** + String get repeater_tempRadioSubtitle; + + /// No description provided for @repeater_tempRadioMinutes. + /// + /// In en, this message translates to: + /// **'Duration (minutes)'** + String get repeater_tempRadioMinutes; + + /// No description provided for @repeater_tempRadioPathHashNote. + /// + /// In en, this message translates to: + /// **'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'** + String get repeater_tempRadioPathHashNote; + + /// No description provided for @repeater_tempRadioMinutesInvalid. + /// + /// In en, this message translates to: + /// **'1 to {max} minutes'** + String repeater_tempRadioMinutesInvalid(int max); + /// No description provided for @repeater_locationSettings. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 784e4a3e..64b5d55e 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1176,6 +1176,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Управление на сървър за стая'; @@ -2654,6 +2657,25 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_codingRate => 'Такса за кодиране'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Настройки на местоположението'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 5b4d83e3..a15d7ac0 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1171,6 +1171,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_manageRepeater => 'Repeater verwalten'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Raum-Server verwalten'; @@ -2649,6 +2652,25 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_codingRate => 'Kodierungsrate'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Standort Einstellungen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index d52671e9..57677e4f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1155,6 +1155,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contacts_manageRepeater => 'Manage Repeater'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Manage Room Server'; @@ -2605,6 +2608,25 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_codingRate => 'Coding Rate'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Location Settings'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index a39262f2..4eec6a4b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1170,6 +1170,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contacts_manageRepeater => 'Gestionar Repetidor'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Gestionar Servidor de Habitación'; @@ -2646,6 +2649,25 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_codingRate => 'Tasa de Programación'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Configuración de Ubicación'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 40d259e3..ad6c704c 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1176,6 +1176,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_manageRepeater => 'Gérer le répéteur'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Gérer le Room Server'; @@ -2658,6 +2661,25 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_codingRate => 'Taux de codage (CR)'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Paramètres de localisation'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 62dc23ae..1a39cda4 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -1177,6 +1177,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get contacts_manageRepeater => 'Ellenőriző eszköz kezelése'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'A szobai szerver kezelése'; @@ -2662,6 +2665,25 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_codingRate => 'Kódolási sebesség'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Helyszínbeállítások'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 9953f25d..4505a900 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1172,6 +1172,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contacts_manageRepeater => 'Gestisci Ripetitore'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Gestisci Server Camera'; @@ -2650,6 +2653,25 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_codingRate => 'Tasso di Codifica'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Impostazioni Luogo'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 04572b59..c828620c 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1124,6 +1124,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get contacts_manageRepeater => 'リピーターの管理'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'ルームサーバーの管理'; @@ -2556,6 +2559,25 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_codingRate => 'コーディング速度'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => '場所設定'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 30753a94..94593ed4 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1119,6 +1119,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get contacts_manageRepeater => '리피터 관리'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => '방 서버 관리'; @@ -2553,6 +2556,25 @@ class AppLocalizationsKo extends AppLocalizations { @override String get repeater_codingRate => '코딩 속도'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => '위치 설정'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index c8007b6b..3bf04ea4 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1165,6 +1165,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contacts_manageRepeater => 'Beheer Repeater'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Beheer Ruimte Server'; @@ -2633,6 +2636,25 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_codingRate => 'Codeertarief'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Locatie-instellingen'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index f744df37..c3640e5f 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1182,6 +1182,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_manageRepeater => 'Zarządzaj przekaźnikiem'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju'; @@ -2664,6 +2667,25 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_codingRate => 'Współczynnik kodowania'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Ustawienia Lokalizacji'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 6d8b8db1..b6e9cfa1 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1172,6 +1172,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contacts_manageRepeater => 'Gerenciar Repetidor'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Gerenciar Servidor de Sala'; @@ -2644,6 +2647,25 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_codingRate => 'Taxa de Codificação'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Configurações de Localização'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index d851b30a..400c0d67 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1173,6 +1173,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contacts_manageRepeater => 'Управление репитером'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Управление сервером комнат'; @@ -2650,6 +2653,25 @@ class AppLocalizationsRu extends AppLocalizations { @override String get repeater_codingRate => 'Коэффициент кодирования'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Настройки местоположения'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 6c4b847c..badbffd8 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1162,6 +1162,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contacts_manageRepeater => 'Spravovať opakované zoznamy'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Spravovať server miestnosti'; @@ -2636,6 +2639,25 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_codingRate => 'Rýchlosť kódovania'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Nastavenia polohy'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 11c4fe94..56178ea9 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1161,6 +1161,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_manageRepeater => 'Upravljaj Ponovitve'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Upravljajte strežnik sobe'; @@ -2632,6 +2635,25 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_codingRate => 'Programska hitrost'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Nastavitve lokacije'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a1a7b3b8..4a37cf3d 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1156,6 +1156,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contacts_manageRepeater => 'Hantera Upprepare'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Hantera Rumserver'; @@ -2621,6 +2624,25 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_codingRate => 'Kodningsgrad'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Platsinställningar'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f51352f8..160e9216 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1168,6 +1168,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contacts_manageRepeater => 'Керувати ретранслятором'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => 'Керувати сервером кімнати'; @@ -2647,6 +2650,25 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_codingRate => 'Швидкість кодування'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => 'Налаштування геопозиції'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 485b30ee..5dba1640 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1105,6 +1105,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contacts_manageRepeater => '管理转发节点'; + @override + String get contacts_manageSensor => 'Manage Sensor'; + @override String get contacts_manageRoom => '管理房间服务器'; @@ -2512,6 +2515,25 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_codingRate => '编码速率'; + @override + String get repeater_tempRadio => 'Apply temporarily'; + + @override + String get repeater_tempRadioSubtitle => + 'Uses tempradio: the node switches now and goes back to its saved radio settings after the duration. Nothing is saved on the node.'; + + @override + String get repeater_tempRadioMinutes => 'Duration (minutes)'; + + @override + String get repeater_tempRadioPathHashNote => + 'This preset\'s path hash size isn\'t applied with a temporary change: the node can\'t revert it on its own. It stays pending for a normal save.'; + + @override + String repeater_tempRadioMinutesInvalid(int max) { + return '1 to $max minutes'; + } + @override String get repeater_locationSettings => '位置设置'; diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 379b29db..adc9899b 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1509,6 +1509,20 @@ class _ContactsScreenState extends State }, ), ] else ...[ + // Sensors run the same admin login and CLI as repeaters + // (stock SensorMesh), so they share the repeater admin hub (#745). + if (contact.type == advTypeSensor) + ListTile( + leading: const Icon( + Icons.sensors_outlined, + color: Colors.orange, + ), + title: Text(context.l10n.contacts_manageSensor), + onTap: () { + Navigator.pop(sheetContext); + _showRepeaterLogin(context, contact); + }, + ), if (contact.pathLength > 0) ListTile( leading: const Icon(Icons.radar, color: Colors.green), diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 8e5c8922..f1a87478 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -6,9 +6,13 @@ import '../l10n/l10n.dart'; import '../models/contact.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../models/radio_preset.dart'; +import '../services/radio_preset_service.dart'; import '../services/repeater_command_service.dart'; import '../services/storage_service.dart'; import '../widgets/path_management_dialog.dart'; +import '../widgets/radio_preset_picker.dart'; +import '../helpers/remote_radio_commands.dart'; import '../helpers/snack_bar_builder.dart'; class RepeaterSettingsScreen extends StatefulWidget { @@ -123,6 +127,12 @@ class _RepeaterSettingsScreenState extends State { int? _spreadingFactor; int? _codingRate; + /// Apply the radio params with `tempradio` (reverting after the duration) + /// instead of persisting them with `set radio` (#662). + bool _tempRadio = false; + final TextEditingController _tempRadioMinutesController = + TextEditingController(text: '$kTempRadioDefaultMinutes'); + // Location settings final TextEditingController _latController = TextEditingController(); final TextEditingController _lonController = TextEditingController(); @@ -150,6 +160,9 @@ class _RepeaterSettingsScreenState extends State { // Advanced int _pathHashMode = 0; // 0-2 + + /// The pending path hash came from a preset rather than the user (#735). + bool _pathHashFromPreset = false; final TextEditingController _txDelayController = TextEditingController(); final TextEditingController _directTxDelayController = TextEditingController(); @@ -185,8 +198,47 @@ class _RepeaterSettingsScreenState extends State { _commandService = RepeaterCommandService(connector); _setupMessageListener(); _loadSettings(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + Provider.of(context, listen: false).refreshIfStale(); + }); + } + + /// Fill the radio fields from a preset (#734). TX power is not part of a + /// remote preset: it is a separate command on these nodes. + void _applyPreset(RadioPreset preset) { + setState(() { + _freqController.text = preset.frequencyMHz.toStringAsFixed(3); + final bw = preset.bandwidth.hz; + if (!_bandwidthOptions.contains(bw)) { + _bandwidthOptions + ..add(bw) + ..sort(); + } + _bandwidth = bw; + _spreadingFactor = preset.spreadingFactor.value; + _codingRate = preset.codingRate.value; + final hashMode = preset.pathHashMode; + if (hashMode != null) { + _pathHashMode = hashMode; + _pathHashFromPreset = true; + } + }); + _markChanged(_SettingField.radio); + if (preset.pathHashMode != null) { + _markChanged(_SettingField.pathHashMode); + } } + String? _selectedPresetId(List presets) => matchRemotePresetId( + presets, + frequencyText: _freqController.text, + bandwidthHz: _bandwidth, + spreadingFactor: _spreadingFactor, + codingRate: _codingRate, + pathHashMode: _pathHashMode, + ); + @override void dispose() { _frameSubscription?.cancel(); @@ -196,6 +248,7 @@ class _RepeaterSettingsScreenState extends State { _guestPasswordController.dispose(); _freqController.dispose(); _txPowerController.dispose(); + _tempRadioMinutesController.dispose(); _latController.dispose(); _lonController.dispose(); _ownerInfoController.dispose(); @@ -709,6 +762,22 @@ class _RepeaterSettingsScreenState extends State { final connector = Provider.of(context, listen: false); final repeater = _resolveRepeater(connector); + final tempRadioMinutes = parseTempRadioMinutes( + _tempRadioMinutesController.text, + ); + if (_tempRadio && + _dirtyFields.contains(_SettingField.radio) && + tempRadioMinutes == null) { + showDismissibleSnackBar( + context, + content: Text( + context.l10n.repeater_tempRadioMinutesInvalid(kTempRadioMaxMinutes), + ), + backgroundColor: Colors.red, + ); + return; + } + setState(() { _isLoading = true; }); @@ -750,11 +819,22 @@ class _RepeaterSettingsScreenState extends State { _codingRate != null) { final freqText = _freqController.text.trim(); if (double.tryParse(freqText) != null) { - final bwKHz = _bandwidth! / 1000; pending.add(( field: _SettingField.radio, - command: - 'set radio $freqText,$bwKHz,$_spreadingFactor,$_codingRate', + command: _tempRadio + ? tempRadioCommand( + freqText, + _bandwidth!, + _spreadingFactor!, + _codingRate!, + tempRadioMinutes!, + ) + : setRadioCommand( + freqText, + _bandwidth!, + _spreadingFactor!, + _codingRate!, + ), )); } } @@ -846,10 +926,15 @@ class _RepeaterSettingsScreenState extends State { command: 'set owner.info $encoded', )); } - if (_dirtyFields.contains(_SettingField.pathHashMode)) { + final holdPresetPathHash = !sendsPresetPathHash( + temporaryRadio: _tempRadio, + pathHashFromPreset: _pathHashFromPreset, + ); + if (_dirtyFields.contains(_SettingField.pathHashMode) && + !holdPresetPathHash) { pending.add(( field: _SettingField.pathHashMode, - command: 'set path.hash.mode $_pathHashMode', + command: setPathHashModeCommand(_pathHashMode), )); } if (_dirtyFields.contains(_SettingField.txDelay) && @@ -893,7 +978,10 @@ class _RepeaterSettingsScreenState extends State { final retainDirty = <_SettingField>{}; var passwordsFailed = false; var rebootNeeded = false; - for (final entry in pending) { + for (final entry in withRetuneLast( + pending, + (e) => isRetuneCommand(e.command), + )) { var failed = false; try { final response = await _commandService!.sendCommand( @@ -924,6 +1012,15 @@ class _RepeaterSettingsScreenState extends State { await Future.delayed(const Duration(milliseconds: 200)); } + // A preset path hash held back from a temporary save stays pending, + // so a later normal save can still apply it. + if (holdPresetPathHash && + _dirtyFields.contains(_SettingField.pathHashMode)) { + retainDirty.add(_SettingField.pathHashMode); + } else if (!retainDirty.contains(_SettingField.pathHashMode)) { + _pathHashFromPreset = false; + } + // Only clear password fields if every password command succeeded, // otherwise the user keeps their typed value to retry. if (!passwordsFailed) { @@ -1249,6 +1346,20 @@ class _RepeaterSettingsScreenState extends State { onRefresh: _refreshRadioSettings, ), const Divider(), + Consumer( + builder: (context, service, _) { + final presets = service.presets + .where((p) => !p.offGrid) + .toList(); + return RadioPresetPicker( + service: service, + presets: presets, + selectedId: _selectedPresetId(presets), + onSelected: _applyPreset, + ); + }, + ), + const SizedBox(height: 8), TextField( controller: _freqController, decoration: InputDecoration( @@ -1347,6 +1458,43 @@ class _RepeaterSettingsScreenState extends State { } }, ), + CheckboxListTile( + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + title: Text(l10n.repeater_tempRadio), + subtitle: Text(l10n.repeater_tempRadioSubtitle), + value: _tempRadio, + onChanged: (value) { + setState(() => _tempRadio = value ?? false); + _markChanged(_SettingField.radio); + }, + ), + if (_tempRadio) + TextField( + controller: _tempRadioMinutesController, + decoration: InputDecoration( + labelText: l10n.repeater_tempRadioMinutes, + helperText: l10n.repeater_tempRadioMinutesInvalid( + kTempRadioMaxMinutes, + ), + border: const OutlineInputBorder(), + suffixText: 'min', + ), + keyboardType: TextInputType.number, + onChanged: (_) => _markChanged(_SettingField.radio), + ), + if (_tempRadio && + _pathHashFromPreset && + _dirtyFields.contains(_SettingField.pathHashMode)) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + l10n.repeater_tempRadioPathHashNote, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), const SizedBox(height: 8), _buildFeatureToggleRow( title: l10n.repeater_rxGain, @@ -1976,7 +2124,10 @@ class _RepeaterSettingsScreenState extends State { ], onChanged: (v) { if (v != null) { - setState(() => _pathHashMode = v); + setState(() { + _pathHashMode = v; + _pathHashFromPreset = false; + }); _markChanged(_SettingField.pathHashMode); } }, diff --git a/test/helpers/remote_radio_commands_test.dart b/test/helpers/remote_radio_commands_test.dart new file mode 100644 index 00000000..f11eadd4 --- /dev/null +++ b/test/helpers/remote_radio_commands_test.dart @@ -0,0 +1,256 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/helpers/remote_radio_commands.dart'; +import 'package:meshcore_open/models/radio_preset.dart'; +import 'package:meshcore_open/models/radio_settings.dart'; + +RadioPreset preset( + String id, { + double mhz = 908.205, + LoRaBandwidth bw = LoRaBandwidth.bw62_5, + LoRaSpreadingFactor sf = LoRaSpreadingFactor.sf9, + LoRaCodingRate cr = LoRaCodingRate.cr4_8, + int? tx, + int? hashBytes, +}) => RadioPreset( + id: id, + title: id, + region: 'USA', + frequencyMHz: mhz, + bandwidth: bw, + spreadingFactor: sf, + codingRate: cr, + source: RadioPresetSource.offband, + txPowerDbm: tx, + pathHashBytes: hashBytes, +); + +void main() { + group('remote radio commands (#662)', () { + test('set radio keeps its existing format', () { + expect( + setRadioCommand('908.205', 62500, 9, 8), + 'set radio 908.205,62.5,9,8', + ); + }); + + test('tempradio carries the values of the OKI-published command', () { + // docs.okimesh.org 500kHz-testing: tempradio 909.750,500,10,5,1440. + // "500.0" and "500" parse to the same bandwidth (firmware strtof, + // CommonCLI.cpp tempradio handler). + expect( + tempRadioCommand('909.750', 500000, 10, 5, kTempRadioDefaultMinutes), + 'tempradio 909.750,500.0,10,5,1440', + ); + }); + + test('coding rate goes out as the 4/x denominator', () { + expect(tempRadioCommand('910.525', 62500, 7, 5, 60), endsWith(',7,5,60')); + }); + + test( + 'the default duration is valid, so no error shows by default (#748)', + () { + expect( + parseTempRadioMinutes('$kTempRadioDefaultMinutes'), + kTempRadioDefaultMinutes, + ); + }, + ); + + test('duration must be 1 to the firmware overflow bound', () { + expect(parseTempRadioMinutes('1440'), 1440); + expect(parseTempRadioMinutes(' 1 '), 1); + expect(parseTempRadioMinutes('$kTempRadioMaxMinutes'), 35791); + expect(parseTempRadioMinutes('${kTempRadioMaxMinutes + 1}'), isNull); + expect(parseTempRadioMinutes('0'), isNull); + expect(parseTempRadioMinutes('-5'), isNull); + expect(parseTempRadioMinutes('1.5'), isNull); + expect(parseTempRadioMinutes(''), isNull); + // 2000 + 35791 min fits a 32-bit int; one more minute does not. + expect(2000 + kTempRadioMaxMinutes * 60 * 1000, lessThan(1 << 31)); + expect( + 2000 + (kTempRadioMaxMinutes + 1) * 60 * 1000, + greaterThan(1 << 31), + ); + }); + + test('tempradio is sent after every other command', () { + final ordered = withRetuneLast([ + 'set name X', + 'tempradio 909.75,500.0,10,5,1440', + 'set tx 20', + 'set repeat on', + ], isRetuneCommand); + expect(ordered, [ + 'set name X', + 'set tx 20', + 'set repeat on', + 'tempradio 909.75,500.0,10,5,1440', + ]); + }); + + test('without tempradio the order is unchanged', () { + final commands = [ + 'set name X', + 'set radio 908.205,62.5,9,8', + 'set tx 20', + ]; + expect(withRetuneLast(commands, isRetuneCommand), commands); + }); + }); + + group('preset path hash on remote nodes (#735)', () { + test('published bytes become the zero-based CLI mode', () { + final philly = preset('philly', hashBytes: 2); + final socal = preset('socal', hashBytes: 3); + expect( + setPathHashModeCommand(philly.pathHashMode!), + 'set path.hash.mode 1', + ); + expect( + setPathHashModeCommand(socal.pathHashMode!), + 'set path.hash.mode 2', + ); + expect(preset('usa').pathHashMode, isNull); + }); + + test('a preset path hash is held back from a temporary change', () { + expect( + sendsPresetPathHash(temporaryRadio: true, pathHashFromPreset: true), + isFalse, + ); + expect( + sendsPresetPathHash(temporaryRadio: false, pathHashFromPreset: true), + isTrue, + ); + }); + + test('a path hash set by hand is always sent', () { + expect( + sendsPresetPathHash(temporaryRadio: true, pathHashFromPreset: false), + isTrue, + ); + expect( + sendsPresetPathHash(temporaryRadio: false, pathHashFromPreset: false), + isTrue, + ); + }); + }); + + group('matchRemotePresetId (#734)', () { + final presets = [ + preset('arizona', tx: 20), + preset( + 'oki', + mhz: 909.75, + bw: LoRaBandwidth.bw500, + sf: LoRaSpreadingFactor.sf10, + cr: LoRaCodingRate.cr4_5, + ), + ]; + + test('matches the frequency a repeater reports, to the nearest kHz', () { + expect( + matchRemotePresetId( + presets, + frequencyText: '908.205017', + bandwidthHz: 62500, + spreadingFactor: 9, + codingRate: 8, + pathHashMode: 0, + ), + 'arizona', + ); + expect( + matchRemotePresetId( + presets, + frequencyText: '909.750', + bandwidthHz: 500000, + spreadingFactor: 10, + codingRate: 5, + pathHashMode: 0, + ), + 'oki', + ); + }); + + test('any differing value means custom (null)', () { + expect( + matchRemotePresetId( + presets, + frequencyText: '908.205', + bandwidthHz: 62500, + spreadingFactor: 9, + codingRate: 5, + pathHashMode: 0, + ), + isNull, + ); + expect( + matchRemotePresetId( + presets, + frequencyText: '908.300', + bandwidthHz: 62500, + spreadingFactor: 9, + codingRate: 8, + pathHashMode: 0, + ), + isNull, + ); + }); + + test('unknown or unparsable fields match nothing', () { + expect( + matchRemotePresetId( + presets, + frequencyText: '', + bandwidthHz: 62500, + spreadingFactor: 9, + codingRate: 8, + pathHashMode: 0, + ), + isNull, + ); + expect( + matchRemotePresetId( + presets, + frequencyText: '908.205', + bandwidthHz: null, + spreadingFactor: 9, + codingRate: 8, + pathHashMode: 0, + ), + isNull, + ); + }); + + test('a path hash preset only matches a node on that mode (#747)', () { + final usaPair = [ + preset( + 'canada', + mhz: 910.525, + sf: LoRaSpreadingFactor.sf7, + cr: LoRaCodingRate.cr4_5, + hashBytes: 3, + ), + preset( + 'usa', + mhz: 910.525, + sf: LoRaSpreadingFactor.sf7, + cr: LoRaCodingRate.cr4_5, + ), + ]; + String? match(int mode) => matchRemotePresetId( + usaPair, + frequencyText: '910.525', + bandwidthHz: 62500, + spreadingFactor: 7, + codingRate: 5, + pathHashMode: mode, + ); + expect(match(0), 'usa'); + expect(match(1), 'usa'); + expect(match(2), 'canada'); + }); + }); +} diff --git a/untranslated.json b/untranslated.json index e45cdf18..c21bd14b 100644 --- a/untranslated.json +++ b/untranslated.json @@ -55,6 +55,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -88,6 +89,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -276,6 +282,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -309,6 +316,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -497,6 +509,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -530,6 +543,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -718,6 +736,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -751,6 +770,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -939,6 +963,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -972,6 +997,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -1160,6 +1190,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -1193,6 +1224,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -1381,6 +1417,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -1414,6 +1451,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -1602,6 +1644,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -1635,6 +1678,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -1823,6 +1871,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -1856,6 +1905,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -2044,6 +2098,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -2077,6 +2132,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -2265,6 +2325,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -2298,6 +2359,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -2486,6 +2552,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -2519,6 +2586,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -2707,6 +2779,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -2740,6 +2813,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -2928,6 +3006,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -2961,6 +3040,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -3149,6 +3233,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -3182,6 +3267,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -3370,6 +3460,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -3403,6 +3494,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood", @@ -3591,6 +3687,7 @@ "appSettings_clock12h", "appSettings_clock24h", "contacts_searchSensors", + "contacts_manageSensor", "channels_notifications", "channels_notifyAll", "channels_notifyMentionsOnly", @@ -3624,6 +3721,11 @@ "map_alwaysShowNames", "path_suggestedRoute", "path_searchRepeaters", + "repeater_tempRadio", + "repeater_tempRadioSubtitle", + "repeater_tempRadioMinutes", + "repeater_tempRadioPathHashNote", + "repeater_tempRadioMinutesInvalid", "repeater_cliCommandTimeout", "channelPath_routeDirect", "channelPath_routeFlood",