11/*
2- * Copyright (c) 2025 Meshtastic LLC
2+ * Copyright (c) 2025-2026 Meshtastic LLC
33 *
44 * This program is free software: you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
1414 * You should have received a copy of the GNU General Public License
1515 * along with this program. If not, see <https://www.gnu.org/licenses/>.
1616 */
17-
1817@file:Suppress(" MagicNumber" )
1918
2019package org.meshtastic.core.model
2120
2221import org.jetbrains.compose.resources.StringResource
2322import org.meshtastic.core.strings.Res
23+ import org.meshtastic.core.strings.label_lite_fast
24+ import org.meshtastic.core.strings.label_lite_slow
2425import org.meshtastic.core.strings.label_long_fast
2526import org.meshtastic.core.strings.label_long_moderate
2627import org.meshtastic.core.strings.label_long_slow
2728import org.meshtastic.core.strings.label_long_turbo
2829import org.meshtastic.core.strings.label_medium_fast
2930import org.meshtastic.core.strings.label_medium_slow
31+ import org.meshtastic.core.strings.label_narrow_fast
32+ import org.meshtastic.core.strings.label_narrow_slow
3033import org.meshtastic.core.strings.label_short_fast
3134import org.meshtastic.core.strings.label_short_slow
3235import org.meshtastic.core.strings.label_short_turbo
3336import org.meshtastic.core.strings.label_very_long_slow
3437import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig
3538import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.ModemPreset
3639import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.RegionCode
37- import kotlin.math.floor
3840
3941/* * hash a string into an integer using the djb2 algorithm by Dan Bernstein http://www.cse.yorku.ca/~oz/hash.html */
4042private fun hash (name : String ): UInt { // using UInt instead of Long to match RadioInterface.cpp results
@@ -75,10 +77,15 @@ val LoRaConfig.numChannels: Int
7577 val bw = bandwidth(regionInfo)
7678 if (bw <= 0f ) return 1 // Return 1 if bandwidth is zero or negative
7779
78- val num = floor((regionInfo.freqEnd - regionInfo.freqStart) / bw)
80+ // Calculate number of channels: spacing = gap between channels (0 for continuous spectrum)
81+ // Match firmware: uint32_t numChannels = round((myRegion->freqEnd - myRegion->freqStart + myRegion->spacing) /
82+ // channelSpacing);
83+ val channelSpacing = regionInfo.spacing + bw
84+ val num = Math .round((regionInfo.freqEnd - regionInfo.freqStart + regionInfo.spacing) / channelSpacing).toInt()
85+
7986 // If the regional frequency range is smaller than the bandwidth, the firmware would
8087 // fall back to a default preset. In the app, we return 1 to avoid a crash.
81- return if (num > 0 ) num.toInt() else 1
88+ return if (num > 0 ) num else 1
8289 }
8390
8491internal fun LoRaConfig.channelNum (primaryName : String ): Int = when {
@@ -91,7 +98,15 @@ internal fun LoRaConfig.radioFreq(channelNum: Int): Float {
9198 if (overrideFrequency != 0f ) return overrideFrequency + frequencyOffset
9299 val regionInfo = RegionInfo .fromRegionCode(region)
93100 return if (regionInfo != null ) {
94- (regionInfo.freqStart + bandwidth(regionInfo) / 2 ) + (channelNum - 1 ) * bandwidth(regionInfo)
101+ val bw = bandwidth(regionInfo)
102+ val channelSpacing = regionInfo.spacing + bw
103+ // Match firmware: float freq = myRegion->freqStart + (bw / 2000) + (channel_num * channelSpacing);
104+ // Note: firmware channel_num is 0-indexed in the calculation, but the app uses 1-indexed for some reason?
105+ // Let's re-verify the firmware logic.
106+ // Firmware: channel_num = hash(channelName) % numChannels;
107+ // freq = myRegion->freqStart + (bw / 2000) + (channel_num * channelSpacing);
108+ // The app's channelNum function returns 1..numChannels if channelNum is 0.
109+ (regionInfo.freqStart + bw / 2 ) + (channelNum - 1 ) * channelSpacing
95110 } else {
96111 0f
97112 }
@@ -105,6 +120,8 @@ internal fun LoRaConfig.radioFreq(channelNum: Int): Float {
105120 * @property freqStart The starting frequency in MHz
106121 * @property freqEnd The ending frequency in MHz
107122 * @property wideLora Whether the region uses wide Lora
123+ * @property spacing The gap between channels in MHz
124+ * @property defaultPreset The default modem preset for this region
108125 * @see
109126 * [LoRaWAN Regional Parameters](https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf)
110127 */
@@ -115,6 +132,8 @@ enum class RegionInfo(
115132 val freqStart : Float ,
116133 val freqEnd : Float ,
117134 val wideLora : Boolean = false ,
135+ val spacing : Float = 0f ,
136+ val defaultPreset : ModemPreset = ModemPreset .LONG_FAST ,
118137) {
119138 /* * This needs to be last. Same as US. */
120139 UNSET (RegionCode .UNSET , " Please set a region" , 902.0f , 928.0f ),
@@ -301,6 +320,38 @@ enum class RegionInfo(
301320 * @see [Firmware Issue #7399](https://github.com/meshtastic/firmware/pull/7399)
302321 */
303322 BR_902 (RegionCode .BR_902 , " Brazil 902MHz" , 902.0f , 907.5f , wideLora = false ),
323+
324+ /* * EU 866MHz RFID band */
325+ EU_866 (
326+ RegionCode .EU_866 ,
327+ " European Union 866MHz" ,
328+ 865.6375f ,
329+ 867.5625f ,
330+ wideLora = false ,
331+ spacing = 0.475f ,
332+ defaultPreset = ModemPreset .LITE_FAST ,
333+ ),
334+
335+ /* * EU 868MHz band, with narrow presets */
336+ NARROW_868 (
337+ RegionCode .NARROW_868 ,
338+ " European Union 868MHz (Narrow)" ,
339+ 869.4f ,
340+ 869.65f ,
341+ wideLora = false ,
342+ spacing = 0.015f ,
343+ defaultPreset = ModemPreset .NARROW_FAST ,
344+ ),
345+
346+ /* * US 433MHz Amateur Use band */
347+ HAM_US433 (
348+ RegionCode .HAM_US433 ,
349+ " United States 433MHz (Amateur)" ,
350+ 430.0f ,
351+ 450.0f ,
352+ wideLora = false ,
353+ defaultPreset = ModemPreset .NARROW_SLOW ,
354+ ),
304355 ;
305356
306357 companion object {
@@ -320,6 +371,10 @@ enum class ChannelOption(val modemPreset: ModemPreset, val labelRes: StringResou
320371 SHORT_FAST (ModemPreset .SHORT_FAST , Res .string.label_short_fast, 0.250f ),
321372 SHORT_SLOW (ModemPreset .SHORT_SLOW , Res .string.label_short_slow, 0.250f ),
322373 SHORT_TURBO (ModemPreset .SHORT_TURBO , Res .string.label_short_turbo, 0.500f ),
374+ LITE_FAST (ModemPreset .LITE_FAST , Res .string.label_lite_fast, 0.125f ),
375+ LITE_SLOW (ModemPreset .LITE_SLOW , Res .string.label_lite_slow, 0.125f ),
376+ NARROW_FAST (ModemPreset .NARROW_FAST , Res .string.label_narrow_fast, 0.0625f ),
377+ NARROW_SLOW (ModemPreset .NARROW_SLOW , Res .string.label_narrow_slow, 0.0625f ),
323378 ;
324379
325380 companion object {
0 commit comments