mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Settings rework - new async location handler
This commit is contained in:
parent
fb66e3b250
commit
126cdfbdb3
20 changed files with 1193 additions and 545 deletions
|
|
@ -37,7 +37,6 @@ struct CannedMessagesConfig: View {
|
|||
@State var messages = ""
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
|
|
|
|||
|
|
@ -41,150 +41,151 @@ struct DetectionSensorConfig: View {
|
|||
@State var monitorPin = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.detectionSensorConfig == nil {
|
||||
Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
if enabled {
|
||||
HStack {
|
||||
Picker(selection: $role, label: Text("Role")) {
|
||||
ForEach(DetectionSensorRole.allCases, id: \.self) { r in
|
||||
Text(r.description)
|
||||
.tag(r)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
if enabled && role == .client {
|
||||
Section(header: Text("Client options")) {
|
||||
Toggle(isOn: $detectionNotificationsEnabled) {
|
||||
Label("Enable Notifications", systemImage: "bell.badge")
|
||||
Text("Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge.")
|
||||
.font(.caption)
|
||||
}
|
||||
.listRowSeparator(.visible)
|
||||
}
|
||||
}
|
||||
if enabled && role == .sensor {
|
||||
Section(header: Text("Sensor options")) {
|
||||
Toggle(isOn: $sendBell) {
|
||||
Label("Send Bell", systemImage: "bell")
|
||||
Text("Send ASCII bell with alert message. Useful for triggering external notification on bell.")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
HStack {
|
||||
Label("Name", systemImage: "signature")
|
||||
TextField("Friendly name", text: $name, axis: .vertical)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: name, perform: { _ in
|
||||
|
||||
let totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 20 {
|
||||
|
||||
let firstNBytes = Data(name.utf8.prefix(20))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
name = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.listRowSeparator(.visible)
|
||||
.offset(y: -10)
|
||||
Picker("GPIO Pin to monitor", selection: $monitorPin) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Toggle(isOn: $detectionTriggeredHigh) {
|
||||
Label("Detection trigger High", systemImage: "dial.high")
|
||||
Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $usePullup) {
|
||||
Label("Uses pullup resistor", systemImage: "arrow.up.to.line")
|
||||
Text(" Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin")
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.detectionSensorConfig == nil {
|
||||
Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
if enabled {
|
||||
HStack {
|
||||
Picker(selection: $role, label: Text("Role")) {
|
||||
ForEach(DetectionSensorRole.allCases, id: \.self) { r in
|
||||
Text(r.description)
|
||||
.tag(r)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(header: Text("update.interval")) {
|
||||
Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
if enabled && role == .client {
|
||||
Section(header: Text("Client options")) {
|
||||
Toggle(isOn: $detectionNotificationsEnabled) {
|
||||
Label("Enable Notifications", systemImage: "bell.badge")
|
||||
Text("Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Mininum time between detection broadcasts. Default is 45 seconds.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.listRowSeparator(.visible)
|
||||
Picker("State Broadcast Interval", selection: $stateBroadcastSecs) {
|
||||
Text("Never").tag(0)
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
if enabled && role == .sensor {
|
||||
Section(header: Text("Sensor options")) {
|
||||
Toggle(isOn: $sendBell) {
|
||||
Label("Send Bell", systemImage: "bell")
|
||||
Text("Send ASCII bell with alert message. Useful for triggering external notification on bell.")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
HStack {
|
||||
Label("Name", systemImage: "signature")
|
||||
TextField("Friendly name", text: $name, axis: .vertical)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: name, perform: { _ in
|
||||
|
||||
let totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 20 {
|
||||
|
||||
let firstNBytes = Data(name.utf8.prefix(20))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
name = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.listRowSeparator(.visible)
|
||||
.offset(y: -10)
|
||||
Picker("GPIO Pin to monitor", selection: $monitorPin) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Toggle(isOn: $detectionTriggeredHigh) {
|
||||
Label("Detection trigger High", systemImage: "dial.high")
|
||||
Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $usePullup) {
|
||||
Label("Uses pullup resistor", systemImage: "arrow.up.to.line")
|
||||
Text(" Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
Section(header: Text("update.interval")) {
|
||||
Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Mininum time between detection broadcasts. Default is 45 seconds.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.listRowSeparator(.visible)
|
||||
Picker("State Broadcast Interval", selection: $stateBroadcastSecs) {
|
||||
Text("Never").tag(0)
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,137 +32,138 @@ struct ExternalNotificationConfig: View {
|
|||
@State var nagTimeout = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.externalNotificationConfig == nil {
|
||||
Text("External notification config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.externalNotificationConfig == nil {
|
||||
Text("External notification config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setExternalNotificationValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setExternalNotificationValues()
|
||||
}
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "megaphone")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertBell) {
|
||||
Label("Alert when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessage) {
|
||||
Label("Alert when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $usePWM) {
|
||||
Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.")
|
||||
.font(.caption)
|
||||
}
|
||||
Section(header: Text("Advanced GPIO Options")) {
|
||||
Section(header: Text("Primary GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $active) {
|
||||
Label("Active", systemImage: "togglepower")
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "megaphone")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.")
|
||||
.font(.caption)
|
||||
Picker("Output pin GPIO", selection: $output) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
Toggle(isOn: $alertBell) {
|
||||
Label("Alert when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("GPIO Output Duration", selection: $outputMilliseconds ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessage) {
|
||||
Label("Alert when receiving a message", systemImage: "message")
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("When using in GPIO mode, keep the output on for this long. ")
|
||||
.font(.caption)
|
||||
Picker("Nag timeout", selection: $nagTimeout ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $usePWM) {
|
||||
Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill")
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("Specifies how long the monitored GPIO should output.")
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
Section(header: Text("Optional GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $alertBellBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertBellVibra) {
|
||||
Label("Alert GPIO vibra motor when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO vibra motor when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
Section(header: Text("Advanced GPIO Options")) {
|
||||
Section(header: Text("Primary GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $active) {
|
||||
Label("Active", systemImage: "togglepower")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("Output pin vibra GPIO", selection: $outputVibra) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.")
|
||||
.font(.caption)
|
||||
Picker("Output pin GPIO", selection: $output) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("GPIO Output Duration", selection: $outputMilliseconds ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("When using in GPIO mode, keep the output on for this long. ")
|
||||
.font(.caption)
|
||||
Picker("Nag timeout", selection: $nagTimeout ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("Specifies how long the monitored GPIO should output.")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
Section(header: Text("Optional GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $alertBellBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertBellVibra) {
|
||||
Label("Alert GPIO vibra motor when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO vibra motor when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("Output pin vibra GPIO", selection: $outputVibra) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.externalNotificationConfig == nil)
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.externalNotificationConfig == nil)
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -25,176 +25,177 @@ struct MQTTConfig: View {
|
|||
@State var root = "msh"
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.mqttConfig == nil {
|
||||
Text("MQTT config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.mqttConfig == nil {
|
||||
Text("MQTT config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setMqttValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setMqttValues()
|
||||
}
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $proxyToClientEnabled) {
|
||||
|
||||
Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If both MQTT and the client proxy are enabled your mobile device will utalize an available network connection to connect to the specified MQTT server.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $encryptionEnabled) {
|
||||
|
||||
Label("Encryption Enabled", systemImage: "lock.icloud")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $jsonEnabled) {
|
||||
|
||||
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("JSON mode is a limited, unencrypted MQTT output.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $tlsEnabled) {
|
||||
|
||||
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Your MQTT Server must support TLS.")
|
||||
.font(.caption2)
|
||||
}
|
||||
Section(header: Text("Custom Server")) {
|
||||
HStack {
|
||||
Label("Address", systemImage: "server.rack")
|
||||
TextField("Server Address", text: $address)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: address, perform: { _ in
|
||||
let totalBytes = address.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
address = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
.keyboardType(.default)
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
|
||||
HStack {
|
||||
Label("mqtt.username", systemImage: "person.text.rectangle")
|
||||
TextField("mqtt.username", text: $username)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: username, perform: { _ in
|
||||
|
||||
let totalBytes = username.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
username = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("password", systemImage: "wallet.pass")
|
||||
TextField("password", text: $password)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: password, perform: { _ in
|
||||
|
||||
let totalBytes = password.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(password.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
password = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("Root Topic", systemImage: "tree")
|
||||
TextField("Root Topic", text: $root)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: root, perform: { _ in
|
||||
let totalBytes = root.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 14 {
|
||||
let firstNBytes = Data(root.utf8.prefix(14))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
root = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.asciiCapable)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disableAutocorrection(true)
|
||||
Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs")
|
||||
.font(.caption2)
|
||||
}
|
||||
Text("You can set uplink and downlink for each channel.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $proxyToClientEnabled) {
|
||||
|
||||
Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If both MQTT and the client proxy are enabled your mobile device will utalize an available network connection to connect to the specified MQTT server.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $encryptionEnabled) {
|
||||
|
||||
Label("Encryption Enabled", systemImage: "lock.icloud")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $jsonEnabled) {
|
||||
|
||||
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("JSON mode is a limited, unencrypted MQTT output.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $tlsEnabled) {
|
||||
|
||||
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Your MQTT Server must support TLS.")
|
||||
.font(.caption2)
|
||||
}
|
||||
Section(header: Text("Custom Server")) {
|
||||
HStack {
|
||||
Label("Address", systemImage: "server.rack")
|
||||
TextField("Server Address", text: $address)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: address, perform: { _ in
|
||||
let totalBytes = address.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
address = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
.keyboardType(.default)
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
|
||||
HStack {
|
||||
Label("mqtt.username", systemImage: "person.text.rectangle")
|
||||
TextField("mqtt.username", text: $username)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: username, perform: { _ in
|
||||
|
||||
let totalBytes = username.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
username = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("password", systemImage: "wallet.pass")
|
||||
TextField("password", text: $password)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: password, perform: { _ in
|
||||
|
||||
let totalBytes = password.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(password.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
password = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("Root Topic", systemImage: "tree")
|
||||
TextField("Root Topic", text: $root)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: root, perform: { _ in
|
||||
let totalBytes = root.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 14 {
|
||||
let firstNBytes = Data(root.utf8.prefix(14))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
root = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.asciiCapable)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disableAutocorrection(true)
|
||||
Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs")
|
||||
.font(.caption2)
|
||||
}
|
||||
Text("You can set uplink and downlink for each channel.")
|
||||
.font(.callout)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.mqttConfig == nil)
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.mqttConfig == nil)
|
||||
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ struct SerialConfig: View {
|
|||
@State var mode = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
|
|
|
|||
|
|
@ -27,72 +27,72 @@ struct StoreForwardConfig: View {
|
|||
@State var historyReturnWindow = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.storeForwardConfig == nil {
|
||||
Text("Store and forward config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.storeForwardConfig == nil {
|
||||
Text("Store and forward config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
|
||||
}
|
||||
Toggle(isOn: $heartbeat) {
|
||||
Label("storeforward.heartbeat", systemImage: "waveform.path.ecg")
|
||||
}
|
||||
Picker("Number of records", selection: $records) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Max", selection: $historyReturnMax ) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Window", selection: $historyReturnWindow ) {
|
||||
Text("unset").tag(0)
|
||||
Text("One Minute").tag(60)
|
||||
Text("Five Minutes").tag(300)
|
||||
Text("Ten Minutes").tag(600)
|
||||
Text("Fifteen Minutes").tag(900)
|
||||
Text("Thirty Minutes").tag(1800)
|
||||
Text("One Hour").tag(3600)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
|
||||
}
|
||||
Toggle(isOn: $heartbeat) {
|
||||
Label("storeforward.heartbeat", systemImage: "waveform.path.ecg")
|
||||
}
|
||||
Picker("Number of records", selection: $records) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Max", selection: $historyReturnMax ) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Window", selection: $historyReturnWindow ) {
|
||||
Text("unset").tag(0)
|
||||
Text("One Minute").tag(60)
|
||||
Text("Five Minutes").tag(300)
|
||||
Text("Ten Minutes").tag(600)
|
||||
Text("Fifteen Minutes").tag(900)
|
||||
Text("Thirty Minutes").tag(1800)
|
||||
Text("One Hour").tag(3600)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.storeForwardConfig == nil)
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.storeForwardConfig == nil)
|
||||
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ struct TelemetryConfig: View {
|
|||
@State var environmentDisplayFahrenheit = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue