From d00198c829a6ec28e7d5830ce1585949117de011 Mon Sep 17 00:00:00 2001 From: Janez T Date: Wed, 8 Apr 2026 18:43:12 +0200 Subject: [PATCH] Fix radio preset resync --- lib/screens/device_config_screen.dart | 176 ++++++++++++++++---- test/screens/device_config_screen_test.dart | 123 ++++++++++++++ 2 files changed, 265 insertions(+), 34 deletions(-) create mode 100644 test/screens/device_config_screen_test.dart diff --git a/lib/screens/device_config_screen.dart b/lib/screens/device_config_screen.dart index 153a406..e1ba8fa 100644 --- a/lib/screens/device_config_screen.dart +++ b/lib/screens/device_config_screen.dart @@ -219,6 +219,7 @@ class _DeviceConfigScreenState extends State { bool _autoDiscoverySettingsSaved = false; bool _autoDiscoverySettingsDirty = false; String? _lastAutoDiscoverySignature; + String? _lastRadioSettingsSignature; String? _publicInfoError; String? _radioSettingsError; String? _autoDiscoverySettingsError; @@ -226,6 +227,7 @@ class _DeviceConfigScreenState extends State { int _selectedSpreadingFactor = 8; int _selectedCodingRate = 8; _RadioPreset? _selectedRadioPreset; + bool _radioSettingsDirty = false; final List _bandwidthOptions = [ '7.8 kHz', @@ -280,29 +282,7 @@ class _DeviceConfigScreenState extends State { text: (deviceInfo.autoAddMaxHops ?? 0).toString(), ); - if (deviceInfo.radioBw != null && - deviceInfo.radioBw! >= 0 && - deviceInfo.radioBw! <= 9) { - _selectedBandwidth = _bandwidthFromValue(deviceInfo.radioBw!); - } - if (deviceInfo.radioSf != null && - deviceInfo.radioSf! >= 7 && - deviceInfo.radioSf! <= 12) { - _selectedSpreadingFactor = deviceInfo.radioSf!; - } - if (deviceInfo.radioCr != null && - deviceInfo.radioCr! >= 5 && - deviceInfo.radioCr! <= 8) { - _selectedCodingRate = deviceInfo.radioCr!; - } - - _selectedRadioPreset = _matchRadioPreset( - frequencyKhz: deviceInfo.radioFreq, - bandwidth: deviceInfo.radioBw, - spreadingFactor: deviceInfo.radioSf, - codingRate: deviceInfo.radioCr, - ); - _showCustomRadioSettings = _selectedRadioPreset == null; + _syncRadioSettingsState(deviceInfo); final telemetryModes = deviceInfo.telemetryModes; _baseTelemetryMode = telemetryModes != null ? telemetryModes & 0x03 : 0; @@ -381,30 +361,140 @@ class _DeviceConfigScreenState extends State { return _bandwidthOptions.indexOf(bw); } + int? _normalizeBandwidthValue(int? bw) { + if (bw == null) { + return null; + } + if (bw >= 0 && bw <= 9) { + return bw; + } + switch (bw) { + case 7800: + return 0; + case 10400: + return 1; + case 15600: + return 2; + case 20800: + return 3; + case 31250: + return 4; + case 41700: + return 5; + case 62500: + return 6; + case 125000: + return 7; + case 250000: + return 8; + case 500000: + return 9; + default: + return null; + } + } + + int? _normalizeSpreadingFactor(int? spreadingFactor) { + if (spreadingFactor == null || + spreadingFactor < 7 || + spreadingFactor > 12) { + return null; + } + return spreadingFactor; + } + + int? _normalizeCodingRate(int? codingRate) { + if (codingRate == null) { + return null; + } + if (codingRate >= 5 && codingRate <= 8) { + return codingRate; + } + if (codingRate >= 1 && codingRate <= 4) { + return codingRate + 4; + } + return null; + } + _RadioPreset? _matchRadioPreset({ int? frequencyKhz, int? bandwidth, int? spreadingFactor, int? codingRate, }) { + final normalizedBandwidth = _normalizeBandwidthValue(bandwidth); + final normalizedSpreadingFactor = _normalizeSpreadingFactor( + spreadingFactor, + ); + final normalizedCodingRate = _normalizeCodingRate(codingRate); + if (frequencyKhz == null || - bandwidth == null || - spreadingFactor == null || - codingRate == null) { + normalizedBandwidth == null || + normalizedSpreadingFactor == null || + normalizedCodingRate == null) { return null; } for (final preset in _radioPresets) { if (preset.frequencyKhz == frequencyKhz && - preset.bandwidth == bandwidth && - preset.spreadingFactor == spreadingFactor && - preset.codingRate == codingRate) { + preset.bandwidth == normalizedBandwidth && + preset.spreadingFactor == normalizedSpreadingFactor && + preset.codingRate == normalizedCodingRate) { return preset; } } return null; } + String _radioSettingsSignature(DeviceInfo deviceInfo) { + return [ + deviceInfo.radioFreq, + _normalizeBandwidthValue(deviceInfo.radioBw), + _normalizeSpreadingFactor(deviceInfo.radioSf), + _normalizeCodingRate(deviceInfo.radioCr), + deviceInfo.txPower, + deviceInfo.clientRepeat, + deviceInfo.pathHashMode, + ].join('|'); + } + + void _syncRadioSettingsState(DeviceInfo deviceInfo) { + final normalizedBandwidth = _normalizeBandwidthValue(deviceInfo.radioBw); + final normalizedSpreadingFactor = _normalizeSpreadingFactor( + deviceInfo.radioSf, + ); + final normalizedCodingRate = _normalizeCodingRate(deviceInfo.radioCr); + + if (deviceInfo.radioFreq != null) { + _freqController.text = (deviceInfo.radioFreq! / 1000).toStringAsFixed(3); + } + if (normalizedBandwidth != null) { + _selectedBandwidth = _bandwidthFromValue(normalizedBandwidth); + } + if (normalizedSpreadingFactor != null) { + _selectedSpreadingFactor = normalizedSpreadingFactor; + } + if (normalizedCodingRate != null) { + _selectedCodingRate = normalizedCodingRate; + } + if (deviceInfo.txPower != null) { + _txPowerController.text = deviceInfo.txPower!.toString(); + } + if (deviceInfo.clientRepeat != null) { + _repeatEnabled = deviceInfo.clientRepeat!; + } + _selectedPathHashMode = deviceInfo.pathHashMode; + _selectedRadioPreset = _matchRadioPreset( + frequencyKhz: deviceInfo.radioFreq, + bandwidth: deviceInfo.radioBw, + spreadingFactor: deviceInfo.radioSf, + codingRate: deviceInfo.radioCr, + ); + _showCustomRadioSettings = _selectedRadioPreset == null; + _radioSettingsDirty = false; + _lastRadioSettingsSignature = _radioSettingsSignature(deviceInfo); + } + void _applyRadioPreset(_RadioPreset preset) { setState(() { _selectedRadioPreset = preset; @@ -441,6 +531,7 @@ class _DeviceConfigScreenState extends State { } void _markRadioSettingsDirty() { + _radioSettingsDirty = true; if (_radioSettingsSaved || _radioSettingsError != null) { setState(() { _radioSettingsSaved = false; @@ -472,15 +563,30 @@ class _DeviceConfigScreenState extends State { } void _handleConnectionProviderChanged() { - if (!mounted || _autoDiscoverySettingsDirty) return; + if (!mounted) return; final deviceInfo = _connectionProvider.deviceInfo; - final nextSignature = _autoDiscoverySignature(deviceInfo); - if (nextSignature == _lastAutoDiscoverySignature) return; + final nextRadioSettingsSignature = _radioSettingsSignature(deviceInfo); + final nextAutoDiscoverySignature = _autoDiscoverySignature(deviceInfo); + final shouldSyncRadioSettings = + !_radioSettingsDirty && + nextRadioSettingsSignature != _lastRadioSettingsSignature; + final shouldSyncAutoDiscovery = + !_autoDiscoverySettingsDirty && + nextAutoDiscoverySignature != _lastAutoDiscoverySignature; + + if (!shouldSyncRadioSettings && !shouldSyncAutoDiscovery) { + return; + } - _lastAutoDiscoverySignature = nextSignature; setState(() { - _syncAutoDiscoveryState(deviceInfo); + if (shouldSyncRadioSettings) { + _syncRadioSettingsState(deviceInfo); + } + if (shouldSyncAutoDiscovery) { + _lastAutoDiscoverySignature = nextAutoDiscoverySignature; + _syncAutoDiscoveryState(deviceInfo); + } }); } @@ -722,6 +828,7 @@ class _DeviceConfigScreenState extends State { if (mounted) { setState(() { + _syncRadioSettingsState(connectionProvider.deviceInfo); _isSavingRadioSettings = false; _radioSettingsSaved = true; }); @@ -1754,6 +1861,7 @@ class _DeviceConfigScreenState extends State { setState(() { _selectedRadioPreset = null; _showCustomRadioSettings = true; + _radioSettingsDirty = true; _radioSettingsSaved = false; _radioSettingsError = null; }); diff --git a/test/screens/device_config_screen_test.dart b/test/screens/device_config_screen_test.dart new file mode 100644 index 0000000..e9e1761 --- /dev/null +++ b/test/screens/device_config_screen_test.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_sar_app/l10n/app_localizations.dart'; +import 'package:meshcore_sar_app/models/device_info.dart' as device_info; +import 'package:meshcore_sar_app/providers/channels_provider.dart'; +import 'package:meshcore_sar_app/providers/connection_provider.dart'; +import 'package:meshcore_sar_app/providers/contacts_provider.dart'; +import 'package:meshcore_sar_app/screens/device_config_screen.dart'; +import 'package:provider/provider.dart'; + +class _FakeConnectionProvider extends ChangeNotifier + implements ConnectionProvider { + _FakeConnectionProvider(this._deviceInfo); + + device_info.DeviceInfo _deviceInfo; + + @override + device_info.DeviceInfo get deviceInfo => _deviceInfo; + + void updateDeviceInfo(device_info.DeviceInfo nextDeviceInfo) { + _deviceInfo = nextDeviceInfo; + notifyListeners(); + } + + @override + Future> getCustomVars() async => const {}; + + @override + Future getBatteryAndStorage() async {} + + @override + Future getAutoaddConfig() async {} + + @override + Future getAllowedRepeatFreq() async {} + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +Future _pumpDeviceConfigScreen( + WidgetTester tester, { + required ConnectionProvider connectionProvider, +}) async { + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value( + value: connectionProvider, + ), + ChangeNotifierProvider( + create: (_) => ChannelsProvider()..initializePublicChannel(), + ), + ChangeNotifierProvider( + create: (_) => ContactsProvider(), + ), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const DeviceConfigScreen(), + ), + ), + ); + + await tester.pump(); +} + +void main() { + testWidgets( + 'matches a preset when bandwidth arrives as raw Hz', + (tester) async { + final connectionProvider = _FakeConnectionProvider( + device_info.DeviceInfo( + connectionState: device_info.ConnectionState.connected, + radioFreq: 869618, + radioBw: 62500, + radioSf: 8, + radioCr: 8, + ), + ); + + await _pumpDeviceConfigScreen( + tester, + connectionProvider: connectionProvider, + ); + + expect(find.byKey(const ValueKey('eu_uk_narrow')), findsOneWidget); + expect(find.text('EU/UK (Narrow)'), findsWidgets); + }, + ); + + testWidgets( + 're-syncs the preset dropdown when device info changes', + (tester) async { + final connectionProvider = _FakeConnectionProvider( + device_info.DeviceInfo( + connectionState: device_info.ConnectionState.connected, + ), + ); + + await _pumpDeviceConfigScreen( + tester, + connectionProvider: connectionProvider, + ); + + expect(find.byKey(const ValueKey('custom')), findsOneWidget); + + connectionProvider.updateDeviceInfo( + device_info.DeviceInfo( + connectionState: device_info.ConnectionState.connected, + radioFreq: 869618, + radioBw: 62500, + radioSf: 8, + radioCr: 8, + ), + ); + await tester.pump(); + + expect(find.byKey(const ValueKey('eu_uk_narrow')), findsOneWidget); + }, + ); +}