Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
62 changes: 62 additions & 0 deletions Meshtastic/Enums/LoraConfigEnums.swift
Original file line number Diff line number Diff line change
Expand Up @@ -775,10 +775,37 @@ enum Bandwidths: Int, CaseIterable, Identifiable {
case thirtyOne = 31
case sixtyTwo = 62
case oneHundredTwentyFive = 125
case twoHundred = 200
case twoHundredFifty = 250
case fourHundred = 400
case fiveHundred = 500
case eightHundred = 800
case sixteenHundred = 1600

private static let subGHzCases: [Bandwidths] = [
.thirtyOne, .sixtyTwo, .oneHundredTwentyFive, .twoHundredFifty, .fiveHundred
]

private static let highBandCases: [Bandwidths] = [
.twoHundred, .fourHundred, .eightHundred
]

/// Targets whose firmware definitions select SX128x exclusively. Targets that can be built
/// with more than one radio, plus unknown future targets, use the conservative high-band set.
private static let sx128xTargets: Set<String> = [
"betafpv_2400_tx_micro",
"makerpython_nrf52840_sx1280_eink",
"makerpython_nrf52840_sx1280_oled",
"tlora-v2-1-1_8"
]

var id: Int { self.rawValue }

/// The protobuf uses zero as the default 250 kHz sub-GHz value.
var pickerValue: Int {
self == .twoHundredFifty ? 0 : rawValue
}

var description: String {
switch self {
case .thirtyOne:
Expand All @@ -787,12 +814,47 @@ enum Bandwidths: Int, CaseIterable, Identifiable {
return "62 kHz"
case .oneHundredTwentyFive:
return "125 kHz"
case .twoHundred:
return "203.125 kHz"
case .twoHundredFifty:
return "250 kHz"
case .fourHundred:
return "406.25 kHz"
case .fiveHundred:
return "500 kHz"
case .eightHundred:
return "812.5 kHz"
case .sixteenHundred:
return "1625 kHz"
}
}

static func selectable(region: RegionCodes?, pioEnv: String?) -> [Bandwidths] {
guard region == .lora24 else { return subGHzCases }

var options = highBandCases
if let pioEnv, sx128xTargets.contains(pioEnv.lowercased()) {
options.append(.sixteenHundred)
}
return options
}

static func isValid(_ value: Int, region: RegionCodes?, pioEnv: String?) -> Bool {
let normalizedValue = pickerValue(forStoredValue: value, region: region)
return selectable(region: region, pioEnv: pioEnv).contains { $0.pickerValue == normalizedValue }
}

static func pickerValue(forStoredValue value: Int, region: RegionCodes?) -> Int {
if region != .lora24, value == Bandwidths.twoHundredFifty.rawValue {
return Bandwidths.twoHundredFifty.pickerValue
}
return value
}

static func description(forPickerValue value: Int) -> String {
if value == 0 { return Bandwidths.twoHundredFifty.description }
return Bandwidths(rawValue: value)?.description ?? "\(value) kHz"
}
}

///
Expand Down
148 changes: 102 additions & 46 deletions Meshtastic/Views/Settings/Config/LoRaConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,33 @@ struct LoRaConfig: View {
return presets
}

private var availableBandwidths: [Bandwidths] {
Bandwidths.selectable(
region: RegionCodes(rawValue: region),
pioEnv: node?.myInfo?.pioEnv
)
}

private var customBandwidthIsValid: Bool {
usePreset || Bandwidths.isValid(
bandwidth,
region: RegionCodes(rawValue: region),
pioEnv: node?.myInfo?.pioEnv
)
}

private var bandwidthSelection: Binding<Int> {
Binding(
get: {
Bandwidths.pickerValue(
forStoredValue: bandwidth,
region: RegionCodes(rawValue: region)
)
},
set: { bandwidth = $0 }
)
}

var body: some View {
Form {
ConfigHeader(title: "LoRa", config: \.loRaConfig, node: node, onAppear: setLoRaValues)
Expand Down Expand Up @@ -221,23 +248,20 @@ struct LoRaConfig: View {
}
.tint(.accentColor)

if !usePreset {
HStack {
Picker("Bandwidth", selection: $bandwidth) {
ForEach(Bandwidths.allCases) { bw in
Text(bw.description)
.tag(bw.rawValue == 250 ? 0 : bw.rawValue)
}
}
}
HStack {
Picker("Spread Factor", selection: $spreadFactor) {
ForEach(7..<13) {
Text("\($0)")
.tag($0 == 12 ? 0 : $0)
}
}
}
if !usePreset {
CustomBandwidthPicker(
selection: bandwidthSelection,
options: availableBandwidths,
isValid: customBandwidthIsValid
)
HStack {
Picker("Spread Factor", selection: $spreadFactor) {
ForEach(7..<13) {
Text("\($0)")
.tag($0 == 12 ? 0 : $0)
}
}
}
}

VStack(alignment: .leading, spacing: 8) {
Expand Down Expand Up @@ -345,37 +369,39 @@ struct LoRaConfig: View {
.disabled(!accessoryManager.isConnected || node?.loRaConfig == nil)
.safeAreaInset(edge: .bottom, alignment: .center) {
HStack(spacing: 0) {
SaveConfigButton(node: node, hasChanges: $hasChanges) {
performConfigSave(
node: node,
context: context,
accessoryManager: accessoryManager,
hasChanges: $hasChanges,
dismiss: goBack
) { fromUser, toUser in
var lc = Config.LoRaConfig()
lc.hopLimit = UInt32(hopLimit)
lc.region = RegionCodes(rawValue: region)!.protoEnumValue()
lc.modemPreset = ModemPresets(rawValue: modemPreset)!.protoEnumValue()
lc.usePreset = usePreset
lc.txEnabled = txEnabled
lc.txPower = Int32(txPower)
lc.channelNum = UInt32(channelNum)
lc.bandwidth = UInt32(bandwidth)
lc.codingRate = UInt32(normalizedCodingRate)
lc.spreadFactor = UInt32(spreadFactor)
lc.sx126XRxBoostedGain = rxBoostedGain
lc.overrideFrequency = overrideFrequency
lc.ignoreMqtt = ignoreMqtt
lc.configOkToMqtt = okToMqtt
if let deviceNum = accessoryManager.activeDeviceNum,
let connectedNode = getNodeInfo(id: deviceNum, context: context),
connectedNode.num == node?.user?.num ?? 0 {
UserDefaults.modemPreset = modemPreset
SaveConfigButton(node: node, hasChanges: $hasChanges) {
guard customBandwidthIsValid else { return }
performConfigSave(
node: node,
context: context,
accessoryManager: accessoryManager,
hasChanges: $hasChanges,
dismiss: goBack
) { fromUser, toUser in
var lc = Config.LoRaConfig()
lc.hopLimit = UInt32(hopLimit)
lc.region = RegionCodes(rawValue: region)!.protoEnumValue()
lc.modemPreset = ModemPresets(rawValue: modemPreset)!.protoEnumValue()
lc.usePreset = usePreset
lc.txEnabled = txEnabled
lc.txPower = Int32(txPower)
lc.channelNum = UInt32(channelNum)
lc.bandwidth = UInt32(bandwidth)
lc.codingRate = UInt32(normalizedCodingRate)
lc.spreadFactor = UInt32(spreadFactor)
lc.sx126XRxBoostedGain = rxBoostedGain
lc.overrideFrequency = overrideFrequency
lc.ignoreMqtt = ignoreMqtt
lc.configOkToMqtt = okToMqtt
if let deviceNum = accessoryManager.activeDeviceNum,
let connectedNode = getNodeInfo(id: deviceNum, context: context),
connectedNode.num == node?.user?.num ?? 0 {
UserDefaults.modemPreset = modemPreset
}
_ = try await accessoryManager.saveLoRaConfig(config: lc, fromUser: fromUser, toUser: toUser)
}
_ = try await accessoryManager.saveLoRaConfig(config: lc, fromUser: fromUser, toUser: toUser)
}
}
.disabled(!customBandwidthIsValid)
}
}
.navigationTitle("LoRa Config")
Expand Down Expand Up @@ -493,6 +519,36 @@ struct LoRaConfig: View {
}
}

private struct CustomBandwidthPicker: View {
@Binding var selection: Int
let options: [Bandwidths]
let isValid: Bool

var body: some View {
VStack(alignment: .leading, spacing: 8) {
Picker("Bandwidth", selection: $selection) {
if !isValid {
Text("Unsupported (\(Bandwidths.description(forPickerValue: selection)))")
.tag(selection)
}
ForEach(options) { bandwidth in
Text(bandwidth.description)
.tag(bandwidth.pickerValue)
}
}
if !isValid {
Label {
Text("This bandwidth is not supported by the connected radio in the selected region. Choose a supported value before saving.")
} icon: {
Image(systemName: "exclamationmark.triangle.fill")
}
.foregroundStyle(.orange)
.font(.callout)
}
}
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
#Preview {
LoRaConfig(node: nil)
.environmentObject(AccessoryManager.shared)
Expand Down
40 changes: 39 additions & 1 deletion MeshtasticTests/LoraDeviceEnumTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,45 @@ struct BandwidthsTests {
}

@Test func totalCaseCount() {
#expect(Bandwidths.allCases.count == 5)
#expect(Bandwidths.allCases.count == 9)
}

@Test func subGHzOptions_preserveExistingWireValues() {
let options = Bandwidths.selectable(region: .us, pioEnv: nil)

#expect(options.map(\.pickerValue) == [31, 62, 125, 0, 500])
}

@Test func sx128xOptions_includeAllCanonicalHighBandValues() {
let options = Bandwidths.selectable(region: .lora24, pioEnv: "tlora-v2-1-1_8")

#expect(options.map(\.pickerValue) == [200, 400, 800, 1600])
#expect(options.map(\.description) == ["203.125 kHz", "406.25 kHz", "812.5 kHz", "1625 kHz"])
}

@Test func lr1121Options_excludeUnsupported1625kHzBandwidth() {
let options = Bandwidths.selectable(region: .lora24, pioEnv: "muzi-base")

#expect(options.map(\.pickerValue) == [200, 400, 800])
}

@Test func mixedSX128xLR1121Target_usesConservativeOptions() {
let options = Bandwidths.selectable(region: .lora24, pioEnv: "tlora-t3s3-v1")

#expect(options.map(\.pickerValue) == [200, 400, 800])
}

@Test func invalidStoredBandwidth_mustBeReplacedBeforeSaving() {
#expect(!Bandwidths.isValid(125, region: .lora24, pioEnv: "tlora-v2-1-1_8"))
#expect(!Bandwidths.isValid(1600, region: .lora24, pioEnv: "muzi-base"))
#expect(Bandwidths.isValid(800, region: .lora24, pioEnv: "muzi-base"))
#expect(Bandwidths.isValid(0, region: .us, pioEnv: nil))
}

@Test func explicit250kHzStoredValue_remainsValidAndDisplaysAsDefaultOption() {
#expect(Bandwidths.isValid(250, region: .us, pioEnv: nil))
#expect(Bandwidths.pickerValue(forStoredValue: 250, region: .us) == 0)
#expect(Bandwidths.pickerValue(forStoredValue: 250, region: .lora24) == 250)
}
}

Expand Down
Loading