Skip to content

Commit 57ca94e

Browse files
committed
fix: align channel frequency display with firmware
1 parent c819aa2 commit 57ca94e

2 files changed

Lines changed: 162 additions & 34 deletions

File tree

Meshtastic/Helpers/LoRaChannelCalculator.swift

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,48 @@ import Foundation
1616
/// Moved out of `Channels.swift` (was `private struct`) so the Mesh Beacons join
1717
/// enhancement can reuse the exact same math instead of hand-rolling a second hash.
1818
struct LoRaChannelCalculator {
19-
let config: LoRaConfigEntity?
19+
private let regionCode: Int
20+
private let usePreset: Bool
21+
private let modemPreset: Int
22+
private let channelNum: Int
23+
private let bandwidth: Int
24+
private let overrideFrequency: Double
25+
private let frequencyOffset: Double
26+
27+
init(config: LoRaConfigEntity?) {
28+
regionCode = Int(config?.regionCode ?? 0)
29+
usePreset = config?.usePreset ?? false
30+
modemPreset = Int(config?.modemPreset ?? 0)
31+
channelNum = Int(config?.channelNum ?? 0)
32+
bandwidth = Int(config?.bandwidth ?? 0)
33+
overrideFrequency = Double(config?.overrideFrequency ?? 0)
34+
frequencyOffset = Double(config?.frequencyOffset ?? 0)
35+
}
36+
37+
init(
38+
regionCode: Int,
39+
usePreset: Bool,
40+
modemPreset: Int,
41+
channelNum: Int,
42+
bandwidth: Int,
43+
overrideFrequency: Double,
44+
frequencyOffset: Double = 0
45+
) {
46+
self.regionCode = regionCode
47+
self.usePreset = usePreset
48+
self.modemPreset = modemPreset
49+
self.channelNum = channelNum
50+
self.bandwidth = bandwidth
51+
self.overrideFrequency = overrideFrequency
52+
self.frequencyOffset = frequencyOffset
53+
}
2054

2155
private var region: RegionInfo? {
22-
RegionInfo(regionCode: Int(config?.regionCode ?? 0))
56+
RegionInfo(regionCode: regionCode)
2357
}
2458

2559
var regionName: String {
26-
guard let regionCode = RegionCodes(rawValue: Int(config?.regionCode ?? 0)) else {
60+
guard let regionCode = RegionCodes(rawValue: regionCode) else {
2761
return "Unknown region"
2862
}
2963
return regionCode.description
@@ -33,8 +67,11 @@ struct LoRaChannelCalculator {
3367
/// (the firmware pins the slot when set); otherwise derives it from the primary
3468
/// channel name.
3569
func effectiveChannelSlot(primaryName: String) -> Int {
36-
if let channelNum = config?.channelNum, channelNum != 0 {
37-
return Int(channelNum)
70+
if channelNum != 0 {
71+
return channelNum
72+
}
73+
if let defaultSlot = region?.defaultSlot, defaultSlot > 0 {
74+
return defaultSlot
3875
}
3976
let numChannels = numChannels()
4077
guard numChannels > 0 else { return 0 }
@@ -51,30 +88,30 @@ struct LoRaChannelCalculator {
5188
}
5289

5390
func radioFrequencyMHz(slot: Int) -> Double {
54-
guard let config else { return 0 }
55-
if config.overrideFrequency != 0 {
56-
return Double(config.overrideFrequency)
91+
if overrideFrequency != 0 {
92+
return overrideFrequency + frequencyOffset
5793
}
5894
guard let region else { return 0 }
59-
let bandwidth = bandwidthMHz(region: region)
60-
guard bandwidth > 0, slot > 0 else { return 0 }
61-
return region.freqStart + bandwidth / 2 + Double(slot - 1) * bandwidth
95+
let bandwidthMHz = bandwidthMHz(region: region)
96+
guard bandwidthMHz > 0, slot > 0 else { return 0 }
97+
let slotWidth = region.spacing + 2 * region.padding + bandwidthMHz
98+
return region.freqStart + bandwidthMHz / 2 + region.padding + Double(slot - 1) * slotWidth + frequencyOffset
6299
}
63100

64101
private func numChannels() -> Int {
65102
guard let region else { return 0 }
66-
let bandwidth = bandwidthMHz(region: region)
67-
guard bandwidth > 0 else { return 1 }
68-
return max(Int(floor((region.freqEnd - region.freqStart) / bandwidth)), 1)
103+
let bandwidthMHz = bandwidthMHz(region: region)
104+
guard bandwidthMHz > 0 else { return 1 }
105+
let slotWidth = region.spacing + 2 * region.padding + bandwidthMHz
106+
return max(Int(((region.freqEnd - region.freqStart + region.spacing) / slotWidth).rounded()), 1)
69107
}
70108

71109
private func bandwidthMHz(region: RegionInfo) -> Double {
72-
guard let config else { return 0 }
73-
if config.usePreset {
74-
let presetBandwidth = ModemPresets(rawValue: Int(config.modemPreset))?.bandwidthMHz ?? 0
110+
if usePreset {
111+
let presetBandwidth = ModemPresets(rawValue: modemPreset)?.bandwidthMHz ?? 0
75112
return presetBandwidth * (region.wideLoRa ? 3.25 : 1)
76113
}
77-
switch config.bandwidth {
114+
switch bandwidth {
78115
case 31:
79116
return 0.03125
80117
case 62:
@@ -88,7 +125,7 @@ struct LoRaChannelCalculator {
88125
case 1600:
89126
return 1.625
90127
default:
91-
return Double(config.bandwidth) / 1000
128+
return Double(bandwidth) / 1000
92129
}
93130
}
94131

@@ -185,6 +222,9 @@ struct RegionInfo {
185222
let freqStart: Double
186223
let freqEnd: Double
187224
let wideLoRa: Bool
225+
let spacing: Double
226+
let padding: Double
227+
let defaultSlot: Int
188228

189229
init?(regionCode: Int) {
190230
guard let region = RegionCodes(rawValue: regionCode) else { return nil }
@@ -241,38 +281,45 @@ struct RegionInfo {
241281
self.init(freqStart: 865.0, freqEnd: 868.0)
242282
case .br902:
243283
self.init(freqStart: 902.0, freqEnd: 907.5)
244-
case .itu12M, .itu22M:
245-
self.init(freqStart: 144.0, freqEnd: 148.0)
284+
case .itu12M:
285+
self.init(freqStart: 144.0, freqEnd: 146.0, padding: 0.0022, defaultSlot: 26)
286+
case .itu22M:
287+
self.init(freqStart: 144.0, freqEnd: 148.0, padding: 0.0022, defaultSlot: 51)
246288
case .eu866:
247289
self.init(freqStart: 866.0, freqEnd: 866.5)
248290
case .eu874:
249291
self.init(freqStart: 873.0, freqEnd: 876.0)
250292
case .eu917:
251293
self.init(freqStart: 917.0, freqEnd: 921.0)
252294
case .euN868:
253-
self.init(freqStart: 869.4, freqEnd: 869.65)
295+
self.init(freqStart: 869.4, freqEnd: 869.65, padding: 0.0104, defaultSlot: 1)
254296
case .itu32M:
255-
// ITU Region 3 Amateur Radio 2m band.
256-
self.init(freqStart: 144.0, freqEnd: 148.0)
297+
self.init(freqStart: 144.0, freqEnd: 148.0, padding: 0.0022, defaultSlot: 33)
257298
case .itu170Cm:
258-
// ITU Region 1 Amateur Radio 70cm band.
259-
self.init(freqStart: 430.0, freqEnd: 440.0)
299+
self.init(freqStart: 430.0, freqEnd: 440.0, padding: 0.01875, defaultSlot: 37)
260300
case .itu270Cm:
261-
// ITU Region 2 Amateur Radio 70cm band.
262-
self.init(freqStart: 420.0, freqEnd: 450.0)
301+
self.init(freqStart: 420.0, freqEnd: 450.0, padding: 0.01875, defaultSlot: 137)
263302
case .itu370Cm:
264-
// ITU Region 3 Amateur Radio 70cm band.
265-
self.init(freqStart: 430.0, freqEnd: 450.0)
303+
self.init(freqStart: 430.0, freqEnd: 450.0, padding: 0.01875, defaultSlot: 37)
266304
case .itu2125Cm:
267-
// ITU Region 2 Amateur Radio 1.25m (125cm) band.
268-
self.init(freqStart: 220.0, freqEnd: 225.0)
305+
self.init(freqStart: 220.0, freqEnd: 225.0, padding: 0.01875, defaultSlot: 37)
269306
}
270307
}
271308

272-
private init(freqStart: Double, freqEnd: Double, wideLoRa: Bool = false) {
309+
private init(
310+
freqStart: Double,
311+
freqEnd: Double,
312+
wideLoRa: Bool = false,
313+
spacing: Double = 0,
314+
padding: Double = 0,
315+
defaultSlot: Int = 0
316+
) {
273317
self.freqStart = freqStart
274318
self.freqEnd = freqEnd
275319
self.wideLoRa = wideLoRa
320+
self.spacing = spacing
321+
self.padding = padding
322+
self.defaultSlot = defaultSlot
276323
}
277324
}
278325

@@ -299,7 +346,7 @@ extension ModemPresets {
299346
case .narrowFast, .narrowSlow:
300347
return 0.0625
301348
case .tinyFast, .tinySlow:
302-
return 0.020
349+
return 0.0156
303350
}
304351
}
305352
}

MeshtasticTests/LoraDeviceEnumTests.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,3 +840,84 @@ struct LoRaPresetSelectionTests {
840840
#expect(result == nil)
841841
}
842842
}
843+
844+
@Suite("LoRa channel frequency calculation")
845+
struct LoRaChannelCalculatorTests {
846+
@Test("ITU Region 2 TinyFast defaults to firmware slot 51 at 145.010 MHz")
847+
func itu2TinyFastDefault() {
848+
let calculator = LoRaChannelCalculator(
849+
regionCode: RegionCodes.itu22M.rawValue,
850+
usePreset: true,
851+
modemPreset: ModemPresets.tinyFast.rawValue,
852+
channelNum: 0,
853+
bandwidth: 0,
854+
overrideFrequency: 0
855+
)
856+
857+
let slot = calculator.effectiveChannelSlot(primaryName: "TinyFast")
858+
#expect(slot == 51)
859+
#expect(abs(calculator.radioFrequencyMHz(slot: slot) - 145.010) < 0.001)
860+
}
861+
862+
@Test("ITU Region 2 TinyFast explicit slots advance by 20 kHz")
863+
func itu2TinyFastExplicitSlot() {
864+
let calculator = LoRaChannelCalculator(
865+
regionCode: RegionCodes.itu22M.rawValue,
866+
usePreset: true,
867+
modemPreset: ModemPresets.tinyFast.rawValue,
868+
channelNum: 52,
869+
bandwidth: 0,
870+
overrideFrequency: 0
871+
)
872+
873+
let slot = calculator.effectiveChannelSlot(primaryName: "TinyFast")
874+
#expect(slot == 52)
875+
#expect(abs(calculator.radioFrequencyMHz(slot: slot) - 145.030) < 0.001)
876+
}
877+
878+
@Test("Channel frequency applies the configured offset after the firmware slot calculation")
879+
func appliesFrequencyOffset() {
880+
let calculator = LoRaChannelCalculator(
881+
regionCode: RegionCodes.itu22M.rawValue,
882+
usePreset: true,
883+
modemPreset: ModemPresets.tinyFast.rawValue,
884+
channelNum: 0,
885+
bandwidth: 0,
886+
overrideFrequency: 0,
887+
frequencyOffset: 0.010
888+
)
889+
890+
let slot = calculator.effectiveChannelSlot(primaryName: "TinyFast")
891+
#expect(abs(calculator.radioFrequencyMHz(slot: slot) - 145.020) < 0.001)
892+
}
893+
894+
@Test("ITU Region 2 1.25m uses the 100 kHz ham channel plan")
895+
func itu2OnePointTwoFiveMeterDefault() {
896+
let calculator = LoRaChannelCalculator(
897+
regionCode: RegionCodes.itu2125Cm.rawValue,
898+
usePreset: true,
899+
modemPreset: ModemPresets.narrowSlow.rawValue,
900+
channelNum: 0,
901+
bandwidth: 0,
902+
overrideFrequency: 0
903+
)
904+
905+
let slot = calculator.effectiveChannelSlot(primaryName: "NarrowSlow")
906+
#expect(slot == 37)
907+
#expect(abs(calculator.radioFrequencyMHz(slot: slot) - 223.650) < 0.001)
908+
}
909+
910+
@Test("US LongFast keeps standard channel centers")
911+
func usLongFast() {
912+
let calculator = LoRaChannelCalculator(
913+
regionCode: RegionCodes.us.rawValue,
914+
usePreset: true,
915+
modemPreset: ModemPresets.longFast.rawValue,
916+
channelNum: 1,
917+
bandwidth: 0,
918+
overrideFrequency: 0
919+
)
920+
921+
#expect(abs(calculator.radioFrequencyMHz(slot: 1) - 902.125) < 0.001)
922+
}
923+
}

0 commit comments

Comments
 (0)