From 1ad756205ffaec24c446e7545ff08f93da524d40 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:27:46 -0500 Subject: [PATCH] feat(lora): default US region to LongTurbo preset (#6009) Co-authored-by: Claude Opus 4.8 --- .../core/model/RegionPresetConstraint.kt | 13 +++++++++++++ .../core/model/LoRaRegionPresetsTest.kt | 6 ++++++ .../settings/radio/channel/ChannelScreen.kt | 10 +++++++++- .../radio/component/LoRaConfigItemList.kt | 16 +++++++++++++++- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RegionPresetConstraint.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RegionPresetConstraint.kt index 9222ed6f9..16df1c730 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RegionPresetConstraint.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RegionPresetConstraint.kt @@ -73,3 +73,16 @@ fun LoRaRegionPresetMap?.repairPresetFor(region: RegionCode, current: ModemPrese else -> constraint.presets.firstOrNull() ?: current } } + +/** + * The app's built-in default modem presets for regions whose default differs from the global [ChannelOption.DEFAULT]. + * Mirrors the per-region defaults newer firmware advertises in [LoRaRegionPresetMap], so the default channel and a + * fresh setup over old firmware (which sends no map) land on the same preset a new node would. + */ +private val REGION_DEFAULT_PRESETS = mapOf(RegionCode.US to ModemPreset.LONG_TURBO) + +/** + * The app's preferred default preset for [region], or `null` when it has no region-specific default and the caller + * should fall back to [ChannelOption.DEFAULT] / the current preset. + */ +fun defaultPresetFor(region: RegionCode): ModemPreset? = REGION_DEFAULT_PRESETS[region] diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LoRaRegionPresetsTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LoRaRegionPresetsTest.kt index ba6dac7d5..501acaa32 100644 --- a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LoRaRegionPresetsTest.kt +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/LoRaRegionPresetsTest.kt @@ -138,4 +138,10 @@ class LoRaRegionPresetsTest { ) assertEquals(ModemPreset.MEDIUM_FAST, oddMap.repairPresetFor(RegionCode.US, ModemPreset.SHORT_FAST)) } + + @Test + fun `region default preset is LongTurbo for US and absent for other regions`() { + assertEquals(ModemPreset.LONG_TURBO, defaultPresetFor(RegionCode.US)) + assertNull(defaultPresetFor(RegionCode.EU_868)) + } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt index 2f07b7910..03b360ba7 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/channel/ChannelScreen.kt @@ -63,6 +63,7 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import org.meshtastic.core.model.Channel import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.defaultPresetFor import org.meshtastic.core.model.util.getChannelUrl import org.meshtastic.core.navigation.Route import org.meshtastic.core.resources.Res @@ -198,8 +199,15 @@ fun ChannelScreen( messageRes = Res.string.are_you_sure_change_default, onConfirm = { Logger.d { "Switching back to default channel" } + // The default channel takes the region's preferred preset (e.g. US -> LongTurbo) so its derived name, + // hash, and frequency match what a freshly-set-up node in that region would use. + val preset = defaultPresetFor(viewModel.region) ?: Channel.default.loraConfig.modem_preset val lora = - (Channel.default.loraConfig).copy(region = viewModel.region, tx_enabled = viewModel.txEnabled) + Channel.default.loraConfig.copy( + region = viewModel.region, + modem_preset = preset, + tx_enabled = viewModel.txEnabled, + ) installSettings(Channel.default.settings, lora) showResetDialog = false }, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt index 55fd505ee..5805f86aa 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt @@ -33,6 +33,7 @@ import org.meshtastic.core.model.ChannelOption import org.meshtastic.core.model.RegionInfo import org.meshtastic.core.model.RegionPresetConstraint import org.meshtastic.core.model.constraintFor +import org.meshtastic.core.model.defaultPresetFor import org.meshtastic.core.model.numChannels import org.meshtastic.core.model.repairPresetFor import org.meshtastic.core.resources.Res @@ -70,6 +71,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel import org.meshtastic.feature.settings.util.hopLimits import org.meshtastic.proto.Config import org.meshtastic.proto.Config.LoRaConfig.ModemPreset +import org.meshtastic.proto.Config.LoRaConfig.RegionCode private val SPREAD_FACTOR_RANGE = 7..12 private val CODING_RATE_RANGE = 5..8 @@ -159,10 +161,22 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { items = RegionInfo.entries.map { it.regionCode to it.description }, selectedItem = formState.value.region, onItemSelected = { region -> + val freshSetup = formState.value.region == RegionCode.UNSET // When the region changes, snap the preset to the region's default if the current one is // no longer legal there (R7); a no-op when the region is unconstrained. val repaired = regionPresetMap.repairPresetFor(region, formState.value.modem_preset) - formState.value = formState.value.copy(region = region, modem_preset = repaired) + // At fresh setup (region was UNSET) adopt the region's advertised default rather than keeping + // a merely-legal preset: the firmware map's default when present, else the app's built-in + // default (e.g. US -> LongTurbo on pre-2.8 firmware that sends no map). + val preset = + if (freshSetup) { + regionPresetMap.constraintFor(region)?.defaultPreset + ?: defaultPresetFor(region) + ?: repaired + } else { + repaired + } + formState.value = formState.value.copy(region = region, modem_preset = preset) }, ) HorizontalDivider()