diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt index b333681722..239e750586 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt @@ -94,6 +94,10 @@ data class Channel( ModemPreset.LONG_MODERATE -> "LongMod" ModemPreset.VERY_LONG_SLOW -> "VLongSlow" ModemPreset.LONG_TURBO -> "LongTurbo" + ModemPreset.LITE_FAST -> "LiteFast" + ModemPreset.LITE_SLOW -> "LiteSlow" + ModemPreset.NARROW_FAST -> "NarrowFast" + ModemPreset.NARROW_SLOW -> "NarrowSlow" else -> "Invalid" } } else { 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 9589770531..14bad5fdde 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 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 @@ -14,19 +14,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - @file:Suppress("MagicNumber") package org.meshtastic.core.model import org.jetbrains.compose.resources.StringResource import org.meshtastic.core.strings.Res +import org.meshtastic.core.strings.label_lite_fast +import org.meshtastic.core.strings.label_lite_slow 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_narrow_fast +import org.meshtastic.core.strings.label_narrow_slow import org.meshtastic.core.strings.label_short_fast import org.meshtastic.core.strings.label_short_slow import org.meshtastic.core.strings.label_short_turbo @@ -34,7 +37,6 @@ import org.meshtastic.core.strings.label_very_long_slow import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.ModemPreset import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.RegionCode -import kotlin.math.floor /** hash a string into an integer using the djb2 algorithm by Dan Bernstein http://www.cse.yorku.ca/~oz/hash.html */ private fun hash(name: String): UInt { // using UInt instead of Long to match RadioInterface.cpp results @@ -75,10 +77,14 @@ val LoRaConfig.numChannels: Int val bw = bandwidth(regionInfo) if (bw <= 0f) return 1 // Return 1 if bandwidth is zero or negative - val num = floor((regionInfo.freqEnd - regionInfo.freqStart) / bw) + // Calculate number of channels: spacing = gap between channels (0 for continuous spectrum) + // Match firmware: uint32_t numChannels = round((myRegion->freqEnd - myRegion->freqStart) / channelSpacing); + val channelSpacing = regionInfo.spacing + bw + val num = Math.round((regionInfo.freqEnd - regionInfo.freqStart) / channelSpacing).toInt() + // If the regional frequency range is smaller than the bandwidth, the firmware would // fall back to a default preset. In the app, we return 1 to avoid a crash. - return if (num > 0) num.toInt() else 1 + return if (num > 0) num else 1 } internal fun LoRaConfig.channelNum(primaryName: String): Int = when { @@ -91,7 +97,13 @@ internal fun LoRaConfig.radioFreq(channelNum: Int): Float { if (overrideFrequency != 0f) return overrideFrequency + frequencyOffset val regionInfo = RegionInfo.fromRegionCode(region) return if (regionInfo != null) { - (regionInfo.freqStart + bandwidth(regionInfo) / 2) + (channelNum - 1) * bandwidth(regionInfo) + val bw = bandwidth(regionInfo) + val channelSpacing = regionInfo.spacing + bw + // The frequency calculation attempts to match firmware behavior, + // where channel_num is 0-indexed in the calculation, but 1-indexed in the app's channelNum function. + // Firmware example: float freq = myRegion->freqStart + myRegion->spacing + (bw / 2000) + (channel_num * + // channelSpacing); + (regionInfo.freqStart + regionInfo.spacing + bw / 2) + (channelNum - 1) * channelSpacing } else { 0f } @@ -105,6 +117,8 @@ internal fun LoRaConfig.radioFreq(channelNum: Int): Float { * @property freqStart The starting frequency in MHz * @property freqEnd The ending frequency in MHz * @property wideLora Whether the region uses wide Lora + * @property spacing The gap between channels in MHz + * @property defaultPreset The default modem preset for this region * @see * [LoRaWAN Regional Parameters](https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf) */ @@ -115,6 +129,8 @@ enum class RegionInfo( val freqStart: Float, val freqEnd: Float, val wideLora: Boolean = false, + val spacing: Float = 0f, + val defaultPreset: ModemPreset = ModemPreset.LONG_FAST, ) { /** This needs to be last. Same as US. */ UNSET(RegionCode.UNSET, "Please set a region", 902.0f, 928.0f), @@ -301,6 +317,28 @@ enum class RegionInfo( * @see [Firmware Issue #7399](https://github.com/meshtastic/firmware/pull/7399) */ BR_902(RegionCode.BR_902, "Brazil 902MHz", 902.0f, 907.5f, wideLora = false), + + /** EU 866MHz RFID band */ + EU_866( + RegionCode.EU_866, + "European Union 866MHz", + 865.6375f, + 867.5625f, + wideLora = false, + spacing = 0.475f, + defaultPreset = ModemPreset.LITE_FAST, + ), + + /** EU 868MHz band, with narrow presets */ + NARROW_868( + RegionCode.NARROW_868, + "European Union 868MHz (Narrow)", + 869.4f, + 869.65f, + wideLora = false, + spacing = 0.015f, + defaultPreset = ModemPreset.NARROW_FAST, + ), ; companion object { @@ -320,6 +358,10 @@ enum class ChannelOption(val modemPreset: ModemPreset, val labelRes: StringResou 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), + LITE_FAST(ModemPreset.LITE_FAST, Res.string.label_lite_fast, 0.125f), + LITE_SLOW(ModemPreset.LITE_SLOW, Res.string.label_lite_slow, 0.125f), + NARROW_FAST(ModemPreset.NARROW_FAST, Res.string.label_narrow_fast, 0.0625f), + NARROW_SLOW(ModemPreset.NARROW_SLOW, Res.string.label_narrow_slow, 0.0625f), ; companion object { 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 index 70197d8e2a..cdd0b5ce5a 100644 --- a/core/model/src/test/kotlin/org/meshtastic/core/model/ChannelOptionTest.kt +++ b/core/model/src/test/kotlin/org/meshtastic/core/model/ChannelOptionTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 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 @@ -14,13 +14,14 @@ * 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.ConfigKt.loRaConfig import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.ModemPreset +import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.RegionCode class ChannelOptionTest { @@ -78,4 +79,24 @@ class ChannelOptionTest { ChannelOption.entries.size, ) } + + @Test + fun `test radioFreq and numChannels for NARROW_868`() { + val loraConfig = loRaConfig { + region = RegionCode.NARROW_868 + usePreset = true + modemPreset = ModemPreset.NARROW_FAST + } + + // bw = 0.0625, spacing = 0.015, channelSpacing = 0.0775 + // Range = 869.65 - 869.4 = 0.25 + // numChannels = round(0.25 / 0.0775) = 3 + assertEquals(3, loraConfig.numChannels) + + // Slot 1: freqStart + spacing + bw/2 = 869.4 + 0.015 + 0.03125 = 869.44625 + assertEquals(869.44625f, loraConfig.radioFreq(1), 0.0001f) + + // Slot 3: 869.44625 + 2 * 0.0775 = 869.44625 + 0.155 = 869.60125 + assertEquals(869.60125f, loraConfig.radioFreq(3), 0.0001f) + } } diff --git a/core/proto/src/main/proto b/core/proto/src/main/proto index 77c8329a59..a7c51f5743 160000 --- a/core/proto/src/main/proto +++ b/core/proto/src/main/proto @@ -1 +1 @@ -Subproject commit 77c8329a59a9c96a61c447b5d5f1a52ca583e4f2 +Subproject commit a7c51f5743bff9d5e6ef7ae3ddcac37201edc5ec diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml index 8a0a4f4f83..10f1a2fd68 100644 --- a/core/strings/src/commonMain/composeResources/values/strings.xml +++ b/core/strings/src/commonMain/composeResources/values/strings.xml @@ -150,6 +150,10 @@ Short Range - Turbo Short Range - Fast Short Range - Slow + Lite - Fast + Lite - Slow + Narrow - Fast + Narrow - Slow Enabling WiFi will disable the bluetooth connection to the app. Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices. diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt index a83dba3823..3a0e4c7e02 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.settings.radio.component import androidx.compose.foundation.text.KeyboardActions @@ -96,7 +95,16 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { enabled = state.connected, items = RegionInfo.entries.map { it.regionCode to it.description }, selectedItem = formState.value.region, - onItemSelected = { formState.value = formState.value.copy { region = it } }, + onItemSelected = { + val regionInfo = RegionInfo.fromRegionCode(it) + formState.value = + formState.value.copy { + region = it + if (regionInfo != null) { + modemPreset = regionInfo.defaultPreset + } + } + }, ) HorizontalDivider() SwitchPreference(