Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -405,8 +406,9 @@ private fun PopularPresetCard(
Spacer(Modifier.height(8.dp))

// Settings preview - show all parameters since these are complete presets
Row(
FlowRow(
Comment thread
greptile-apps[bot] marked this conversation as resolved.
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
SettingChip(
label = "${preset.frequency / 1_000_000.0} MHz",
Expand All @@ -424,6 +426,12 @@ private fun PopularPresetCard(
label = "${preset.txPower} dBm",
isSelected = isSelected,
)
preset.longTermAirtimeLimit?.let { limit ->
SettingChip(
label = "${limit}% LT",
isSelected = isSelected,
)
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3167,6 +3167,7 @@ class RNodeWizardViewModel
}

fun selectPreset(preset: RNodeRegionalPreset) {
val ltAlock = preset.longTermAirtimeLimit?.toString()
_state.update {
it.copy(
selectedPreset = preset,
Expand All @@ -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,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -487,7 +487,7 @@ class RNodeConfigValidatorTest {

@Test
fun `getMaxTxPower returns default without region`() {
assertEquals(22, RNodeConfigValidator.getMaxTxPower(null))
assertEquals(27, RNodeConfigValidator.getMaxTxPower(null))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading