Merge pull request #275 from just-stuff-tm/enhancement/preset-offgrid-repeat-toggle

Enhancement/preset offgrid repeat toggle
This commit is contained in:
Ded
2026-04-08 10:10:08 -07:00
committed by GitHub
2 changed files with 419 additions and 18 deletions
+24
View File
@@ -104,6 +104,22 @@ class RepeaterBatterySnapshot {
}); });
} }
class MeshCoreRadioStateSnapshot {
final int freqHz;
final int bwHz;
final int sf;
final int cr;
final int txPowerDbm;
const MeshCoreRadioStateSnapshot({
required this.freqHz,
required this.bwHz,
required this.sf,
required this.cr,
required this.txPowerDbm,
});
}
class MeshCoreConnector extends ChangeNotifier { class MeshCoreConnector extends ChangeNotifier {
// Message windowing to limit memory usage // Message windowing to limit memory usage
static const int _messageWindowSize = 200; static const int _messageWindowSize = 200;
@@ -169,6 +185,7 @@ class MeshCoreConnector extends ChangeNotifier {
int? _currentSf; int? _currentSf;
int? _currentCr; int? _currentCr;
bool? _clientRepeat; bool? _clientRepeat;
MeshCoreRadioStateSnapshot? _rememberedNonRepeatRadioState;
int? _firmwareVerCode; int? _firmwareVerCode;
int _pathHashByteWidth = 1; int _pathHashByteWidth = 1;
CompanionRadioStats? _latestRadioStats; CompanionRadioStats? _latestRadioStats;
@@ -369,6 +386,8 @@ class MeshCoreConnector extends ChangeNotifier {
int? get currentBwHz => _currentBwHz; int? get currentBwHz => _currentBwHz;
int? get currentSf => _currentSf; int? get currentSf => _currentSf;
int? get currentCr => _currentCr; int? get currentCr => _currentCr;
MeshCoreRadioStateSnapshot? get rememberedNonRepeatRadioState =>
_rememberedNonRepeatRadioState;
bool? get autoAddUsers => _autoAddUsers; bool? get autoAddUsers => _autoAddUsers;
bool? get autoAddRepeaters => _autoAddRepeaters; bool? get autoAddRepeaters => _autoAddRepeaters;
bool? get autoAddRoomServers => _autoAddRoomServers; bool? get autoAddRoomServers => _autoAddRoomServers;
@@ -380,6 +399,10 @@ class MeshCoreConnector extends ChangeNotifier {
int get advertLocationPolicy => _advertLocPolicy; int get advertLocationPolicy => _advertLocPolicy;
int get multiAcks => _multiAcks; int get multiAcks => _multiAcks;
bool? get clientRepeat => _clientRepeat; bool? get clientRepeat => _clientRepeat;
void rememberNonRepeatRadioState(MeshCoreRadioStateSnapshot snapshot) {
_rememberedNonRepeatRadioState = snapshot;
}
int? get firmwareVerCode => _firmwareVerCode; int? get firmwareVerCode => _firmwareVerCode;
Map<String, String>? get currentCustomVars => _currentCustomVars; Map<String, String>? get currentCustomVars => _currentCustomVars;
int? get batteryMillivolts => _batteryMillivolts; int? get batteryMillivolts => _batteryMillivolts;
@@ -2278,6 +2301,7 @@ class MeshCoreConnector extends ChangeNotifier {
_selfLatitude = null; _selfLatitude = null;
_selfLongitude = null; _selfLongitude = null;
_clientRepeat = null; _clientRepeat = null;
_rememberedNonRepeatRadioState = null;
_firmwareVerCode = null; _firmwareVerCode = null;
_batteryMillivolts = null; _batteryMillivolts = null;
_repeaterBatterySnapshots.clear(); _repeaterBatterySnapshots.clear();
+395 -18
View File
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:meshcore_open/utils/gpx_export.dart'; import 'package:meshcore_open/utils/gpx_export.dart';
import 'package:meshcore_open/widgets/elements_ui.dart'; import 'package:meshcore_open/widgets/elements_ui.dart';
@@ -8,12 +9,28 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/radio_settings.dart'; import '../models/radio_settings.dart';
import '../services/app_debug_log_service.dart';
import '../widgets/app_bar.dart'; import '../widgets/app_bar.dart';
import 'app_settings_screen.dart'; import 'app_settings_screen.dart';
import 'app_debug_log_screen.dart'; import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart'; import 'ble_debug_log_screen.dart';
import '../widgets/radio_stats_entry.dart'; import '../widgets/radio_stats_entry.dart';
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
/// to the UI enum range (always 5-8).
int _toUiCodingRate(int deviceCr) {
return deviceCr <= 4 ? deviceCr + 4 : deviceCr;
}
/// Convert UI coding-rate value (5-8) back to firmware encoding.
/// Uses the current device CR to detect which encoding the firmware expects.
int _toDeviceCodingRate(int uiCr, int? deviceCr) {
if (deviceCr != null && deviceCr <= 4) {
return uiCr - 4;
}
return uiCr;
}
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@@ -1088,6 +1105,11 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5; LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
final _txPowerController = TextEditingController(text: '20'); final _txPowerController = TextEditingController(text: '20');
bool _clientRepeat = false; bool _clientRepeat = false;
int? _selectedPresetIndex;
_RadioSettingsSnapshot? _lastNonRepeatSnapshot;
AppDebugLogService get _appLog =>
Provider.of<AppDebugLogService>(context, listen: false);
@override @override
void initState() { void initState() {
@@ -1139,6 +1161,22 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
} }
_clientRepeat = widget.connector.clientRepeat ?? false; _clientRepeat = widget.connector.clientRepeat ?? false;
_selectedPresetIndex = _findMatchingPresetIndex();
_lastNonRepeatSnapshot = _currentSnapshot();
if (_clientRepeat) {
_lastNonRepeatSnapshot =
_sessionRememberedNonRepeatSnapshot() ??
_inferNonRepeatSnapshotForRepeatEnabled();
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
_lastNonRepeatSnapshot!,
);
} else {
_lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_logRadioSettingsState('Dialog initialized');
});
} }
@override @override
@@ -1148,14 +1186,223 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
super.dispose(); super.dispose();
} }
void _applyPreset(RadioSettings preset) { void _applyPreset(int index) {
setState(() { setState(() {
_frequencyController.text = preset.frequencyMHz.toString(); _applyPresetState(index);
_bandwidth = preset.bandwidth;
_spreadingFactor = preset.spreadingFactor;
_codingRate = preset.codingRate;
_txPowerController.text = preset.txPowerDbm.toString();
}); });
_logRadioSettingsState(
'Applied preset ${RadioSettings.presets[index].$1} (#$index)',
);
}
int? _findMatchingPresetIndex() {
return _findMatchingPresetIndexForSnapshot(_currentSnapshot());
}
int? _findMatchingPresetIndexForSnapshot(_RadioSettingsSnapshot snapshot) {
for (final i in _visiblePresetIndexes()) {
final preset = RadioSettings.presets[i].$2;
if (preset.frequencyHz == snapshot.frequencyHz &&
preset.bandwidth == snapshot.bandwidth &&
preset.spreadingFactor == snapshot.spreadingFactor &&
preset.codingRate == snapshot.codingRate &&
preset.txPowerDbm == snapshot.txPowerDbm) {
return i;
}
}
return null;
}
Iterable<int> _visiblePresetIndexes() sync* {
for (var i = 0; i < RadioSettings.presets.length; i++) {
if (_isOffGridPresetIndex(i)) {
continue;
}
yield i;
}
}
_RadioSettingsSnapshot _currentSnapshot() {
final frequencyMHz = double.tryParse(_frequencyController.text) ?? 915.0;
final txPowerDbm = int.tryParse(_txPowerController.text) ?? 20;
return _RadioSettingsSnapshot(
frequencyMHz: frequencyMHz,
bandwidth: _bandwidth,
spreadingFactor: _spreadingFactor,
codingRate: _codingRate,
txPowerDbm: txPowerDbm,
);
}
bool _isOffGridPresetIndex(int? index) {
if (index == null) return false;
return RadioSettings.presets[index].$1.startsWith('Off-Grid ');
}
double _offGridFrequencyForBaseFrequency(double baseFrequencyMHz) {
if (baseFrequencyMHz < 500) return 433.0;
if (baseFrequencyMHz < 900) return 869.0;
return 918.0;
}
double _normalFrequencyForBand(double frequencyMHz) {
if (frequencyMHz < 500) return 433.650;
if (frequencyMHz < 900) return 869.432;
return 915.8;
}
_RadioSettingsSnapshot _fallbackNonRepeatSnapshot(
double currentFrequencyMHz,
) {
return _RadioSettingsSnapshot(
frequencyMHz: _normalFrequencyForBand(currentFrequencyMHz),
bandwidth: _bandwidth,
spreadingFactor: _spreadingFactor,
codingRate: _codingRate,
txPowerDbm: int.tryParse(_txPowerController.text) ?? 20,
);
}
_RadioSettingsSnapshot _nonRepeatSnapshotForCurrentSelection() {
final current = _currentSnapshot();
if (!_isOffGridPresetIndex(_selectedPresetIndex)) {
return current;
}
return _fallbackNonRepeatSnapshot(current.frequencyMHz);
}
_RadioSettingsSnapshot? _sessionRememberedNonRepeatSnapshot() {
final snapshot = widget.connector.rememberedNonRepeatRadioState;
if (snapshot == null) return null;
return _RadioSettingsSnapshot.fromMeshCoreSnapshot(snapshot);
}
_RadioSettingsSnapshot _inferNonRepeatSnapshotForRepeatEnabled() {
final current = _currentSnapshot();
for (final i in _visiblePresetIndexes()) {
final preset = RadioSettings.presets[i].$2;
final offGridFreqHz =
(_offGridFrequencyForBaseFrequency(preset.frequencyMHz) * 1000)
.round();
if (offGridFreqHz == current.frequencyHz &&
preset.bandwidth == current.bandwidth &&
preset.spreadingFactor == current.spreadingFactor &&
preset.codingRate == current.codingRate &&
preset.txPowerDbm == current.txPowerDbm) {
return _RadioSettingsSnapshot(
frequencyMHz: preset.frequencyMHz,
bandwidth: preset.bandwidth,
spreadingFactor: preset.spreadingFactor,
codingRate: preset.codingRate,
txPowerDbm: preset.txPowerDbm,
);
}
}
return _fallbackNonRepeatSnapshot(current.frequencyMHz);
}
void _applySnapshot(_RadioSettingsSnapshot snapshot) {
_frequencyController.text = snapshot.frequencyMHz.toStringAsFixed(3);
_bandwidth = snapshot.bandwidth;
_spreadingFactor = snapshot.spreadingFactor;
_codingRate = snapshot.codingRate;
_txPowerController.text = snapshot.txPowerDbm.toString();
}
void _applyPresetState(int index) {
final preset = RadioSettings.presets[index].$2;
final baseSnapshot = _RadioSettingsSnapshot(
frequencyMHz: preset.frequencyMHz,
bandwidth: preset.bandwidth,
spreadingFactor: preset.spreadingFactor,
codingRate: preset.codingRate,
txPowerDbm: preset.txPowerDbm,
);
final frequencyMHz = _clientRepeat
? _offGridFrequencyForBaseFrequency(baseSnapshot.frequencyMHz)
: baseSnapshot.frequencyMHz;
_frequencyController.text = frequencyMHz.toString();
_bandwidth = preset.bandwidth;
_spreadingFactor = preset.spreadingFactor;
_codingRate = preset.codingRate;
_txPowerController.text = preset.txPowerDbm.toString();
_selectedPresetIndex = index;
_lastNonRepeatSnapshot = baseSnapshot;
}
void _syncPresetSelection() {
final previousPresetIndex = _selectedPresetIndex;
final previousLastNonRepeat = _lastNonRepeatSnapshot;
if (_clientRepeat) {
final baseSnapshot =
previousLastNonRepeat ?? _inferNonRepeatSnapshotForRepeatEnabled();
if (_bandwidth != baseSnapshot.bandwidth ||
_spreadingFactor != baseSnapshot.spreadingFactor ||
_codingRate != baseSnapshot.codingRate ||
(int.tryParse(_txPowerController.text) ?? 20) !=
baseSnapshot.txPowerDbm) {
_lastNonRepeatSnapshot = _RadioSettingsSnapshot(
frequencyMHz: baseSnapshot.frequencyMHz,
bandwidth: _bandwidth,
spreadingFactor: _spreadingFactor,
codingRate: _codingRate,
txPowerDbm: int.tryParse(_txPowerController.text) ?? 20,
);
}
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
_lastNonRepeatSnapshot ?? baseSnapshot,
);
if (previousPresetIndex != _selectedPresetIndex ||
previousLastNonRepeat != _lastNonRepeatSnapshot) {
_logRadioSettingsState(
'Preset match updated while repeat enabled: ${_presetLabel(previousPresetIndex)} -> ${_presetLabel(_selectedPresetIndex)}',
);
}
return;
}
_lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection();
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
_lastNonRepeatSnapshot!,
);
if (previousPresetIndex != _selectedPresetIndex ||
previousLastNonRepeat != _lastNonRepeatSnapshot) {
_logRadioSettingsState(
'Preset sync updated state from ${_presetLabel(previousPresetIndex)} to ${_presetLabel(_selectedPresetIndex)}',
);
}
}
void _handleManualSettingsChanged(String source) {
_logRadioSettingsState('Manual settings edit: $source');
setState(_syncPresetSelection);
}
void _handleClientRepeatChanged(bool enabled) {
_logRadioSettingsState(
'Off-grid repeat toggle requested: $_clientRepeat -> $enabled',
);
setState(() {
final currentSnapshot = _currentSnapshot();
if (enabled) {
if (!_clientRepeat) {
_syncPresetSelection();
}
final baseSnapshot = _lastNonRepeatSnapshot ?? currentSnapshot;
_clientRepeat = true;
_frequencyController.text = _offGridFrequencyForBaseFrequency(
baseSnapshot.frequencyMHz,
).toStringAsFixed(3);
return;
}
_clientRepeat = false;
_applySnapshot(
_lastNonRepeatSnapshot ??
_fallbackNonRepeatSnapshot(currentSnapshot.frequencyMHz),
);
_syncPresetSelection();
});
_logRadioSettingsState('Off-grid repeat toggle applied');
} }
Future<void> _saveSettings() async { Future<void> _saveSettings() async {
@@ -1203,6 +1450,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
} }
try { try {
_logRadioSettingsState('Saving radio settings');
await widget.connector.sendFrame( await widget.connector.sendFrame(
buildSetRadioParamsFrame( buildSetRadioParamsFrame(
freqHz, freqHz,
@@ -1214,13 +1462,23 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
); );
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
await widget.connector.refreshDeviceInfo(); await widget.connector.refreshDeviceInfo();
final rememberedSnapshot = _clientRepeat
? _lastNonRepeatSnapshot
: _currentSnapshot();
if (rememberedSnapshot != null) {
widget.connector.rememberNonRepeatRadioState(
rememberedSnapshot.toMeshCoreSnapshot(widget.connector.currentCr),
);
}
if (!mounted) return; if (!mounted) return;
Navigator.pop(context); Navigator.pop(context);
_logRadioSettingsState('Radio settings saved successfully');
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_radioSettingsUpdated)), SnackBar(content: Text(l10n.settings_radioSettingsUpdated)),
); );
} catch (e) { } catch (e) {
_appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings');
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_error(e.toString()))), SnackBar(content: Text(l10n.settings_error(e.toString()))),
@@ -1228,15 +1486,38 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
} }
} }
int _toUiCodingRate(int deviceCr) { String _presetLabel(int? index) {
return deviceCr <= 4 ? deviceCr + 4 : deviceCr; if (index == null) {
return 'custom';
}
return '${RadioSettings.presets[index].$1} (#$index)';
} }
int _toDeviceCodingRate(int uiCr, int? deviceCr) { String _formatSnapshot(_RadioSettingsSnapshot? snapshot) {
if (deviceCr != null && deviceCr <= 4) { if (snapshot == null) {
return uiCr - 4; return 'null';
} }
return uiCr; return '${snapshot.frequencyMHz.toStringAsFixed(3)}MHz/'
'${snapshot.bandwidth.label}/'
'${snapshot.spreadingFactor.label}/'
'${snapshot.codingRate.label}/'
'${snapshot.txPowerDbm}dBm';
}
void _logRadioSettingsState(String message) {
if (!kDebugMode) return;
_appLog.info(
'$message | '
'freq=${_frequencyController.text}MHz '
'bw=${_bandwidth.label} '
'sf=${_spreadingFactor.label} '
'cr=${_codingRate.label} '
'tx=${_txPowerController.text}dBm '
'repeat=$_clientRepeat '
'preset=${_presetLabel(_selectedPresetIndex)} '
'lastNonRepeat=${_formatSnapshot(_lastNonRepeatSnapshot)}',
tag: 'RadioSettings',
);
} }
@override @override
@@ -1250,12 +1531,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
DropdownButtonFormField<int>( DropdownButtonFormField<int>(
key: ValueKey<int?>(_selectedPresetIndex),
initialValue: _selectedPresetIndex,
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.settings_presets, labelText: l10n.settings_presets,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
items: [ items: [
for (var i = 0; i < RadioSettings.presets.length; i++) for (final i in _visiblePresetIndexes())
DropdownMenuItem( DropdownMenuItem(
value: i, value: i,
child: Text(RadioSettings.presets[i].$1), child: Text(RadioSettings.presets[i].$1),
@@ -1263,13 +1546,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
], ],
onChanged: (index) { onChanged: (index) {
if (index != null) { if (index != null) {
_applyPreset(RadioSettings.presets[index].$2); _applyPreset(index);
} }
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextField( TextField(
controller: _frequencyController, controller: _frequencyController,
onChanged: (_) => _handleManualSettingsChanged('frequency'),
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.settings_frequency, labelText: l10n.settings_frequency,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
@@ -1292,7 +1576,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
) )
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value != null) setState(() => _bandwidth = value); if (value != null) {
setState(() {
_bandwidth = value;
_syncPresetSelection();
});
_logRadioSettingsState('Manual settings edit: bandwidth');
}
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -1308,7 +1598,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
) )
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value != null) setState(() => _spreadingFactor = value); if (value != null) {
setState(() {
_spreadingFactor = value;
_syncPresetSelection();
});
_logRadioSettingsState(
'Manual settings edit: spreading factor',
);
}
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -1324,12 +1622,19 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
) )
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value != null) setState(() => _codingRate = value); if (value != null) {
setState(() {
_codingRate = value;
_syncPresetSelection();
});
_logRadioSettingsState('Manual settings edit: coding rate');
}
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextField( TextField(
controller: _txPowerController, controller: _txPowerController,
onChanged: (_) => _handleManualSettingsChanged('tx power'),
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.settings_txPower, labelText: l10n.settings_txPower,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
@@ -1345,7 +1650,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
title: Text(l10n.settings_clientRepeat), title: Text(l10n.settings_clientRepeat),
subtitle: Text(l10n.settings_clientRepeatSubtitle), subtitle: Text(l10n.settings_clientRepeatSubtitle),
value: _clientRepeat, value: _clientRepeat,
onChanged: (value) => setState(() => _clientRepeat = value), onChanged: _handleClientRepeatChanged,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
), ),
], ],
@@ -1362,3 +1667,75 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
); );
} }
} }
class _RadioSettingsSnapshot {
final double frequencyMHz;
final LoRaBandwidth bandwidth;
final LoRaSpreadingFactor spreadingFactor;
final LoRaCodingRate codingRate;
final int txPowerDbm;
const _RadioSettingsSnapshot({
required this.frequencyMHz,
required this.bandwidth,
required this.spreadingFactor,
required this.codingRate,
required this.txPowerDbm,
});
/// Frequency in integer Hz — avoids floating-point comparison issues.
int get frequencyHz => (frequencyMHz * 1000).round();
/// Convert from the connector's raw-int snapshot to UI-enum snapshot.
static _RadioSettingsSnapshot? fromMeshCoreSnapshot(
MeshCoreRadioStateSnapshot snapshot,
) {
final bw = LoRaBandwidth.values
.where((b) => b.hz == snapshot.bwHz)
.firstOrNull;
final sf = LoRaSpreadingFactor.values
.where((s) => s.value == snapshot.sf)
.firstOrNull;
final cr = LoRaCodingRate.values
.where((c) => c.value == _toUiCodingRate(snapshot.cr))
.firstOrNull;
if (bw == null || sf == null || cr == null) return null;
return _RadioSettingsSnapshot(
frequencyMHz: snapshot.freqHz / 1000.0,
bandwidth: bw,
spreadingFactor: sf,
codingRate: cr,
txPowerDbm: snapshot.txPowerDbm,
);
}
/// Convert back to the connector's raw-int snapshot.
MeshCoreRadioStateSnapshot toMeshCoreSnapshot(int? deviceCr) {
return MeshCoreRadioStateSnapshot(
freqHz: frequencyHz,
bwHz: bandwidth.hz,
sf: spreadingFactor.value,
cr: _toDeviceCodingRate(codingRate.value, deviceCr),
txPowerDbm: txPowerDbm,
);
}
@override
bool operator ==(Object other) {
return other is _RadioSettingsSnapshot &&
frequencyHz == other.frequencyHz &&
bandwidth == other.bandwidth &&
spreadingFactor == other.spreadingFactor &&
codingRate == other.codingRate &&
txPowerDbm == other.txPowerDbm;
}
@override
int get hashCode => Object.hash(
frequencyHz,
bandwidth,
spreadingFactor,
codingRate,
txPowerDbm,
);
}