From 559300504ae4b97a6ec671ff985bc20e2333839c Mon Sep 17 00:00:00 2001 From: torlando-tech <239676438+torlando-tech@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:34:56 +0000 Subject: [PATCH 1/6] Add Ruhrgebiet RNode preset and raise custom TX power cap to 27 dBm Popular Local Presets: - Add a Germany "Ruhrgebiet" preset (freq 869462500, bw 125000, SF8, CR5, 27 dBm, 10% long-term airtime). The frequency sits in EU sub-band P (869.4-869.65 MHz), whose regulatory limit is 27 dBm / 10% duty, so the preset is compliant. Preset airtime support: - RNodeRegionalPreset gains an optional longTermAirtimeLimit, applied to the lt_alock field when a preset is selected; a "10% LT" chip is shown on the preset card when set. Custom config TX cap: - Raise the no-region/custom default max TX power from 22 to 27 dBm in RNodeConfigValidator, the ReviewConfigStep fallback, and the ReticulumConfig doc. The Python backend already permits up to 36 dBm and surfaces device errors, so no backend change is required. Tests: - RNodeRegionalPresetsTest: assert the exact Ruhrgebiet field set. - RNodeWizardViewModelTest: selectPreset applies tx 27 + lt 10%, and a preset without an airtime limit preserves the existing ltAlock. - RNodeConfigValidatorTest: updated for the new 27 dBm default. --- .../app/data/model/RNodeRegionalPreset.kt | 19 +++++++++ .../ui/screens/rnode/RegionSelectionStep.kt | 6 +++ .../app/ui/screens/rnode/ReviewConfigStep.kt | 2 +- .../app/viewmodel/RNodeConfigValidator.kt | 2 +- .../app/viewmodel/RNodeWizardViewModel.kt | 4 ++ .../data/model/RNodeRegionalPresetsTest.kt | 16 ++++++++ .../app/viewmodel/RNodeConfigValidatorTest.kt | 8 ++-- .../app/viewmodel/RNodeWizardViewModelTest.kt | 40 +++++++++++++++++++ .../app/rns/api/model/ReticulumConfig.kt | 2 +- 9 files changed, 92 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt b/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt index dd737a407..e690c3378 100644 --- a/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt +++ b/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt @@ -81,6 +81,12 @@ data class RNodeRegionalPreset( val codingRate: Int, /** Transmission power in dBm */ val txPower: Int, + /** + * Long-term airtime limit in percent (1-100), or null when the preset + * carries no explicit long-term airtime constraint. Applied to the + * `lt_alock` field when the preset is selected. + */ + val longTermAirtimeLimit: Int? = null, val description: String, ) @@ -796,6 +802,19 @@ object RNodeRegionalPresets { txPower = 14, description = "Wiesbaden configuration", ), + RNodeRegionalPreset( + id = "de_ruhrgebiet", + countryCode = "DE", + countryName = "Germany", + cityOrRegion = "Ruhrgebiet", + frequency = 869462500, + bandwidth = 125000, + spreadingFactor = 8, + codingRate = 5, + txPower = 27, + longTermAirtimeLimit = 10, + description = "Ruhrgebiet configuration (869.4625 MHz, 27 dBm, 10% LT airtime)", + ), // ==================== ITALY ==================== RNodeRegionalPreset( id = "it_default", diff --git a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt index a6f51c91a..c363893f3 100644 --- a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt +++ b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt @@ -424,6 +424,12 @@ private fun PopularPresetCard( label = "${preset.txPower} dBm", isSelected = isSelected, ) + preset.longTermAirtimeLimit?.let { limit -> + SettingChip( + label = "${limit}% LT", + isSelected = isSelected, + ) + } } } } diff --git a/app/src/main/java/network/columba/app/ui/screens/rnode/ReviewConfigStep.kt b/app/src/main/java/network/columba/app/ui/screens/rnode/ReviewConfigStep.kt index d196d59c2..3d63a38dc 100644 --- a/app/src/main/java/network/columba/app/ui/screens/rnode/ReviewConfigStep.kt +++ b/app/src/main/java/network/columba/app/ui/screens/rnode/ReviewConfigStep.kt @@ -398,7 +398,7 @@ fun ReviewConfigStep(viewModel: RNodeWizardViewModel) { Spacer(Modifier.height(8.dp)) // SF, CR, TX Power row - val maxTxPower = regionLimits?.maxTxPower ?: 22 + val maxTxPower = regionLimits?.maxTxPower ?: 27 Row( horizontalArrangement = Arrangement.spacedBy(8.dp), diff --git a/app/src/main/java/network/columba/app/viewmodel/RNodeConfigValidator.kt b/app/src/main/java/network/columba/app/viewmodel/RNodeConfigValidator.kt index d27c42593..b1286de5a 100644 --- a/app/src/main/java/network/columba/app/viewmodel/RNodeConfigValidator.kt +++ b/app/src/main/java/network/columba/app/viewmodel/RNodeConfigValidator.kt @@ -54,7 +54,7 @@ object RNodeConfigValidator { private const val MIN_CR = 5 private const val MAX_CR = 8 private const val MIN_TX_POWER = 0 - private const val DEFAULT_MAX_TX_POWER = 22 + private const val DEFAULT_MAX_TX_POWER = 27 // Default frequency range (when no region is selected) private const val DEFAULT_MIN_FREQ = 137_000_000L diff --git a/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt b/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt index b671af715..a31e3789b 100644 --- a/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt +++ b/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt @@ -3167,6 +3167,7 @@ class RNodeWizardViewModel } fun selectPreset(preset: RNodeRegionalPreset) { + val ltAlock = preset.longTermAirtimeLimit?.toString() _state.update { it.copy( selectedPreset = preset, @@ -3182,6 +3183,9 @@ class RNodeWizardViewModel codingRateError = null, txPower = preset.txPower.toString(), txPowerError = null, + // Apply the preset's long-term airtime limit if it defines one + ltAlock = ltAlock ?: it.ltAlock, + ltAlockError = if (ltAlock != null) null else it.ltAlockError, ) } } diff --git a/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt b/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt index bb136de4d..dc5107b0f 100644 --- a/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt +++ b/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt @@ -40,6 +40,22 @@ class RNodeRegionalPresetsTest { assertTrue("Should include United Kingdom", "United Kingdom" in countries) } + @Test + fun `Germany includes the Ruhrgebiet preset with expected values`() { + val preset = RNodeRegionalPresets.getPresetsForCountry("Germany") + .firstOrNull { it.id == "de_ruhrgebiet" } + + assertNotNull("Ruhrgebiet preset should exist for Germany", preset) + assertEquals("Ruhrgebiet", preset!!.cityOrRegion) + assertEquals(869_462_500L, preset.frequency) + assertEquals(125_000, preset.bandwidth) + assertEquals(8, preset.spreadingFactor) + assertEquals(5, preset.codingRate) + assertEquals(27, preset.txPower) + // Preset carries the long-term airtime limit applied to lt_alock on selection + assertEquals(10, preset.longTermAirtimeLimit) + } + // ========== getByCountry Tests ========== @Test diff --git a/app/src/test/java/network/columba/app/viewmodel/RNodeConfigValidatorTest.kt b/app/src/test/java/network/columba/app/viewmodel/RNodeConfigValidatorTest.kt index 1548eb9c6..50dc5122f 100644 --- a/app/src/test/java/network/columba/app/viewmodel/RNodeConfigValidatorTest.kt +++ b/app/src/test/java/network/columba/app/viewmodel/RNodeConfigValidatorTest.kt @@ -240,10 +240,10 @@ class RNodeConfigValidatorTest { @Test fun `validateTxPower without region uses default max`() { - // Default max is 22 - val result = RNodeConfigValidator.validateTxPower("25", null) + // Default max is 27 + val result = RNodeConfigValidator.validateTxPower("30", null) assertFalse(result.isValid) - assertTrue(result.errorMessage!!.contains("22")) + assertTrue(result.errorMessage!!.contains("27")) } @Test @@ -487,7 +487,7 @@ class RNodeConfigValidatorTest { @Test fun `getMaxTxPower returns default without region`() { - assertEquals(22, RNodeConfigValidator.getMaxTxPower(null)) + assertEquals(27, RNodeConfigValidator.getMaxTxPower(null)) } @Test diff --git a/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt b/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt index 462396d72..39b20a366 100644 --- a/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt +++ b/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt @@ -11,6 +11,7 @@ import network.columba.app.data.model.DiscoveredUsbDevice import network.columba.app.data.model.FrequencyRegions import network.columba.app.data.model.ModemPreset import network.columba.app.data.model.RNodeRegionalPreset +import network.columba.app.data.model.RNodeRegionalPresets import network.columba.app.repository.InterfaceRepository import network.columba.app.rns.api.model.InterfaceConfig import network.columba.app.service.InterfaceConfigManager @@ -781,6 +782,45 @@ class RNodeWizardViewModelTest { } } + @Test + fun `selectPreset applies tx power and long-term airtime limit`() = + runViewModelTest { + advanceUntilIdle() + + viewModel.goToStep(WizardStep.REGION_SELECTION) + advanceUntilIdle() + + val preset = RNodeRegionalPresets.presets.first { it.id == "de_ruhrgebiet" } + viewModel.selectPreset(preset) + advanceUntilIdle() + + val state = viewModel.state.value + assertEquals(preset.id, state.selectedPreset?.id) + assertEquals("869462500", state.frequency) + assertEquals("27", state.txPower) + // Preset carries an explicit long-term airtime limit, applied to ltAlock + assertEquals("10", state.ltAlock) + } + + @Test + fun `selectPreset without airtime limit preserves existing ltAlock`() = + runViewModelTest { + advanceUntilIdle() + + viewModel.goToStep(WizardStep.REGION_SELECTION) + advanceUntilIdle() + + viewModel.updateLtAlock("5") + advanceUntilIdle() + + val preset = RNodeRegionalPresets.presets.first { it.id == "us_default" } + viewModel.selectPreset(preset) + advanceUntilIdle() + + // Preset defines no long-term airtime limit, so the prior value is kept + assertEquals("5", viewModel.state.value.ltAlock) + } + @Test fun `updateStAlock sets error when exceeding duty cycle limit`() = runViewModelTest { diff --git a/rns-api/src/main/java/network/columba/app/rns/api/model/ReticulumConfig.kt b/rns-api/src/main/java/network/columba/app/rns/api/model/ReticulumConfig.kt index ddfa5dc83..10026a5e6 100644 --- a/rns-api/src/main/java/network/columba/app/rns/api/model/ReticulumConfig.kt +++ b/rns-api/src/main/java/network/columba/app/rns/api/model/ReticulumConfig.kt @@ -325,7 +325,7 @@ sealed class InterfaceConfig : Parcelable { * @param usbDeviceId Android USB device ID for USB serial mode (required when connectionMode="usb") * @param frequency LoRa frequency in Hz (137000000 - 3000000000) * @param bandwidth LoRa bandwidth in Hz (7800 - 1625000) - * @param txPower Transmission power in dBm (0-22) + * @param txPower Transmission power in dBm (0-27) * @param spreadingFactor LoRa spreading factor (5-12) * @param codingRate LoRa coding rate (5-8) * @param stAlock Short-term airtime limit percentage (optional) From 420d5cb764a49aa41f16651df2bffc37e4c76692 Mon Sep 17 00:00:00 2001 From: torlando-tech <239676438+torlando-tech@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:01:07 +0000 Subject: [PATCH 2/6] Wrap RNode preset settings chips in FlowRow to prevent overflow Greptile found the new LT-airtime chip pushes the Ruhrgebiet preset to five labels in a single non-wrapping Row, which can overflow or clip on narrow screens / larger fonts. Switch the settings-preview row to FlowRow (with vertical spacing), matching how chips wrap elsewhere in this wizard (ModemPresetStep), so the card stays usable on constrained displays. --- .../columba/app/ui/screens/rnode/RegionSelectionStep.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt index c363893f3..83fcc7924 100644 --- a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt +++ b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt @@ -2,6 +2,7 @@ package network.columba.app.ui.screens.rnode import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -405,8 +406,9 @@ private fun PopularPresetCard( Spacer(Modifier.height(8.dp)) // Settings preview - show all parameters since these are complete presets - Row( + FlowRow( horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { SettingChip( label = "${preset.frequency / 1_000_000.0} MHz", From 1aaa08d547e59b4b57e1b11bca4f2337df231e01 Mon Sep 17 00:00:00 2001 From: torlando-tech <239676438+torlando-tech@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:10:49 +0000 Subject: [PATCH 3/6] Opt in to ExperimentalLayoutApi for FlowRow in preset card Match the module convention used by every other FlowRow consumer (ModemPresetStep, DiscoveredInterfacesScreen, ...) by importing ExperimentalLayoutApi and annotating PopularPresetCard with @OptIn(ExperimentalLayoutApi::class). --- .../network/columba/app/ui/screens/rnode/RegionSelectionStep.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt index 83fcc7924..583bea653 100644 --- a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt +++ b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt @@ -2,6 +2,7 @@ package network.columba.app.ui.screens.rnode import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -344,6 +345,7 @@ private fun CountryCard( } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun PopularPresetCard( preset: RNodeRegionalPreset, From 4a8e5fe82a7cc546acf96308971ecec9eb072172 Mon Sep 17 00:00:00 2001 From: torlando-tech <239676438+torlando-tech@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:20:23 +0000 Subject: [PATCH 4/6] Omit explicit TX power from Ruhrgebiet preset Only the Heltec v4 reaches above the 17-22 dBm ceiling that most RNode radios cap at, so the preset no longer forces 27 dBm. The preset's txPower field is now optional (null = keep the frequency region's default) and the wizard only overrides the current value when a preset actually specifies one. The 27 dBm custom-config cap from the previous commit stays for manual use. --- .../app/data/model/RNodeRegionalPreset.kt | 14 +++++-- .../ui/screens/rnode/RegionSelectionStep.kt | 10 +++-- .../app/viewmodel/RNodeWizardViewModel.kt | 9 +++- .../data/model/RNodeRegionalPresetsTest.kt | 8 ++-- .../app/viewmodel/RNodeWizardViewModelTest.kt | 41 ++++++++++++++++++- 5 files changed, 67 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt b/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt index e690c3378..cd1e79670 100644 --- a/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt +++ b/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt @@ -79,8 +79,15 @@ data class RNodeRegionalPreset( val spreadingFactor: Int, /** LoRa CR (5-8) */ val codingRate: Int, - /** Transmission power in dBm */ - val txPower: Int, + /** + * Transmission power in dBm, or null when the preset carries no explicit + * override and the frequency region's default should be kept. Most RNode + * radios top out at 17-22 dBm; only the Heltec v4 reaches the higher EU868 + * sub-band-P ceiling, so presets defer to the region default unless a + * board is known to need more. Applied (when non-null) when the preset is + * selected; a null value leaves the current / region-default TX power. + */ + val txPower: Int? = null, /** * Long-term airtime limit in percent (1-100), or null when the preset * carries no explicit long-term airtime constraint. Applied to the @@ -811,9 +818,8 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 27, longTermAirtimeLimit = 10, - description = "Ruhrgebiet configuration (869.4625 MHz, 27 dBm, 10% LT airtime)", + description = "Ruhrgebiet configuration (869.4625 MHz, region-default TX, 10% LT airtime)", ), // ==================== ITALY ==================== RNodeRegionalPreset( diff --git a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt index 583bea653..4e881432e 100644 --- a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt +++ b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt @@ -424,10 +424,12 @@ private fun PopularPresetCard( label = "${preset.bandwidth / 1000} kHz", isSelected = isSelected, ) - SettingChip( - label = "${preset.txPower} dBm", - isSelected = isSelected, - ) + preset.txPower?.let { tx -> + SettingChip( + label = "$tx dBm", + isSelected = isSelected, + ) + } preset.longTermAirtimeLimit?.let { limit -> SettingChip( label = "${limit}% LT", diff --git a/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt b/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt index a31e3789b..f4b9b92ad 100644 --- a/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt +++ b/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt @@ -3168,6 +3168,7 @@ class RNodeWizardViewModel fun selectPreset(preset: RNodeRegionalPreset) { val ltAlock = preset.longTermAirtimeLimit?.toString() + val presetTx = preset.txPower?.toString() _state.update { it.copy( selectedPreset = preset, @@ -3181,8 +3182,12 @@ class RNodeWizardViewModel spreadingFactorError = null, codingRate = preset.codingRate.toString(), codingRateError = null, - txPower = preset.txPower.toString(), - txPowerError = null, + // Presets without an explicit TX power keep the current value + // (the region default), since most RNode radios top out at + // 17-22 dBm and only the Heltec v4 reaches the higher + // EU868 sub-band-P ceiling. + txPower = presetTx ?: it.txPower, + txPowerError = if (presetTx != null) null else it.txPowerError, // Apply the preset's long-term airtime limit if it defines one ltAlock = ltAlock ?: it.ltAlock, ltAlockError = if (ltAlock != null) null else it.ltAlockError, diff --git a/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt b/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt index dc5107b0f..9c9334d85 100644 --- a/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt +++ b/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt @@ -51,7 +51,9 @@ class RNodeRegionalPresetsTest { assertEquals(125_000, preset.bandwidth) assertEquals(8, preset.spreadingFactor) assertEquals(5, preset.codingRate) - assertEquals(27, preset.txPower) + // No explicit TX power: only the Heltec v4 reaches >22 dBm, so the + // preset defers to the frequency region's default. + assertNull(preset.txPower) // Preset carries the long-term airtime limit applied to lt_alock on selection assertEquals(10, preset.longTermAirtimeLimit) } @@ -233,8 +235,8 @@ class RNodeRegionalPresetsTest { fun `all presets have valid TX power`() { RNodeRegionalPresets.presets.forEach { preset -> assertTrue( - "Preset ${preset.id} should have TX power 1-30, got ${preset.txPower}", - preset.txPower in 1..30, + "Preset ${preset.id} should have TX power 1-30 or none (region default), got ${preset.txPower}", + preset.txPower == null || preset.txPower in 1..30, ) } } diff --git a/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt b/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt index 39b20a366..47adffaf1 100644 --- a/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt +++ b/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt @@ -783,13 +783,17 @@ class RNodeWizardViewModelTest { } @Test - fun `selectPreset applies tx power and long-term airtime limit`() = + fun `selectPreset without explicit tx power keeps region default and applies airtime limit`() = runViewModelTest { advanceUntilIdle() viewModel.goToStep(WizardStep.REGION_SELECTION) advanceUntilIdle() + // EU868 sub-band P defaults to 14 dBm; the preset must not override it. + viewModel.selectFrequencyRegion(euRegionP) + advanceUntilIdle() + val preset = RNodeRegionalPresets.presets.first { it.id == "de_ruhrgebiet" } viewModel.selectPreset(preset) advanceUntilIdle() @@ -797,11 +801,44 @@ class RNodeWizardViewModelTest { val state = viewModel.state.value assertEquals(preset.id, state.selectedPreset?.id) assertEquals("869462500", state.frequency) - assertEquals("27", state.txPower) + // No explicit TX power on the preset -> region default (14) is kept. + assertNull(preset.txPower) + assertEquals(euRegionP.defaultTxPower.toString(), state.txPower) // Preset carries an explicit long-term airtime limit, applied to ltAlock assertEquals("10", state.ltAlock) } + @Test + fun `selectPreset with explicit tx power applies it over the region default`() = + runViewModelTest { + advanceUntilIdle() + + viewModel.goToStep(WizardStep.REGION_SELECTION) + advanceUntilIdle() + + viewModel.selectFrequencyRegion(euRegionP) // default 14 dBm + advanceUntilIdle() + + val preset = + RNodeRegionalPreset( + id = "test_explicit_tx", + countryCode = "DE", + countryName = "Germany", + cityOrRegion = "Test City", + frequency = 869_462_500, + bandwidth = 125_000, + spreadingFactor = 8, + codingRate = 5, + txPower = 17, + description = "Explicit TX test preset", + ) + viewModel.selectPreset(preset) + advanceUntilIdle() + + val state = viewModel.state.value + assertEquals("17", state.txPower) + } + @Test fun `selectPreset without airtime limit preserves existing ltAlock`() = runViewModelTest { From e37806062d944c5ac228803c70901aff1a9f6877 Mon Sep 17 00:00:00 2001 From: torlando-tech <239676438+torlando-tech@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:05:43 +0000 Subject: [PATCH 5/6] Remove TX power from RNode presets (board-dependent) TX power is hardware-dependent: most RNode radios top out at 17-22 dBm while the Heltec v4 reaches the higher EU868 sub-band-P ceiling, so a preset cannot know what its target board supports. The preset model no longer carries a txPower field, selectPreset never touches the wizard's TX power (region default or user-set value is preserved), and the preset card no longer shows a dBm chip. --- .../app/data/model/RNodeRegionalPreset.kt | 48 ------------------- .../ui/screens/rnode/RegionSelectionStep.kt | 6 --- .../app/viewmodel/RNodeWizardViewModel.kt | 12 ++--- .../data/model/RNodeRegionalPresetsTest.kt | 13 ----- .../app/viewmodel/RNodeWizardViewModelTest.kt | 43 +++++------------ 5 files changed, 17 insertions(+), 105 deletions(-) diff --git a/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt b/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt index cd1e79670..f9d0cbf12 100644 --- a/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt +++ b/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt @@ -79,15 +79,6 @@ data class RNodeRegionalPreset( val spreadingFactor: Int, /** LoRa CR (5-8) */ val codingRate: Int, - /** - * Transmission power in dBm, or null when the preset carries no explicit - * override and the frequency region's default should be kept. Most RNode - * radios top out at 17-22 dBm; only the Heltec v4 reaches the higher EU868 - * sub-band-P ceiling, so presets defer to the region default unless a - * board is known to need more. Applied (when non-null) when the preset is - * selected; a null value leaves the current / region-default TX power. - */ - val txPower: Int? = null, /** * Long-term airtime limit in percent (1-100), or null when the preset * carries no explicit long-term airtime constraint. Applied to the @@ -683,7 +674,6 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 9, codingRate = 5, - txPower = 17, description = "915-928 MHz AU band (default)", ), RNodeRegionalPreset( @@ -695,7 +685,6 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 11, codingRate = 5, - txPower = 17, description = "Sydney long-range configuration", ), RNodeRegionalPreset( @@ -707,7 +696,6 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 9, codingRate = 5, - txPower = 17, description = "Brisbane configuration", ), RNodeRegionalPreset( @@ -719,7 +707,6 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 9, codingRate = 5, - txPower = 17, description = "Western Sydney configuration", ), // ==================== BELGIUM ==================== @@ -732,7 +719,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, - txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -744,7 +730,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 14, description = "Duffel configuration", ), // ==================== FINLAND ==================== @@ -757,7 +742,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, - txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -769,7 +753,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 14, description = "Turku configuration", ), // ==================== GERMANY ==================== @@ -782,7 +765,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, - txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -794,7 +776,6 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 7, codingRate = 5, - txPower = 14, description = "Darmstadt configuration", ), RNodeRegionalPreset( @@ -806,7 +787,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 14, description = "Wiesbaden configuration", ), RNodeRegionalPreset( @@ -831,7 +811,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, - txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -843,7 +822,6 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 8, codingRate = 5, - txPower = 14, description = "Salerno configuration", ), RNodeRegionalPreset( @@ -855,7 +833,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 7, codingRate = 5, - txPower = 14, description = "Brescia configuration", ), RNodeRegionalPreset( @@ -867,7 +844,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 7, codingRate = 5, - txPower = 14, description = "Treviso configuration", ), RNodeRegionalPreset( @@ -879,7 +855,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 12, codingRate = 5, - txPower = 14, description = "Genova 433 MHz configuration", ), // ==================== MALAYSIA ==================== @@ -892,7 +867,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 17, description = "920 MHz AS923 band", ), // ==================== NETHERLANDS ==================== @@ -905,7 +879,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, - txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -917,7 +890,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 14, description = "Rotterdam Nesselande configuration", ), RNodeRegionalPreset( @@ -929,7 +901,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 14, description = "Brugge (Bruges) configuration", ), // ==================== NORWAY ==================== @@ -942,7 +913,6 @@ object RNodeRegionalPresets { bandwidth = 62500, spreadingFactor = 7, codingRate = 5, - txPower = 14, description = "Norway narrowband", ), // ==================== SINGAPORE ==================== @@ -955,7 +925,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 17, description = "920 MHz AS923 band", ), // ==================== SPAIN ==================== @@ -968,7 +937,6 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 8, codingRate = 5, - txPower = 14, description = "869 MHz EU band", ), RNodeRegionalPreset( @@ -980,7 +948,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 14, description = "Madrid configuration", ), // ==================== SWEDEN ==================== @@ -993,7 +960,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, - txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -1005,7 +971,6 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 10, codingRate = 5, - txPower = 14, description = "Gothenburg area configuration", ), RNodeRegionalPreset( @@ -1017,7 +982,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 12, description = "433 MHz band shared by Gothenburg, Borås, Älvsered", ), RNodeRegionalPreset( @@ -1029,7 +993,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 14, description = "Mörbylånga/Bredinge configuration", ), // ==================== SWITZERLAND ==================== @@ -1042,7 +1005,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, - txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -1054,7 +1016,6 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 8, codingRate = 5, - txPower = 14, description = "Bern configuration", ), // ==================== THAILAND ==================== @@ -1067,7 +1028,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 17, description = "920 MHz AS923 band", ), // ==================== UNITED KINGDOM ==================== @@ -1080,7 +1040,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, - txPower = 14, description = "868 MHz UK band", ), RNodeRegionalPreset( @@ -1092,7 +1051,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, - txPower = 14, description = "St. Helens configuration", ), RNodeRegionalPreset( @@ -1104,7 +1062,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, - txPower = 14, description = "Edinburgh 868 MHz configuration", ), RNodeRegionalPreset( @@ -1116,7 +1073,6 @@ object RNodeRegionalPresets { bandwidth = 812500, spreadingFactor = 7, codingRate = 5, - txPower = 14, description = "Edinburgh 2.4 GHz configuration", ), // ==================== UNITED STATES ==================== @@ -1129,7 +1085,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 17, description = "915 MHz ISM band (default)", ), RNodeRegionalPreset( @@ -1141,7 +1096,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 17, description = "Portsmouth, NH configuration", ), RNodeRegionalPreset( @@ -1153,7 +1107,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 17, description = "Olympia, WA configuration", ), RNodeRegionalPreset( @@ -1165,7 +1118,6 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, - txPower = 17, description = "Chicago, IL configuration", ), ) diff --git a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt index 4e881432e..1ca575a5a 100644 --- a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt +++ b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt @@ -424,12 +424,6 @@ private fun PopularPresetCard( label = "${preset.bandwidth / 1000} kHz", isSelected = isSelected, ) - preset.txPower?.let { tx -> - SettingChip( - label = "$tx dBm", - isSelected = isSelected, - ) - } preset.longTermAirtimeLimit?.let { limit -> SettingChip( label = "${limit}% LT", diff --git a/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt b/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt index f4b9b92ad..cf06021f2 100644 --- a/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt +++ b/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt @@ -3168,7 +3168,6 @@ class RNodeWizardViewModel fun selectPreset(preset: RNodeRegionalPreset) { val ltAlock = preset.longTermAirtimeLimit?.toString() - val presetTx = preset.txPower?.toString() _state.update { it.copy( selectedPreset = preset, @@ -3182,12 +3181,11 @@ class RNodeWizardViewModel spreadingFactorError = null, codingRate = preset.codingRate.toString(), codingRateError = null, - // Presets without an explicit TX power keep the current value - // (the region default), since most RNode radios top out at - // 17-22 dBm and only the Heltec v4 reaches the higher - // EU868 sub-band-P ceiling. - txPower = presetTx ?: it.txPower, - txPowerError = if (presetTx != null) null else it.txPowerError, + // TX power is intentionally not part of presets: supported + // output power depends on the RNode board (e.g. the Heltec v4 + // exceeds the 17-22 dBm ceiling most radios have). The + // wizard keeps the frequency region's default (or a value the + // user set in custom mode) and the user can adjust it later. // Apply the preset's long-term airtime limit if it defines one ltAlock = ltAlock ?: it.ltAlock, ltAlockError = if (ltAlock != null) null else it.ltAlockError, diff --git a/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt b/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt index 9c9334d85..1212c72eb 100644 --- a/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt +++ b/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt @@ -51,9 +51,6 @@ class RNodeRegionalPresetsTest { assertEquals(125_000, preset.bandwidth) assertEquals(8, preset.spreadingFactor) assertEquals(5, preset.codingRate) - // No explicit TX power: only the Heltec v4 reaches >22 dBm, so the - // preset defers to the frequency region's default. - assertNull(preset.txPower) // Preset carries the long-term airtime limit applied to lt_alock on selection assertEquals(10, preset.longTermAirtimeLimit) } @@ -231,16 +228,6 @@ class RNodeRegionalPresetsTest { } } - @Test - fun `all presets have valid TX power`() { - RNodeRegionalPresets.presets.forEach { preset -> - assertTrue( - "Preset ${preset.id} should have TX power 1-30 or none (region default), got ${preset.txPower}", - preset.txPower == null || preset.txPower in 1..30, - ) - } - } - @Test fun `all presets have valid country codes`() { RNodeRegionalPresets.presets.forEach { preset -> diff --git a/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt b/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt index 47adffaf1..2a45201ca 100644 --- a/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt +++ b/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt @@ -330,7 +330,6 @@ class RNodeWizardViewModelTest { bandwidth = 125_000, spreadingFactor = 9, codingRate = 5, - txPower = 17, description = "Test preset", ) viewModel.selectPreset(testPreset) @@ -357,7 +356,6 @@ class RNodeWizardViewModelTest { bandwidth = 125_000, spreadingFactor = 9, codingRate = 5, - txPower = 17, description = "Test preset", ) viewModel.selectPreset(testPreset) @@ -388,7 +386,6 @@ class RNodeWizardViewModelTest { bandwidth = 125_000, spreadingFactor = 9, codingRate = 5, - txPower = 17, description = "Test preset", ) viewModel.selectPreset(testPreset) @@ -783,14 +780,15 @@ class RNodeWizardViewModelTest { } @Test - fun `selectPreset without explicit tx power keeps region default and applies airtime limit`() = + fun `selectPreset preserves the region TX power and applies airtime limit`() = runViewModelTest { advanceUntilIdle() viewModel.goToStep(WizardStep.REGION_SELECTION) advanceUntilIdle() - // EU868 sub-band P defaults to 14 dBm; the preset must not override it. + // EU868 sub-band P defaults to 14 dBm; presets never set TX power, + // so the region default must survive preset selection. viewModel.selectFrequencyRegion(euRegionP) advanceUntilIdle() @@ -801,15 +799,14 @@ class RNodeWizardViewModelTest { val state = viewModel.state.value assertEquals(preset.id, state.selectedPreset?.id) assertEquals("869462500", state.frequency) - // No explicit TX power on the preset -> region default (14) is kept. - assertNull(preset.txPower) + // Presets do not carry TX power -> region default (14) is kept. assertEquals(euRegionP.defaultTxPower.toString(), state.txPower) // Preset carries an explicit long-term airtime limit, applied to ltAlock assertEquals("10", state.ltAlock) } @Test - fun `selectPreset with explicit tx power applies it over the region default`() = + fun `selectPreset never overrides an existing TX power value`() = runViewModelTest { advanceUntilIdle() @@ -819,19 +816,12 @@ class RNodeWizardViewModelTest { viewModel.selectFrequencyRegion(euRegionP) // default 14 dBm advanceUntilIdle() - val preset = - RNodeRegionalPreset( - id = "test_explicit_tx", - countryCode = "DE", - countryName = "Germany", - cityOrRegion = "Test City", - frequency = 869_462_500, - bandwidth = 125_000, - spreadingFactor = 8, - codingRate = 5, - txPower = 17, - description = "Explicit TX test preset", - ) + // The user raises TX power manually (e.g. a Heltec v4 allows more + // than the region default); selecting a preset must not clobber it. + viewModel.updateTxPower("17") + advanceUntilIdle() + + val preset = RNodeRegionalPresets.presets.first { it.id == "de_ruhrgebiet" } viewModel.selectPreset(preset) advanceUntilIdle() @@ -1664,7 +1654,6 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, - txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -1710,7 +1699,6 @@ class RNodeWizardViewModelTest { tcpPort = 7633, frequency = 915000000, bandwidth = 125000, - txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -1813,7 +1801,6 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 433500000, // Custom frequency bandwidth = 62500, // Custom bandwidth - txPower = 10, spreadingFactor = 9, codingRate = 6, ) @@ -1858,9 +1845,9 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 869525000, bandwidth = 250000, - txPower = 14, spreadingFactor = 10, codingRate = 5, + txPower = 14, stAlock = 15.0, ltAlock = 5.0, mode = "gateway", @@ -1974,7 +1961,6 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, - txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -2273,7 +2259,6 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, - txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -2343,7 +2328,6 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, - txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -2383,7 +2367,6 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, - txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -2444,7 +2427,6 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, - txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -2496,7 +2478,6 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, - txPower = 17, spreadingFactor = 8, codingRate = 5, ) From 047e53e2deada971d28ce078c66092febf699c65 Mon Sep 17 00:00:00 2001 From: torlando-tech <239676438+torlando-tech@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:07:36 +0000 Subject: [PATCH 6/6] Revert "Remove TX power from RNode presets (board-dependent)" This reverts commit e37806062d944c5ac228803c70901aff1a9f6877. --- .../app/data/model/RNodeRegionalPreset.kt | 48 +++++++++++++++++++ .../ui/screens/rnode/RegionSelectionStep.kt | 6 +++ .../app/viewmodel/RNodeWizardViewModel.kt | 12 +++-- .../data/model/RNodeRegionalPresetsTest.kt | 13 +++++ .../app/viewmodel/RNodeWizardViewModelTest.kt | 43 ++++++++++++----- 5 files changed, 105 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt b/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt index f9d0cbf12..cd1e79670 100644 --- a/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt +++ b/app/src/main/java/network/columba/app/data/model/RNodeRegionalPreset.kt @@ -79,6 +79,15 @@ data class RNodeRegionalPreset( val spreadingFactor: Int, /** LoRa CR (5-8) */ val codingRate: Int, + /** + * Transmission power in dBm, or null when the preset carries no explicit + * override and the frequency region's default should be kept. Most RNode + * radios top out at 17-22 dBm; only the Heltec v4 reaches the higher EU868 + * sub-band-P ceiling, so presets defer to the region default unless a + * board is known to need more. Applied (when non-null) when the preset is + * selected; a null value leaves the current / region-default TX power. + */ + val txPower: Int? = null, /** * Long-term airtime limit in percent (1-100), or null when the preset * carries no explicit long-term airtime constraint. Applied to the @@ -674,6 +683,7 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 9, codingRate = 5, + txPower = 17, description = "915-928 MHz AU band (default)", ), RNodeRegionalPreset( @@ -685,6 +695,7 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 11, codingRate = 5, + txPower = 17, description = "Sydney long-range configuration", ), RNodeRegionalPreset( @@ -696,6 +707,7 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 9, codingRate = 5, + txPower = 17, description = "Brisbane configuration", ), RNodeRegionalPreset( @@ -707,6 +719,7 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 9, codingRate = 5, + txPower = 17, description = "Western Sydney configuration", ), // ==================== BELGIUM ==================== @@ -719,6 +732,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, + txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -730,6 +744,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 14, description = "Duffel configuration", ), // ==================== FINLAND ==================== @@ -742,6 +757,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, + txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -753,6 +769,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 14, description = "Turku configuration", ), // ==================== GERMANY ==================== @@ -765,6 +782,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, + txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -776,6 +794,7 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 7, codingRate = 5, + txPower = 14, description = "Darmstadt configuration", ), RNodeRegionalPreset( @@ -787,6 +806,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 14, description = "Wiesbaden configuration", ), RNodeRegionalPreset( @@ -811,6 +831,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, + txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -822,6 +843,7 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 8, codingRate = 5, + txPower = 14, description = "Salerno configuration", ), RNodeRegionalPreset( @@ -833,6 +855,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 7, codingRate = 5, + txPower = 14, description = "Brescia configuration", ), RNodeRegionalPreset( @@ -844,6 +867,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 7, codingRate = 5, + txPower = 14, description = "Treviso configuration", ), RNodeRegionalPreset( @@ -855,6 +879,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 12, codingRate = 5, + txPower = 14, description = "Genova 433 MHz configuration", ), // ==================== MALAYSIA ==================== @@ -867,6 +892,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 17, description = "920 MHz AS923 band", ), // ==================== NETHERLANDS ==================== @@ -879,6 +905,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, + txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -890,6 +917,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 14, description = "Rotterdam Nesselande configuration", ), RNodeRegionalPreset( @@ -901,6 +929,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 14, description = "Brugge (Bruges) configuration", ), // ==================== NORWAY ==================== @@ -913,6 +942,7 @@ object RNodeRegionalPresets { bandwidth = 62500, spreadingFactor = 7, codingRate = 5, + txPower = 14, description = "Norway narrowband", ), // ==================== SINGAPORE ==================== @@ -925,6 +955,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 17, description = "920 MHz AS923 band", ), // ==================== SPAIN ==================== @@ -937,6 +968,7 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 8, codingRate = 5, + txPower = 14, description = "869 MHz EU band", ), RNodeRegionalPreset( @@ -948,6 +980,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 14, description = "Madrid configuration", ), // ==================== SWEDEN ==================== @@ -960,6 +993,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, + txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -971,6 +1005,7 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 10, codingRate = 5, + txPower = 14, description = "Gothenburg area configuration", ), RNodeRegionalPreset( @@ -982,6 +1017,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 12, description = "433 MHz band shared by Gothenburg, Borås, Älvsered", ), RNodeRegionalPreset( @@ -993,6 +1029,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 14, description = "Mörbylånga/Bredinge configuration", ), // ==================== SWITZERLAND ==================== @@ -1005,6 +1042,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, + txPower = 14, description = "868 MHz EU band", ), RNodeRegionalPreset( @@ -1016,6 +1054,7 @@ object RNodeRegionalPresets { bandwidth = 250000, spreadingFactor = 8, codingRate = 5, + txPower = 14, description = "Bern configuration", ), // ==================== THAILAND ==================== @@ -1028,6 +1067,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 17, description = "920 MHz AS923 band", ), // ==================== UNITED KINGDOM ==================== @@ -1040,6 +1080,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, + txPower = 14, description = "868 MHz UK band", ), RNodeRegionalPreset( @@ -1051,6 +1092,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, + txPower = 14, description = "St. Helens configuration", ), RNodeRegionalPreset( @@ -1062,6 +1104,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 9, codingRate = 5, + txPower = 14, description = "Edinburgh 868 MHz configuration", ), RNodeRegionalPreset( @@ -1073,6 +1116,7 @@ object RNodeRegionalPresets { bandwidth = 812500, spreadingFactor = 7, codingRate = 5, + txPower = 14, description = "Edinburgh 2.4 GHz configuration", ), // ==================== UNITED STATES ==================== @@ -1085,6 +1129,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 17, description = "915 MHz ISM band (default)", ), RNodeRegionalPreset( @@ -1096,6 +1141,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 17, description = "Portsmouth, NH configuration", ), RNodeRegionalPreset( @@ -1107,6 +1153,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 17, description = "Olympia, WA configuration", ), RNodeRegionalPreset( @@ -1118,6 +1165,7 @@ object RNodeRegionalPresets { bandwidth = 125000, spreadingFactor = 8, codingRate = 5, + txPower = 17, description = "Chicago, IL configuration", ), ) diff --git a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt index 1ca575a5a..4e881432e 100644 --- a/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt +++ b/app/src/main/java/network/columba/app/ui/screens/rnode/RegionSelectionStep.kt @@ -424,6 +424,12 @@ private fun PopularPresetCard( label = "${preset.bandwidth / 1000} kHz", isSelected = isSelected, ) + preset.txPower?.let { tx -> + SettingChip( + label = "$tx dBm", + isSelected = isSelected, + ) + } preset.longTermAirtimeLimit?.let { limit -> SettingChip( label = "${limit}% LT", diff --git a/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt b/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt index cf06021f2..f4b9b92ad 100644 --- a/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt +++ b/app/src/main/java/network/columba/app/viewmodel/RNodeWizardViewModel.kt @@ -3168,6 +3168,7 @@ class RNodeWizardViewModel fun selectPreset(preset: RNodeRegionalPreset) { val ltAlock = preset.longTermAirtimeLimit?.toString() + val presetTx = preset.txPower?.toString() _state.update { it.copy( selectedPreset = preset, @@ -3181,11 +3182,12 @@ class RNodeWizardViewModel spreadingFactorError = null, codingRate = preset.codingRate.toString(), codingRateError = null, - // TX power is intentionally not part of presets: supported - // output power depends on the RNode board (e.g. the Heltec v4 - // exceeds the 17-22 dBm ceiling most radios have). The - // wizard keeps the frequency region's default (or a value the - // user set in custom mode) and the user can adjust it later. + // Presets without an explicit TX power keep the current value + // (the region default), since most RNode radios top out at + // 17-22 dBm and only the Heltec v4 reaches the higher + // EU868 sub-band-P ceiling. + txPower = presetTx ?: it.txPower, + txPowerError = if (presetTx != null) null else it.txPowerError, // Apply the preset's long-term airtime limit if it defines one ltAlock = ltAlock ?: it.ltAlock, ltAlockError = if (ltAlock != null) null else it.ltAlockError, diff --git a/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt b/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt index 1212c72eb..9c9334d85 100644 --- a/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt +++ b/app/src/test/java/network/columba/app/data/model/RNodeRegionalPresetsTest.kt @@ -51,6 +51,9 @@ class RNodeRegionalPresetsTest { assertEquals(125_000, preset.bandwidth) assertEquals(8, preset.spreadingFactor) assertEquals(5, preset.codingRate) + // No explicit TX power: only the Heltec v4 reaches >22 dBm, so the + // preset defers to the frequency region's default. + assertNull(preset.txPower) // Preset carries the long-term airtime limit applied to lt_alock on selection assertEquals(10, preset.longTermAirtimeLimit) } @@ -228,6 +231,16 @@ class RNodeRegionalPresetsTest { } } + @Test + fun `all presets have valid TX power`() { + RNodeRegionalPresets.presets.forEach { preset -> + assertTrue( + "Preset ${preset.id} should have TX power 1-30 or none (region default), got ${preset.txPower}", + preset.txPower == null || preset.txPower in 1..30, + ) + } + } + @Test fun `all presets have valid country codes`() { RNodeRegionalPresets.presets.forEach { preset -> diff --git a/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt b/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt index 2a45201ca..47adffaf1 100644 --- a/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt +++ b/app/src/test/java/network/columba/app/viewmodel/RNodeWizardViewModelTest.kt @@ -330,6 +330,7 @@ class RNodeWizardViewModelTest { bandwidth = 125_000, spreadingFactor = 9, codingRate = 5, + txPower = 17, description = "Test preset", ) viewModel.selectPreset(testPreset) @@ -356,6 +357,7 @@ class RNodeWizardViewModelTest { bandwidth = 125_000, spreadingFactor = 9, codingRate = 5, + txPower = 17, description = "Test preset", ) viewModel.selectPreset(testPreset) @@ -386,6 +388,7 @@ class RNodeWizardViewModelTest { bandwidth = 125_000, spreadingFactor = 9, codingRate = 5, + txPower = 17, description = "Test preset", ) viewModel.selectPreset(testPreset) @@ -780,15 +783,14 @@ class RNodeWizardViewModelTest { } @Test - fun `selectPreset preserves the region TX power and applies airtime limit`() = + fun `selectPreset without explicit tx power keeps region default and applies airtime limit`() = runViewModelTest { advanceUntilIdle() viewModel.goToStep(WizardStep.REGION_SELECTION) advanceUntilIdle() - // EU868 sub-band P defaults to 14 dBm; presets never set TX power, - // so the region default must survive preset selection. + // EU868 sub-band P defaults to 14 dBm; the preset must not override it. viewModel.selectFrequencyRegion(euRegionP) advanceUntilIdle() @@ -799,14 +801,15 @@ class RNodeWizardViewModelTest { val state = viewModel.state.value assertEquals(preset.id, state.selectedPreset?.id) assertEquals("869462500", state.frequency) - // Presets do not carry TX power -> region default (14) is kept. + // No explicit TX power on the preset -> region default (14) is kept. + assertNull(preset.txPower) assertEquals(euRegionP.defaultTxPower.toString(), state.txPower) // Preset carries an explicit long-term airtime limit, applied to ltAlock assertEquals("10", state.ltAlock) } @Test - fun `selectPreset never overrides an existing TX power value`() = + fun `selectPreset with explicit tx power applies it over the region default`() = runViewModelTest { advanceUntilIdle() @@ -816,12 +819,19 @@ class RNodeWizardViewModelTest { viewModel.selectFrequencyRegion(euRegionP) // default 14 dBm advanceUntilIdle() - // The user raises TX power manually (e.g. a Heltec v4 allows more - // than the region default); selecting a preset must not clobber it. - viewModel.updateTxPower("17") - advanceUntilIdle() - - val preset = RNodeRegionalPresets.presets.first { it.id == "de_ruhrgebiet" } + val preset = + RNodeRegionalPreset( + id = "test_explicit_tx", + countryCode = "DE", + countryName = "Germany", + cityOrRegion = "Test City", + frequency = 869_462_500, + bandwidth = 125_000, + spreadingFactor = 8, + codingRate = 5, + txPower = 17, + description = "Explicit TX test preset", + ) viewModel.selectPreset(preset) advanceUntilIdle() @@ -1654,6 +1664,7 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, + txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -1699,6 +1710,7 @@ class RNodeWizardViewModelTest { tcpPort = 7633, frequency = 915000000, bandwidth = 125000, + txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -1801,6 +1813,7 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 433500000, // Custom frequency bandwidth = 62500, // Custom bandwidth + txPower = 10, spreadingFactor = 9, codingRate = 6, ) @@ -1845,9 +1858,9 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 869525000, bandwidth = 250000, + txPower = 14, spreadingFactor = 10, codingRate = 5, - txPower = 14, stAlock = 15.0, ltAlock = 5.0, mode = "gateway", @@ -1961,6 +1974,7 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, + txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -2259,6 +2273,7 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, + txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -2328,6 +2343,7 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, + txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -2367,6 +2383,7 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, + txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -2427,6 +2444,7 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, + txPower = 17, spreadingFactor = 8, codingRate = 5, ) @@ -2478,6 +2496,7 @@ class RNodeWizardViewModelTest { targetDeviceName = "", frequency = 915000000, bandwidth = 125000, + txPower = 17, spreadingFactor = 8, codingRate = 5, )