Fix radio preset resync

This commit is contained in:
Janez T
2026-04-08 18:43:12 +02:00
parent e6cd59e96f
commit d00198c829
2 changed files with 265 additions and 34 deletions

View File

@@ -219,6 +219,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
bool _autoDiscoverySettingsSaved = false;
bool _autoDiscoverySettingsDirty = false;
String? _lastAutoDiscoverySignature;
String? _lastRadioSettingsSignature;
String? _publicInfoError;
String? _radioSettingsError;
String? _autoDiscoverySettingsError;
@@ -226,6 +227,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
int _selectedSpreadingFactor = 8;
int _selectedCodingRate = 8;
_RadioPreset? _selectedRadioPreset;
bool _radioSettingsDirty = false;
final List<String> _bandwidthOptions = [
'7.8 kHz',
@@ -280,29 +282,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
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<DeviceConfigScreen> {
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<DeviceConfigScreen> {
}
void _markRadioSettingsDirty() {
_radioSettingsDirty = true;
if (_radioSettingsSaved || _radioSettingsError != null) {
setState(() {
_radioSettingsSaved = false;
@@ -472,15 +563,30 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
}
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<DeviceConfigScreen> {
if (mounted) {
setState(() {
_syncRadioSettingsState(connectionProvider.deviceInfo);
_isSavingRadioSettings = false;
_radioSettingsSaved = true;
});
@@ -1754,6 +1861,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
setState(() {
_selectedRadioPreset = null;
_showCustomRadioSettings = true;
_radioSettingsDirty = true;
_radioSettingsSaved = false;
_radioSettingsError = null;
});

View File

@@ -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<Map<String, String>> getCustomVars() async => const {};
@override
Future<void> getBatteryAndStorage() async {}
@override
Future<void> getAutoaddConfig() async {}
@override
Future<void> getAllowedRepeatFreq() async {}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
Future<void> _pumpDeviceConfigScreen(
WidgetTester tester, {
required ConnectionProvider connectionProvider,
}) async {
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider<ConnectionProvider>.value(
value: connectionProvider,
),
ChangeNotifierProvider<ChannelsProvider>(
create: (_) => ChannelsProvider()..initializePublicChannel(),
),
ChangeNotifierProvider<ContactsProvider>(
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);
},
);
}