Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions Meshtastic/Enums/LoraConfigEnums.swift
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,16 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
return "NarrowSlow"
}
}
var defaultCodingRate: Int {
switch self {
case .longTurbo, .longModerate, .longSlow:
return 8
case .narrowFast, .narrowSlow:
return 6
default:
return 5
}
}
func snrLimit() -> Float {
switch self {
case .longFast:
Expand Down Expand Up @@ -569,6 +579,37 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
}
}

enum CodingRates {
static let validRange = 5...8

static func options(usePreset: Bool, modemPreset: ModemPresets?) -> [Int] {
guard usePreset else {
return Array(validRange)
}
let defaultCodingRate = modemPreset?.defaultCodingRate ?? ModemPresets.longFast.defaultCodingRate
return [0] + validRange.filter { $0 > defaultCodingRate }
}

static func normalized(_ codingRate: Int, usePreset: Bool, modemPreset: ModemPresets?) -> Int {
let options = options(usePreset: usePreset, modemPreset: modemPreset)
if options.contains(codingRate) {
return codingRate
}
if usePreset {
return 0
}
return validRange.lowerBound
}

static func description(for codingRate: Int, modemPreset: ModemPresets?) -> String {
if codingRate == 0 {
let defaultCodingRate = modemPreset?.defaultCodingRate ?? ModemPresets.longFast.defaultCodingRate
return String.localizedStringWithFormat("Preset Default (4/%d)".localized, defaultCodingRate)
}
return "4/\(codingRate)"
}
}

enum Bandwidths: Int, CaseIterable, Identifiable {

case thirtyOne = 31
Expand Down
131 changes: 120 additions & 11 deletions Meshtastic/Views/Settings/Config/LoRaConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,60 @@ struct LoRaConfig: View {

let node: NodeInfoEntity?

private var selectedModemPreset: ModemPresets {
ModemPresets(rawValue: modemPreset) ?? .longFast
}

private var normalizedCodingRate: Int {
CodingRates.normalized(codingRate, usePreset: usePreset, modemPreset: selectedModemPreset)
}

private var defaultCodingRate: Int {
selectedModemPreset.defaultCodingRate
}

private var canOverridePresetCodingRate: Bool {
defaultCodingRate < CodingRates.validRange.upperBound
}

private var usePresetCodingRate: Binding<Bool> {
Binding(
get: { normalizedCodingRate == 0 },
set: { useDefault in
if useDefault || !canOverridePresetCodingRate {
codingRate = 0
} else {
codingRate = defaultCodingRate + 1
}
}
)
}

private var presetCodingRateSliderValue: Binding<Double> {
Binding(
get: {
let firstOverride = defaultCodingRate + 1
return Double(max(normalizedCodingRate, firstOverride))
},
set: { newValue in
codingRate = Int(newValue.rounded())
}
)
}

private var customCodingRateSliderValue: Binding<Double> {
Binding(
get: { Double(normalizedCodingRate) },
set: { newValue in
codingRate = CodingRates.normalized(
Int(newValue.rounded()),
usePreset: false,
modemPreset: selectedModemPreset
)
}
)
}

@State var hasChanges = false
@State var region: Int = 0
@State var modemPreset = 0
Expand Down Expand Up @@ -126,15 +180,60 @@ struct LoRaConfig: View {
}
}
}
HStack {
Picker("Coding Rate", selection: $codingRate) {
ForEach(5..<9) {
Text("\($0)")
.tag($0)
}
}
}
}

VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Coding Rate")
Spacer()
Text(CodingRates.description(for: normalizedCodingRate, modemPreset: selectedModemPreset))
.foregroundColor(.secondary)
}
if usePreset {
Toggle("Follow Preset Coding Rate", isOn: usePresetCodingRate)
.tint(.accentColor)
if !canOverridePresetCodingRate {
Text("This preset already uses 4/\(defaultCodingRate), the highest redundancy available.")
.foregroundColor(.gray)
.font(.callout)
} else if normalizedCodingRate == 0 {
Text("Uses \(selectedModemPreset.description)'s 4/\(defaultCodingRate) coding rate. Turn this off only when nearby nodes use the same preset and you want extra error correction on noisy links.")
.foregroundColor(.gray)
.font(.callout)
} else {
Slider(
value: presetCodingRateSliderValue,
in: Double(defaultCodingRate + 1)...Double(CodingRates.validRange.upperBound),
step: 1
) {
Text("Coding Rate")
} minimumValueLabel: {
Text("4/\(defaultCodingRate + 1)")
} maximumValueLabel: {
Text("4/\(CodingRates.validRange.upperBound)")
}
Text("Uses 4/\(normalizedCodingRate) while keeping the \(selectedModemPreset.description) bandwidth and spread factor. Higher values add error correction, but each packet uses more airtime and has less throughput.")
.foregroundColor(.gray)
.font(.callout)
}
} else {
Slider(
value: customCodingRateSliderValue,
in: Double(CodingRates.validRange.lowerBound)...Double(CodingRates.validRange.upperBound),
step: 1
) {
Text("Coding Rate")
} minimumValueLabel: {
Text("4/\(CodingRates.validRange.lowerBound)")
} maximumValueLabel: {
Text("4/\(CodingRates.validRange.upperBound)")
}
Text("Coding rate controls error-correction redundancy. Higher values can help noisy links, but reduce throughput and increase airtime. Keep 4/5 unless your channel plan calls for a different value.")
.foregroundColor(.gray)
.font(.callout)
}
}

