diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
index 1b80cf32f..958977053 100644
--- a/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
@@ -15,6 +15,8 @@
* along with this program. If not, see .
*/
+@file:Suppress("MagicNumber")
+
package org.meshtastic.core.model
import org.jetbrains.compose.resources.StringResource
@@ -22,6 +24,7 @@ import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.label_long_fast
import org.meshtastic.core.strings.label_long_moderate
import org.meshtastic.core.strings.label_long_slow
+import org.meshtastic.core.strings.label_long_turbo
import org.meshtastic.core.strings.label_medium_fast
import org.meshtastic.core.strings.label_medium_slow
import org.meshtastic.core.strings.label_short_fast
@@ -306,13 +309,28 @@ enum class RegionInfo(
}
enum class ChannelOption(val modemPreset: ModemPreset, val labelRes: StringResource, val bandwidth: Float) {
- VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, Res.string.label_very_long_slow, .0625f),
- LONG_FAST(ModemPreset.LONG_FAST, Res.string.label_long_fast, .250f),
- LONG_MODERATE(ModemPreset.LONG_MODERATE, Res.string.label_long_moderate, .125f),
- LONG_SLOW(ModemPreset.LONG_SLOW, Res.string.label_long_slow, .125f),
- MEDIUM_FAST(ModemPreset.MEDIUM_FAST, Res.string.label_medium_fast, .250f),
- MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, Res.string.label_medium_slow, .250f),
- SHORT_TURBO(ModemPreset.SHORT_TURBO, Res.string.label_short_turbo, bandwidth = .500f),
- SHORT_FAST(ModemPreset.SHORT_FAST, Res.string.label_short_fast, .250f),
- SHORT_SLOW(ModemPreset.SHORT_SLOW, Res.string.label_short_slow, .250f),
+ // Grouped by range and speed for better readability
+ VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, Res.string.label_very_long_slow, 0.0625f),
+ LONG_TURBO(ModemPreset.LONG_TURBO, Res.string.label_long_turbo, 0.500f),
+ LONG_FAST(ModemPreset.LONG_FAST, Res.string.label_long_fast, 0.250f),
+ LONG_MODERATE(ModemPreset.LONG_MODERATE, Res.string.label_long_moderate, 0.125f),
+ LONG_SLOW(ModemPreset.LONG_SLOW, Res.string.label_long_slow, 0.125f),
+ MEDIUM_FAST(ModemPreset.MEDIUM_FAST, Res.string.label_medium_fast, 0.250f),
+ MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, Res.string.label_medium_slow, 0.250f),
+ SHORT_FAST(ModemPreset.SHORT_FAST, Res.string.label_short_fast, 0.250f),
+ SHORT_SLOW(ModemPreset.SHORT_SLOW, Res.string.label_short_slow, 0.250f),
+ SHORT_TURBO(ModemPreset.SHORT_TURBO, Res.string.label_short_turbo, 0.500f),
+ ;
+
+ companion object {
+ /** The default channel option for new configurations. */
+ val DEFAULT = LONG_FAST
+
+ /** Finds the ChannelOption corresponding to the given ModemPreset. Returns null if no match is found. */
+ fun from(modemPreset: ModemPreset?): ChannelOption? {
+ if (modemPreset == null) return null
+ // The `entries` property is preferred over `values()` since Kotlin 1.9
+ return entries.find { it.modemPreset == modemPreset }
+ }
+ }
}
diff --git a/core/model/src/test/kotlin/org/meshtastic/core/model/ChannelOptionTest.kt b/core/model/src/test/kotlin/org/meshtastic/core/model/ChannelOptionTest.kt
new file mode 100644
index 000000000..70197d8e2
--- /dev/null
+++ b/core/model/src/test/kotlin/org/meshtastic/core/model/ChannelOptionTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.core.model
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.ModemPreset
+
+class ChannelOptionTest {
+
+ /**
+ * This test ensures that every `ModemPreset` defined in the protobufs has a corresponding entry in our
+ * `ChannelOption` enum.
+ *
+ * If this test fails, it means a `ModemPreset` was added or changed in the firmware/protobufs, and you must update
+ * the `ChannelOption` enum to match.
+ */
+ @Test
+ fun `ensure every ModemPreset is mapped in ChannelOption`() {
+ // Get all possible ModemPreset values, excluding the ones we expect to ignore.
+ val unmappedPresets =
+ ModemPreset.entries.filter {
+ // UNRECOGNIZED is a system-generated value for forward compatibility.
+ it != ModemPreset.UNRECOGNIZED
+ }
+
+ unmappedPresets.forEach { preset ->
+ // Attempt to find the corresponding ChannelOption
+ val channelOption = ChannelOption.from(preset)
+
+ // Assert that a mapping exists, with a detailed failure message.
+ assertNotNull(
+ "Missing ChannelOption mapping for ModemPreset: '${preset.name}'. " +
+ "Please add a corresponding entry to the ChannelOption enum class.",
+ channelOption,
+ )
+ }
+ }
+
+ /**
+ * This test ensures that there are no extra entries in `ChannelOption` that don't correspond to a valid
+ * `ModemPreset`.
+ *
+ * If this test fails, it means a `ModemPreset` was removed from the protobufs, and you must remove the
+ * corresponding entry from the `ChannelOption` enum.
+ */
+ @Test
+ fun `ensure no extra mappings exist in ChannelOption`() {
+ val protoPresets = ModemPreset.entries.filter { it != ModemPreset.UNRECOGNIZED }.toSet()
+ val mappedPresets = ChannelOption.entries.map { it.modemPreset }.toSet()
+
+ assertEquals(
+ "The set of ModemPresets in protobufs does not match the set of ModemPresets mapped in ChannelOption. " +
+ "Check for removed presets in protobufs or duplicate mappings in ChannelOption.",
+ protoPresets,
+ mappedPresets,
+ )
+
+ assertEquals(
+ "Each ChannelOption must map to a unique ModemPreset.",
+ protoPresets.size,
+ ChannelOption.entries.size,
+ )
+ }
+}
diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml
index 977e66783..22ec6b345 100644
--- a/core/strings/src/commonMain/composeResources/values/strings.xml
+++ b/core/strings/src/commonMain/composeResources/values/strings.xml
@@ -130,6 +130,7 @@
Very Long Range - Slow
Long Range - Fast
+ Long Range - Turbo
Long Range - Moderate
Long Range - Slow
Medium Range - Fast