allow disable repeater adverts

Adds checkbox to disable adverts and flood adverts

Also updates flood avert range to new max of 168 hours
This commit is contained in:
446564 2026-01-30 11:05:57 -08:00
parent 90ce46392a
commit ede3142d40

View file

@ -41,7 +41,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
// 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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
if (_fetchedSettings.isEmpty) return;
final appLog = Provider.of<AppDebugLogService>(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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
}
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<RepeaterSettingsScreen> {
void _applySettingResponse(String command, String response) {
final appLog = Provider.of<AppDebugLogService>(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<RepeaterSettingsScreen> {
// 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<RepeaterSettingsScreen> {
// 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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
// ),
// ),
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<RepeaterSettingsScreen> {
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;
}