Meshtastic-Apple/Meshtastic/Views/Settings/Config/LoRaConfig.swift
2026-04-16 12:10:00 -07:00

329 lines
11 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// LoRaConfig.swift
// Meshtastic Apple
//
// Copyright (c) by Garth Vander Houwen 6/11/22.
//
import SwiftUI
import SwiftData
import MeshtasticProtobufs
import OSLog
struct LoRaConfig: View {
enum Field: Hashable {
case channelNum
case frequencyOverride
}
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.groupingSeparator = ""
return formatter
}()
@Environment(\.modelContext) private var context
@EnvironmentObject var accessoryManager: AccessoryManager
@Environment(\.dismiss) private var goBack
@FocusState var focusedField: Field?
var node: NodeInfoEntity?
@State var hasChanges = false
@State var region: Int = 0
@State var modemPreset = 0
@State var hopLimit = 3
@State var txPower = 0
@State var txEnabled = true
@State var usePreset = true
@State var channelNum = 0
@State var bandwidth = 0
@State var spreadFactor = 0
@State var codingRate = 0
@State var rxBoostedGain = false
@State var overrideFrequency: Float = 0.0
@State var ignoreMqtt = false
@State var okToMqtt = false
let floatFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.allowsFloats = true
formatter.maximumFractionDigits = 4
return formatter
}()
var body: some View {
Form {
ConfigHeader(title: "LoRa", config: \.loRaConfig, node: node, onAppear: setLoRaValues)
Section(header: Text("Options")) {
VStack(alignment: .leading) {
Picker("Region", selection: $region ) {
ForEach(RegionCodes.allCases) { r in
Text(r.description)
}
}
Text("The region where you will be using your radios.")
.foregroundColor(.gray)
.font(.callout)
}
.pickerStyle(DefaultPickerStyle())
Toggle(isOn: $usePreset) {
Label("Use Preset", systemImage: "list.bullet.rectangle")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if usePreset {
VStack(alignment: .leading) {
Picker("Presets", selection: $modemPreset ) {
ForEach(ModemPresets.allCases) { m in
Text(m.description)
}
}
.pickerStyle(DefaultPickerStyle())
.fixedSize()
Text("Available modem presets, default is Long Fast.")
.foregroundColor(.gray)
.font(.callout)
}
}
}
Section(header: Text("Advanced")) {
Toggle(isOn: $ignoreMqtt) {
Label("Ignore MQTT", systemImage: "server.rack")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $okToMqtt) {
Label("Ok to MQTT", systemImage: "network")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $txEnabled) {
Label("Transmit Enabled", systemImage: "waveform.path")
}
.toggleStyle(SwitchToggleStyle(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)
}
}
}
HStack {
Picker("Coding Rate", selection: $codingRate) {
ForEach(5..<9) {
Text("\($0)")
.tag($0 == 8 ? 0 : $0)
}
}
}
}
VStack(alignment: .leading) {
Picker("Number of hops", selection: $hopLimit) {
ForEach(0..<8) {
Text("\($0)")
.tag($0)
}
}
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. 0 hop broadcast messages will not get ACKs.")
.foregroundColor(.gray)
.font(.callout)
}
.pickerStyle(DefaultPickerStyle())
VStack(alignment: .leading) {
HStack {
Text("Frequency Slot")
.fixedSize()
TextField("Frequency Slot", value: $channelNum, formatter: formatter)
.keyboardType(.numberPad)
.focused($focusedField, equals: .channelNum)
.disabled(overrideFrequency > 0.0)
}
Text("Your nodes operating frequency is calculated based on the region, modem preset, and this field. When 0, the slot is automatically calculated based on the primary channel name.")
.foregroundColor(.gray)
.font(.callout)
}
Toggle(isOn: $rxBoostedGain) {
Label("RX Boosted Gain", systemImage: "waveform.badge.plus")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
HStack {
Label("Frequency Override", systemImage: "waveform.path.ecg")
Spacer()
TextField("Frequency Override", value: $overrideFrequency, formatter: floatFormatter)
.keyboardType(.decimalPad)
.focused($focusedField, equals: .frequencyOverride)
}
HStack {
Image(systemName: "antenna.radiowaves.left.and.right")
.foregroundColor(.accentColor)
Stepper("\(txPower)dBm Transmit Power", value: $txPower, in: 1...30, step: 1)
.padding(5)
}
}
}
.scrollDismissesKeyboard(.immediately)
.disabled(!accessoryManager.isConnected || node?.loRaConfig == nil)
.safeAreaInset(edge: .bottom, alignment: .center) {
HStack(spacing: 0) {
SaveConfigButton(node: node, hasChanges: $hasChanges) {
if let deviceNum = accessoryManager.activeDeviceNum, let connectedNode = getNodeInfo(id: deviceNum, context: context) {
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(codingRate)
lc.spreadFactor = UInt32(spreadFactor)
lc.sx126XRxBoostedGain = rxBoostedGain
lc.overrideFrequency = overrideFrequency
lc.ignoreMqtt = ignoreMqtt
lc.configOkToMqtt = okToMqtt
if connectedNode.num == node?.user?.num ?? 0 {
UserDefaults.modemPreset = modemPreset
}
Task {
_ = try await accessoryManager.saveLoRaConfig(config: lc, fromUser: connectedNode.user!, toUser: node!.user!)
Task { @MainActor in
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
goBack()
}
}
}
}
}
}
.navigationTitle("LoRa Config")
.navigationBarItems(
trailing: ZStack {
ConnectedDevice(deviceConnected: accessoryManager.isConnected, name: accessoryManager.activeConnection?.device.shortName ?? "?")
}
)
.onFirstAppear {
// Need to request a LoRaConfig from the remote node before allowing changes
if let deviceNum = accessoryManager.activeDeviceNum, let node {
if let connectedNode = getNodeInfo(id: deviceNum, context: context) {
if node.num != deviceNum {
if UserDefaults.enableAdministration {
/// 2.5 Administration with session passkey
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.loRaConfig == nil {
Task {
do {
if connectedNode.user != nil && node.user != nil {
Logger.mesh.info("⚙️ Empty or expired lora config requesting via PKI admin")
_ = try await accessoryManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!)
} else {
Logger.mesh.info("🚫 No User or node for lora config request")
}
} catch {
Logger.mesh.info("🚨 Lora config request failed")
}
}
}
} else {
/// Legacy Administration
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
}
}
}
}
}
.onChange(of: region) { _, newRegion in
if newRegion != node?.loRaConfig?.regionCode ?? -1 { hasChanges = true }
}
.onChange(of: usePreset) { _, newPreset in
if newPreset != node?.loRaConfig?.usePreset { hasChanges = true }
}
.onChange(of: modemPreset) { _, newModemPreset in
if newModemPreset != node?.loRaConfig?.modemPreset ?? -1 { hasChanges = true }
}
.onChange(of: hopLimit) { _, newHopLimit in
if newHopLimit != node?.loRaConfig?.hopLimit ?? -1 { hasChanges = true }
}
.onChange(of: channelNum) { _, newChannelNum in
if newChannelNum != node?.loRaConfig?.channelNum ?? -1 { hasChanges = true }
}
.onChange(of: bandwidth) { _, newBandwidth in
if newBandwidth != node?.loRaConfig?.bandwidth ?? -1 { hasChanges = true }
}
.onChange(of: codingRate) { _, newCodingRate in
if newCodingRate != node?.loRaConfig?.codingRate ?? -1 { hasChanges = true }
}
.onChange(of: spreadFactor) { _, newSpreadFactor in
if newSpreadFactor != node?.loRaConfig?.spreadFactor ?? -1 { hasChanges = true }
}
.onChange(of: rxBoostedGain) { _, newRxBoostedGain in
if newRxBoostedGain != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true }
}
.onChange(of: overrideFrequency) { _, newOverrideFrequency in
if newOverrideFrequency != node?.loRaConfig?.overrideFrequency { hasChanges = true }
}
.onChange(of: txPower) { _, newTxPower in
if newTxPower != node?.loRaConfig?.txPower ?? -1 { hasChanges = true }
}
.onChange(of: txEnabled) { _, newTxEnabled in
if newTxEnabled != node?.loRaConfig?.txEnabled { hasChanges = true }
}
.onChange(of: ignoreMqtt) { _, newIgnoreMqtt in
if newIgnoreMqtt != node?.loRaConfig?.ignoreMqtt { hasChanges = true }
}
.onChange(of: okToMqtt) { _, newOkToMqtt in
if newOkToMqtt != node?.loRaConfig?.okToMqtt { hasChanges = true }
}
}
func setLoRaValues() {
if node?.loRaConfig?.modemPreset ?? 0 == 2 {
node?.loRaConfig?.modemPreset = 0
}
self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3)
self.region = Int(node?.loRaConfig?.regionCode ?? 0)
self.usePreset = node?.loRaConfig?.usePreset ?? true
self.modemPreset = Int(node?.loRaConfig?.modemPreset ?? 0)
self.txEnabled = node?.loRaConfig?.txEnabled ?? true
self.txPower = Int(node?.loRaConfig?.txPower ?? 0)
self.channelNum = Int(node?.loRaConfig?.channelNum ?? 0)
self.bandwidth = Int(node?.loRaConfig?.bandwidth ?? 0)
self.codingRate = Int(node?.loRaConfig?.codingRate ?? 0)
self.spreadFactor = Int(node?.loRaConfig?.spreadFactor ?? 0)
self.rxBoostedGain = node?.loRaConfig?.sx126xRxBoostedGain ?? false
self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.0
self.ignoreMqtt = node?.loRaConfig?.ignoreMqtt ?? false
self.okToMqtt = node?.loRaConfig?.okToMqtt ?? false
self.hasChanges = false
}
}
#Preview {
LoRaConfig(node: nil)
.environmentObject(AccessoryManager.shared)
.modelContainer(PersistenceController.preview.container)
}