From ede3142d40ed66cadf05c611468241576aebfbb1 Mon Sep 17 00:00:00 2001 From: 446564 Date: Fri, 30 Jan 2026 11:05:57 -0800 Subject: [PATCH 1/3] allow disable repeater adverts Adds checkbox to disable adverts and flood adverts Also updates flood avert range to new max of 168 hours --- lib/screens/repeater_settings_screen.dart | 241 ++++++++++++++++------ 1 file changed, 178 insertions(+), 63 deletions(-) diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index c757404..6c1e85f 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -41,7 +41,8 @@ class _RepeaterSettingsScreenState extends State { // Basic settings final TextEditingController _nameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); - final TextEditingController _guestPasswordController = TextEditingController(); + final TextEditingController _guestPasswordController = + TextEditingController(); // Radio settings final TextEditingController _freqController = TextEditingController(); @@ -60,7 +61,9 @@ class _RepeaterSettingsScreenState extends State { bool _privacyMode = false; // Advertisement settings + bool _advertEnable = true; int _advertInterval = 120; // minutes/2 + bool _floodAdvertEnable = true; int _floodAdvertInterval = 12; // hours int _privAdvertInterval = 60; // minutes @@ -146,7 +149,10 @@ class _RepeaterSettingsScreenState extends State { if (_fetchedSettings.isEmpty) return; final appLog = Provider.of(context, listen: false); - appLog.info('Updating UI with keys: ${_fetchedSettings.keys.toList()}', tag: 'RadioSettings'); + appLog.info( + 'Updating UI with keys: ${_fetchedSettings.keys.toList()}', + tag: 'RadioSettings', + ); setState(() { // Update name @@ -161,7 +167,10 @@ class _RepeaterSettingsScreenState extends State { final radioStr = _fetchedSettings['radio']!; appLog.info('Raw radio string: "$radioStr"', tag: 'RadioSettings'); final parts = radioStr.split(','); - appLog.info('Split into ${parts.length} parts: $parts', tag: 'RadioSettings'); + appLog.info( + 'Split into ${parts.length} parts: $parts', + tag: 'RadioSettings', + ); if (parts.isNotEmpty) { final freqText = parts[0].replaceAll(RegExp(r'[^0-9.]'), '').trim(); @@ -193,7 +202,10 @@ class _RepeaterSettingsScreenState extends State { appLog.info('CR text: "$crText"', tag: 'RadioSettings'); _codingRate = int.tryParse(crText) ?? _codingRate; } - appLog.info('Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate', tag: 'RadioSettings'); + appLog.info( + 'Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate', + tag: 'RadioSettings', + ); } if (_fetchedSettings.containsKey('tx')) { @@ -207,11 +219,17 @@ class _RepeaterSettingsScreenState extends State { } if (_fetchedSettings.containsKey('lat')) { - appLog.info('Setting lat to: "${_fetchedSettings['lat']}"', tag: 'RadioSettings'); + appLog.info( + 'Setting lat to: "${_fetchedSettings['lat']}"', + tag: 'RadioSettings', + ); _latController.text = _fetchedSettings['lat']!; } if (_fetchedSettings.containsKey('lon')) { - appLog.info('Setting lon to: "${_fetchedSettings['lon']}"', tag: 'RadioSettings'); + appLog.info( + 'Setting lon to: "${_fetchedSettings['lon']}"', + tag: 'RadioSettings', + ); _lonController.text = _fetchedSettings['lon']!; } @@ -268,7 +286,10 @@ class _RepeaterSettingsScreenState extends State { void _applySettingResponse(String command, String response) { final appLog = Provider.of(context, listen: false); - appLog.info('Command: "$command", Raw response: "$response"', tag: 'RadioSettings'); + appLog.info( + 'Command: "$command", Raw response: "$response"', + tag: 'RadioSettings', + ); final value = _extractCliValue(response); appLog.info('Extracted value: "$value"', tag: 'RadioSettings'); if (value == null) return; @@ -280,7 +301,10 @@ class _RepeaterSettingsScreenState extends State { // Validate response content matches expected format for the command // This prevents mismatched responses over LoRa where order isn't guaranteed if (!_validateResponseForCommand(key, value)) { - appLog.warn('Response "$value" does not match expected format for "$key", ignoring', tag: 'RadioSettings'); + appLog.warn( + 'Response "$value" does not match expected format for "$key", ignoring', + tag: 'RadioSettings', + ); return; } @@ -311,7 +335,9 @@ class _RepeaterSettingsScreenState extends State { // Must have at least 3 commas and start with a frequency-like number final parts = value.split(','); if (parts.length < 4) return false; - final freq = double.tryParse(parts[0].replaceAll(RegExp(r'[^0-9.]'), '')); + final freq = double.tryParse( + parts[0].replaceAll(RegExp(r'[^0-9.]'), ''), + ); // Frequency should be in reasonable LoRa range (300-2500 MHz) return freq != null && freq >= 300 && freq <= 2500; @@ -339,7 +365,16 @@ class _RepeaterSettingsScreenState extends State { case 'privacy': // Boolean values: on/off/true/false/1/0/enabled/disabled final lower = value.toLowerCase().trim(); - return ['on', 'off', 'true', 'false', '1', '0', 'enabled', 'disabled'].contains(lower); + return [ + 'on', + 'off', + 'true', + 'false', + '1', + '0', + 'enabled', + 'disabled', + ].contains(lower); case 'advert.interval': case 'flood.advert.interval': @@ -354,7 +389,8 @@ class _RepeaterSettingsScreenState extends State { if (value.isEmpty) return false; // If it has 3+ commas and looks like numbers, probably radio data final commaCount = ','.allMatches(value).length; - if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) return false; + if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) + return false; return true; default: @@ -551,7 +587,9 @@ class _RepeaterSettingsScreenState extends State { final freqMHz = double.tryParse(_freqController.text); if (freqMHz != null) { final bwKHz = _bandwidth! / 1000; - commands.add('set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate'); + commands.add( + 'set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate', + ); } } @@ -590,7 +628,9 @@ class _RepeaterSettingsScreenState extends State { timestampSeconds: timestampSeconds, ); await connector.sendFrame(frame); - await Future.delayed(const Duration(milliseconds: 200)); // Delay between commands + await Future.delayed( + const Duration(milliseconds: 200), + ); // Delay between commands } setState(() { @@ -614,7 +654,9 @@ class _RepeaterSettingsScreenState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.repeater_errorSavingSettings(e.toString())), + content: Text( + context.l10n.repeater_errorSavingSettings(e.toString()), + ), backgroundColor: Colors.red, ), ); @@ -699,7 +741,10 @@ class _RepeaterSettingsScreenState extends State { Text(l10n.repeater_settingsTitle), Text( repeater.name, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ), ), ], ), @@ -723,12 +768,20 @@ class _RepeaterSettingsScreenState extends State { value: 'auto', child: Row( children: [ - Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.auto_mode, + size: 20, + color: !isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( l10n.repeater_autoUseSavedPath, style: TextStyle( - fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: !isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -738,12 +791,20 @@ class _RepeaterSettingsScreenState extends State { value: 'flood', child: Row( children: [ - Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.waves, + size: 20, + color: isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( l10n.repeater_forceFloodMode, style: TextStyle( - fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -754,7 +815,8 @@ class _RepeaterSettingsScreenState extends State { IconButton( icon: const Icon(Icons.timeline), tooltip: l10n.repeater_pathManagement, - onPressed: () => PathManagementDialog.show(context, contact: repeater), + onPressed: () => + PathManagementDialog.show(context, contact: repeater), ), if (_hasChanges) TextButton.icon( @@ -865,7 +927,9 @@ class _RepeaterSettingsScreenState extends State { border: const OutlineInputBorder(), suffixText: 'MHz', ), - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), onChanged: (_) => _markChanged(), ), const SizedBox(height: 16), @@ -923,10 +987,7 @@ class _RepeaterSettingsScreenState extends State { border: const OutlineInputBorder(), ), items: _spreadingFactorOptions.map((sf) { - return DropdownMenuItem( - value: sf, - child: Text('SF$sf'), - ); + return DropdownMenuItem(value: sf, child: Text('SF$sf')); }).toList(), onChanged: (value) { if (value != null) { @@ -945,10 +1006,7 @@ class _RepeaterSettingsScreenState extends State { border: const OutlineInputBorder(), ), items: _codingRateOptions.map((cr) { - return DropdownMenuItem( - value: cr, - child: Text('4/$cr'), - ); + return DropdownMenuItem(value: cr, child: Text('4/$cr')); }).toList(), onChanged: (value) { if (value != null) { @@ -988,7 +1046,10 @@ class _RepeaterSettingsScreenState extends State { helperText: l10n.repeater_latitudeHelper, border: const OutlineInputBorder(), ), - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), onChanged: (_) => _markChanged(), ), const SizedBox(height: 16), @@ -999,7 +1060,10 @@ class _RepeaterSettingsScreenState extends State { helperText: l10n.repeater_longitudeHelper, border: const OutlineInputBorder(), ), - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), onChanged: (_) => _markChanged(), ), ], @@ -1018,11 +1082,17 @@ class _RepeaterSettingsScreenState extends State { children: [ Row( children: [ - Icon(Icons.toggle_on, color: Theme.of(context).textTheme.headlineSmall?.color), + Icon( + Icons.toggle_on, + color: Theme.of(context).textTheme.headlineSmall?.color, + ), const SizedBox(width: 8), Text( l10n.repeater_features, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ], ), @@ -1102,7 +1172,7 @@ class _RepeaterSettingsScreenState extends State { width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), - ) + ) : const Icon(Icons.refresh, size: 20), onPressed: isRefreshing ? null : onRefresh, tooltip: refreshTooltip, @@ -1130,40 +1200,72 @@ class _RepeaterSettingsScreenState extends State { const Divider(), ListTile( title: Text(l10n.repeater_localAdvertInterval), - subtitle: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)), - trailing: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)), + subtitle: Text( + l10n.repeater_localAdvertIntervalMinutes(_advertInterval), + ), + trailing: Checkbox( + value: _advertEnable, + onChanged: (value) { + setState(() { + _advertInterval = value! ? 60 : 0; + _advertEnable = value; + }); + _markChanged(); + }, + ), ), Slider( - value: _advertInterval.toDouble(), + value: _advertInterval == 0 + ? 60.toDouble() + : _advertInterval.toDouble(), min: 60, max: 240, divisions: 18, label: l10n.repeater_localAdvertIntervalMinutes(_advertInterval), - onChanged: (value) { - setState(() { - _advertInterval = value.toInt(); - }); - _markChanged(); - }, + onChanged: _advertEnable + ? (value) { + setState(() { + _advertInterval = value.toInt(); + }); + _markChanged(); + } + : null, ), const SizedBox(height: 16), ListTile( title: Text(l10n.repeater_floodAdvertInterval), - subtitle: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)), - trailing: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)), + subtitle: Text( + l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval), + ), + trailing: Checkbox( + value: _floodAdvertEnable, + onChanged: (value) { + setState(() { + _floodAdvertInterval = value! ? 3 : 0; + _floodAdvertEnable = value; + }); + _markChanged(); + }, + ), ), Slider( - value: _floodAdvertInterval.toDouble(), + value: _floodAdvertInterval == 0 + ? 3.toDouble() + : _floodAdvertInterval.toDouble(), min: 3, - max: 48, - divisions: 45, - label: l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval), - onChanged: (value) { - setState(() { - _floodAdvertInterval = value.toInt(); - }); - _markChanged(); - }, + max: 168, + divisions: 165, + label: l10n.repeater_floodAdvertIntervalHours( + _floodAdvertInterval, + ), + onChanged: _floodAdvertEnable + ? (value) { + setState(() { + _floodAdvertInterval = value.toInt(); + }); + _markChanged(); + } + : null, ), // Encrypted advertisement interval - hidden until privacy mode is implemented // if (_privacyMode) ...[ @@ -1220,10 +1322,15 @@ class _RepeaterSettingsScreenState extends State { const Divider(), ListTile( leading: Icon(Icons.refresh, color: colorScheme.onErrorContainer), - title: Text(l10n.repeater_rebootRepeater, style: TextStyle(color: colorScheme.onErrorContainer)), + title: Text( + l10n.repeater_rebootRepeater, + style: TextStyle(color: colorScheme.onErrorContainer), + ), subtitle: Text( l10n.repeater_rebootRepeaterSubtitle, - style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)), + style: TextStyle( + color: colorScheme.onErrorContainer.withValues(alpha: 0.8), + ), ), onTap: () => _confirmAction( l10n.repeater_rebootRepeater, @@ -1246,11 +1353,19 @@ class _RepeaterSettingsScreenState extends State { // ), // ), ListTile( - leading: Icon(Icons.delete_forever, color: colorScheme.onErrorContainer), - title: Text(l10n.repeater_eraseFileSystem, style: TextStyle(color: colorScheme.onErrorContainer)), + leading: Icon( + Icons.delete_forever, + color: colorScheme.onErrorContainer, + ), + title: Text( + l10n.repeater_eraseFileSystem, + style: TextStyle(color: colorScheme.onErrorContainer), + ), subtitle: Text( l10n.repeater_eraseFileSystemSubtitle, - style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)), + style: TextStyle( + color: colorScheme.onErrorContainer.withValues(alpha: 0.8), + ), ), onTap: () => _confirmAction( l10n.repeater_eraseFileSystem, @@ -1272,9 +1387,9 @@ class _RepeaterSettingsScreenState extends State { if (command == 'erase') { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.repeater_eraseSerialOnly)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly))); } return; } From 4f83d87f8c3fefc4775a0ae895760a03f237a0c3 Mon Sep 17 00:00:00 2001 From: 446564 Date: Sat, 31 Jan 2026 17:07:24 -0800 Subject: [PATCH 2/3] use switch for advert enable/disable move style to align with other toggles and use a switch instead of a checkbox --- lib/screens/repeater_settings_screen.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 6c1e85f..018caef 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -1203,11 +1203,11 @@ class _RepeaterSettingsScreenState extends State { subtitle: Text( l10n.repeater_localAdvertIntervalMinutes(_advertInterval), ), - trailing: Checkbox( + trailing: Switch( value: _advertEnable, onChanged: (value) { setState(() { - _advertInterval = value! ? 60 : 0; + _advertInterval = value ? 60 : 0; _advertEnable = value; }); _markChanged(); @@ -1237,11 +1237,11 @@ class _RepeaterSettingsScreenState extends State { subtitle: Text( l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval), ), - trailing: Checkbox( + trailing: Switch( value: _floodAdvertEnable, onChanged: (value) { setState(() { - _floodAdvertInterval = value! ? 3 : 0; + _floodAdvertInterval = value ? 3 : 0; _floodAdvertEnable = value; }); _markChanged(); From 818f514702011bc4ed5dd1c8a4b3619b45483953 Mon Sep 17 00:00:00 2001 From: Zach Date: Sun, 1 Feb 2026 17:08:53 -0700 Subject: [PATCH 3/3] The first issue was that the toggle switch states weren't being initialized when settings were refreshed from the device. The code would correctly update the interval values themselves, but failed to set the corresponding boolean flags that control whether the toggles appear as "on" or "off". This meant that if you refreshed settings from a device that had advertisements disabled (with an interval of zero), the toggles would incorrectly show as enabled even though the device was actually broadcasting no advertisements. We fixed this by adding two lines that explicitly set _advertEnable = _advertInterval > 0 and _floodAdvertEnable = _floodAdvertInterval > 0 after parsing the interval values from device responses. The second critical bug was in the validation logic that checks whether responses from the device contain valid data. The validator was rejecting any interval values of zero because it checked interval > 0, but zero is now a meaningful and valid value that indicates advertisements are disabled. Without this fix, any time a device reported back that advertisements were disabled, the app would silently discard that information as invalid, leaving the UI out of sync with reality. We changed the validation to use interval >= 0 instead and updated the comment to explicitly document that zero means disabled. The third fix was a minor code style issue where a single-line if statement was missing braces, causing a linter warning. This doesn't affect functionality but ensures the code meets project standards. --- lib/screens/repeater_settings_screen.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 018caef..bae0f50 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -248,12 +248,14 @@ class _RepeaterSettingsScreenState extends State { _fetchedSettings['advert.interval']!, _advertInterval, ); + _advertEnable = _advertInterval > 0; } if (_fetchedSettings.containsKey('flood.advert.interval')) { _floodAdvertInterval = _parseIntWithFallback( _fetchedSettings['flood.advert.interval']!, _floodAdvertInterval, ); + _floodAdvertEnable = _floodAdvertInterval > 0; } if (_fetchedSettings.containsKey('priv.advert.interval')) { _privAdvertInterval = _parseIntWithFallback( @@ -379,18 +381,19 @@ class _RepeaterSettingsScreenState extends State { case 'advert.interval': case 'flood.advert.interval': case 'priv.advert.interval': - // Interval: positive integer + // Interval: non-negative integer (0 means disabled) if (value.contains(',')) return false; final interval = int.tryParse(value.replaceAll(RegExp(r'[^0-9]'), '')); - return interval != null && interval > 0; + return interval != null && interval >= 0; case 'name': // Name: any non-empty string, but should NOT look like radio settings if (value.isEmpty) return false; // If it has 3+ commas and looks like numbers, probably radio data final commaCount = ','.allMatches(value).length; - if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) + if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) { return false; + } return true; default: