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