VStack(alignment: .leading) {
Picker("Number of hops", selection: $hopLimit) {
ForEach(0..<8) {
Expand Down Expand Up @@ -203,7 +302,7 @@ struct LoRaConfig: View {
lc.txPower = Int32(txPower)
lc.channelNum = UInt32(channelNum)
lc.bandwidth = UInt32(bandwidth)
lc.codingRate = UInt32(codingRate)
lc.codingRate = UInt32(normalizedCodingRate)
lc.spreadFactor = UInt32(spreadFactor)
lc.sx126XRxBoostedGain = rxBoostedGain
lc.overrideFrequency = overrideFrequency
Expand Down Expand Up @@ -238,9 +337,11 @@ struct LoRaConfig: View {
if newRegion != node?.loRaConfig?.regionCode ?? -1 { hasChanges = true }
}
.onChange(of: usePreset) { _, newPreset in
codingRate = CodingRates.normalized(codingRate, usePreset: newPreset, modemPreset: selectedModemPreset)
if newPreset != node?.loRaConfig?.usePreset { hasChanges = true }
}
.onChange(of: modemPreset) { _, newModemPreset in
codingRate = CodingRates.normalized(codingRate, usePreset: usePreset, modemPreset: ModemPresets(rawValue: newModemPreset) ?? .longFast)
if newModemPreset != node?.loRaConfig?.modemPreset ?? -1 { hasChanges = true }
}
.onChange(of: hopLimit) { _, newHopLimit in
Expand All @@ -253,7 +354,11 @@ struct LoRaConfig: View {
if newBandwidth != node?.loRaConfig?.bandwidth ?? -1 { hasChanges = true }
}
.onChange(of: codingRate) { _, newCodingRate in
if newCodingRate != node?.loRaConfig?.codingRate ?? -1 { hasChanges = true }
let normalizedNewCodingRate = CodingRates.normalized(newCodingRate, usePreset: usePreset, modemPreset: selectedModemPreset)
if normalizedNewCodingRate != newCodingRate {
codingRate = normalizedNewCodingRate
}
if normalizedNewCodingRate != node?.loRaConfig?.codingRate ?? -1 { hasChanges = true }
}
.onChange(of: spreadFactor) { _, newSpreadFactor in
if newSpreadFactor != node?.loRaConfig?.spreadFactor ?? -1 { hasChanges = true }
Expand Down Expand Up @@ -290,7 +395,11 @@ struct LoRaConfig: View {
self.channelNum = Int(node?.loRaConfig?.channelNum ?? 0)
self.bandwidth = Int(node?.loRaConfig?.bandwidth ?? 0)
let loadedCodingRate = Int(node?.loRaConfig?.codingRate ?? 0)
self.codingRate = loadedCodingRate == 0 ? 5 : loadedCodingRate
self.codingRate = CodingRates.normalized(
loadedCodingRate,
usePreset: self.usePreset,
modemPreset: ModemPresets(rawValue: self.modemPreset) ?? .longFast
)
self.spreadFactor = Int(node?.loRaConfig?.spreadFactor ?? 0)
self.rxBoostedGain = node?.loRaConfig?.sx126xRxBoostedGain ?? false
self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.0
Expand Down
38 changes: 38 additions & 0 deletions MeshtasticTests/LoraDeviceEnumTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,49 @@ struct ModemPresetsTests {
#expect(ModemPresets.shortTurbo.snrLimit() == -7.5)
}

@Test func defaultCodingRate_matchesPresetParams() {
#expect(ModemPresets.longFast.defaultCodingRate == 5)
#expect(ModemPresets.longSlow.defaultCodingRate == 8)
#expect(ModemPresets.longModerate.defaultCodingRate == 8)
#expect(ModemPresets.longTurbo.defaultCodingRate == 8)
#expect(ModemPresets.narrowFast.defaultCodingRate == 6)
#expect(ModemPresets.narrowSlow.defaultCodingRate == 6)
}

@Test func totalCaseCount() {
#expect(ModemPresets.allCases.count == 13)
}
}

// MARK: - CodingRates

@Suite("CodingRates")
struct CodingRatesTests {

@Test func customMode_allowsExplicitCodingRatesOnly() {
#expect(CodingRates.options(usePreset: false, modemPreset: .longFast) == [5, 6, 7, 8])
}

@Test func presetMode_allowsAutoAndHigherRedundancyOnly() {
#expect(CodingRates.options(usePreset: true, modemPreset: .longFast) == [0, 6, 7, 8])
#expect(CodingRates.options(usePreset: true, modemPreset: .narrowFast) == [0, 7, 8])
#expect(CodingRates.options(usePreset: true, modemPreset: .longSlow) == [0])
}

@Test func presetMode_normalizesInvalidValuesToAuto() {
#expect(CodingRates.normalized(0, usePreset: true, modemPreset: .longFast) == 0)
#expect(CodingRates.normalized(5, usePreset: true, modemPreset: .longFast) == 0)
#expect(CodingRates.normalized(5, usePreset: true, modemPreset: .longSlow) == 0)
#expect(CodingRates.normalized(8, usePreset: true, modemPreset: .longSlow) == 0)
#expect(CodingRates.normalized(9, usePreset: true, modemPreset: .longFast) == 0)
}

@Test func customMode_normalizesInvalidValuesToLowestCodingRate() {
#expect(CodingRates.normalized(0, usePreset: false, modemPreset: .longFast) == 5)
#expect(CodingRates.normalized(9, usePreset: false, modemPreset: .longFast) == 5)
}
}

// MARK: - Bandwidths

@Suite("Bandwidths")
Expand Down
Loading