Signal strength indicator updates

This commit is contained in:
Garth Vander Houwen 2023-05-16 05:54:12 -07:00
parent 377eeea177
commit 8382ad962a
10 changed files with 265 additions and 79 deletions

View file

@ -104,6 +104,7 @@
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; };
DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; };
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */; };
DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */; };
DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */; };
DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; };
DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; };
@ -294,6 +295,7 @@
DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = "<group>"; };
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = "<group>"; };
DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV13.xcdatamodel; sourceTree = "<group>"; };
DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrength.swift; sourceTree = "<group>"; };
DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = "<group>"; };
DDC2E15426CE248E0042C5E4 /* Meshtastic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshtastic.app; sourceTree = BUILT_PRODUCTS_DIR; };
DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticApp.swift; sourceTree = "<group>"; };
@ -694,6 +696,7 @@
DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */,
DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */,
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */,
DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -1060,6 +1063,7 @@
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */,
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */,
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */,
DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */,
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */,
DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */,
DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */,
@ -1451,7 +1455,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.1.10;
MARKETING_VERSION = 2.1.11;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1482,7 +1486,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.1.10;
MARKETING_VERSION = 2.1.11;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;

View file

@ -269,6 +269,12 @@ enum MapTileServer: String, CaseIterable, Identifiable {
}
}
enum OverlayType: String, CaseIterable, Equatable {
case tileServer
case geoJson
var localized: String { self.rawValue.localized }
}
enum MapOverlayServer: String, CaseIterable, Identifiable {
case baseReReflectivityCurrent
@ -282,6 +288,29 @@ enum MapOverlayServer: String, CaseIterable, Identifiable {
case mrmsHybridScanReflectivityComposite
var id: String { self.rawValue }
var overlayType: OverlayType {
switch self {
case .baseReReflectivityCurrent:
return .tileServer
case .baseReReflectivityOneHourAgo:
return .tileServer
case .echoTopsEetCurrent:
return .tileServer
case .echoTopsEetOneHourAgo:
return .tileServer
case .q2OneHourPrecipitation:
return .tileServer
case .q2TwentyFourHourPrecipitation:
return .tileServer
case .q2FortyEightHourPrecipitation:
return .tileServer
case .q2SeventyTwoHourPrecipitation:
return .tileServer
case .mrmsHybridScanReflectivityComposite:
return .tileServer
}
}
var attribution: String {
return "NEXRAD Weather tiles from Iowa State University Environmental Mesonet [OGC Web Services](https://mesonet.agron.iastate.edu/ogc/)."
}

View file

@ -0,0 +1,72 @@
//
// LoRaSignalStrength.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 5/15/23.
//
import Foundation
import SwiftUI
struct LoRaSignalStrengthMeter: View {
var snr: Float
var rssi: Int32
var preset: ModemPresets
var compact: Bool
var body: some View {
let signalStrength = getLoRaSignalStrength(snr: snr, rssi: rssi, preset: preset)
let gradient = Gradient(colors: [.red, .orange, .yellow, .green])
if !compact {
VStack {
LoRaSignalStrengthIndicator(signalStrength: signalStrength)
Text("Signal \(signalStrength.description)").font(.footnote)
Text("SNR \(String(format: "%.2f", snr))dB")
.foregroundColor(getSnrColor(snr: snr, preset: ModemPresets.longFast))
.font(.caption2)
Text("RSSI \(rssi)dB")
.foregroundColor(getRssiColor(rssi: rssi))
.font(.caption2)
}
} else {
Gauge(value: Double(signalStrength.rawValue), in: 0...3) {
} currentValueLabel: {
Image(systemName: "dot.radiowaves.left.and.right")
.font(.caption)
Text("Signal \(signalStrength.description)")
.font(.caption)
}
.gaugeStyle(.accessoryLinear)
.tint(gradient)
.font(.caption)
}
}
}
struct LoRaSignalStrengthMeter_Previews: PreviewProvider {
static var previews: some View {
VStack {
LoRaSignalStrengthMeter(snr: -10, rssi: -100, preset: ModemPresets.longFast, compact: false)
LoRaSignalStrengthMeter(snr: -17.5, rssi: -100, preset: ModemPresets.longFast, compact: false)
LoRaSignalStrengthMeter(snr: -12.75, rssi: -139, preset: ModemPresets.longFast, compact: false)
LoRaSignalStrengthMeter(snr: -20.25, rssi: -128, preset: ModemPresets.longFast, compact: false)
LoRaSignalStrengthMeter(snr: -30, rssi: -128, preset: ModemPresets.longFast, compact: false)
}
VStack {
LoRaSignalStrengthMeter(snr: -10, rssi: -100, preset: ModemPresets.longFast, compact: true)
.padding(.bottom)
LoRaSignalStrengthMeter(snr: -17.5, rssi: -100, preset: ModemPresets.longFast, compact: true)
.padding(.bottom)
LoRaSignalStrengthMeter(snr: -12.75, rssi: -139, preset: ModemPresets.longFast, compact: true)
.padding(.bottom)
LoRaSignalStrengthMeter(snr: -20.25, rssi: -128, preset: ModemPresets.longFast, compact: true)
.padding(.bottom)
LoRaSignalStrengthMeter(snr: -30, rssi: -128, preset: ModemPresets.longFast, compact: true)
}
.padding()
}
}

View file

@ -5,8 +5,6 @@
// Copyright Garth Vander Houwen 5/9/23.
//
import Foundation
import Foundation
import SwiftUI
@ -18,22 +16,25 @@ struct LoRaSignalStrengthIndicator: View {
ForEach(0..<3) { bar in
RoundedRectangle(cornerRadius: 3)
.divided(amount: (CGFloat(bar) + 1) / CGFloat(3))
.fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3))
.fill(getColor(signalStrength: signalStrength).opacity(bar <= signalStrength.rawValue ? 1 : 0.3))
.frame(width: 8, height: 40)
}
}
}
}
private func getColor() -> Color {
switch signalStrength {
case .none:
return Color.red
case .bad:
return Color.orange
case .fair:
return Color.yellow
case .good:
return Color.green
struct LoRaSignalStrengthIndicator_Previews: PreviewProvider {
static var previews: some View {
VStack {
let signalStrength = getLoRaSignalStrength(snr: -12.75, rssi: -139, preset: ModemPresets.longFast)
LoRaSignalStrengthIndicator(signalStrength: signalStrength)
Text("Signal \(signalStrength.description)").font(.footnote)
Text("SNR \(String(format: "%.2f", -12.75))dB")
.foregroundColor(getSnrColor(snr: -12.75, preset: ModemPresets.longFast))
.font(.caption2)
Text("RSSI \(-139)dB")
.foregroundColor(getRssiColor(rssi: -139))
.font(.caption2)
}
}
}
@ -57,13 +58,26 @@ enum LoRaSignalStrength: Int {
}
}
func getLoRaSignalStrength(snr: Float, rssi: Int32) -> LoRaSignalStrength {
private func getColor(signalStrength: LoRaSignalStrength) -> Color {
switch signalStrength {
case .none:
return Color.red
case .bad:
return Color.orange
case .fair:
return Color.yellow
case .good:
return Color.green
}
}
func getLoRaSignalStrength(snr: Float, rssi: Int32, preset: ModemPresets) -> LoRaSignalStrength {
if rssi > -115 && snr > -7 {
if rssi > -115 && snr > (preset.snrLimit()) {
return .good
} else if rssi < -126 && snr < -15 {
} else if rssi < -126 && snr < (preset.snrLimit() - 7.5) {
return .none
} else if rssi <= -120 || snr <= -13 {
} else if rssi <= -120 || snr <= (preset.snrLimit() - 5.5) {
return .bad
} else {
return .fair
@ -86,14 +100,14 @@ func getRssiColor(rssi: Int32) -> Color {
}
}
func getSnrColor(snr: Float) -> Color {
if snr > -7 {
func getSnrColor(snr: Float, preset: ModemPresets) -> Color {
if snr > preset.snrLimit() {
/// Good
return .green
} else if snr < -7 && snr > -13 {
} else if snr < preset.snrLimit() && snr > (preset.snrLimit() - 5.5) {
/// Fair
return .yellow
} else if snr >= -14 {
} else if snr >= (preset.snrLimit() - 7.5) {
/// Bad
return .orange
} else {

View file

@ -47,11 +47,11 @@ struct NodeInfoView: View {
Divider()
if node.snr != 0 {
VStack(alignment: .center) {
let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi)
let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate)
LoRaSignalStrengthIndicator(signalStrength: signalStrength)
Text("Signal \(signalStrength.description)").font(.title)
Text("SNR \(String(format: "%.2f", node.snr))dB")
.foregroundColor(getSnrColor(snr: node.snr))
.foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate))
.font(.title3)
Text("RSSI \(node.rssi)dB")
.foregroundColor(getRssiColor(rssi: node.rssi))
@ -156,11 +156,11 @@ struct NodeInfoView: View {
if node.snr != 0 {
Divider()
VStack(alignment: .center) {
let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi)
let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate)
LoRaSignalStrengthIndicator(signalStrength: signalStrength)
Text("Signal \(signalStrength.description)").font(.footnote)
Text("SNR \(String(format: "%.2f", node.snr))dB")
.foregroundColor(getSnrColor(snr: node.snr))
.foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate))
.font(.caption2)
Text("RSSI \(node.rssi)dB")
.foregroundColor(getRssiColor(rssi: node.rssi))

View file

@ -4,9 +4,24 @@
//
// Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22.
import Foundation
import SwiftUI
import MapKit
struct PolygonInfo: Codable {
let stroke: String?
let strokeWidth, strokeOpacity: Int?
let fill: String?
let fillOpacity: Double?
let title, subtitle: String?
}
struct PolylineInfo: Codable {
let stroke: String?
let strokeWidth, strokeOpacity: Int?
let title, subtitle: String?
}
func degreesToRadians(_ number: Double) -> Double {
return number * .pi / 180
}
@ -29,7 +44,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
let showRouteLines: Bool
let mapViewType: MKMapType = MKMapType.standard
// Offline Map Tiles
@AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0
@State private var loadedLastUpdatedLocalMapFile = 0
@ -65,6 +80,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
// Other MKMapView Settings
mapView.preferredConfiguration.elevationStyle = .realistic// .flat
mapView.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll
mapView.isPitchEnabled = true
mapView.isRotateEnabled = true
mapView.isScrollEnabled = true
@ -134,14 +150,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
}
func makeUIView(context: Context) -> MKMapView {
currentMapLayer = nil
mapView.delegate = context.coordinator
self.configureMap(mapView: mapView)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
private func setMbtilesOverlay(mapView: MKMapView) {
// MBTiles Offline
if UserDefaults.enableOfflineMaps && UserDefaults.enableOfflineMapsMBTiles {
@ -149,7 +158,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile {
mapView.removeOverlays(mapView.overlays)
if self.customMapOverlay != nil {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path
@ -169,15 +178,69 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
}
}
}
private func setGeoJsonOverlay(mapView: MKMapView) {
guard let geoJsonFileUrl = URL(string: "https://raw.githubusercontent.com/johan/world.geo.json/master/countries.geo.json"), // Bundle.main.url(forResource: "location", withExtension: "geojson"),
//guard let geoJsonFileUrl = URL(string: "https://hrbrmstr.github.io/noaa-alerts-sp-to-geojson/current-all.geojson"),
let geoJsonData = try? Data.init(contentsOf: geoJsonFileUrl) else {
fatalError("Failure to fetch the file.")
}
guard let objs = try? MKGeoJSONDecoder().decode(geoJsonData) as? [MKGeoJSONFeature] else {
fatalError("Wrong format")
}
// Parse the objects
objs.forEach { (feature) in
guard let geometry = feature.geometry.first,
let propData = feature.properties else {
return;
}
// Check if it is MKPolygon
if let polygon = geometry as? MKPolygon {
let polygonInfo = try? JSONDecoder.init().decode(PolygonInfo.self, from: propData)
mapView.addOverlay(polygon)
//self.view?.render(overlay: polygon, info: polygonInfo)
}
// Check if it is MKPolyline
if let polyline = geometry as? MKPolyline {
mapView.addOverlay(polyline, level: .aboveLabels)
//let polylineInfo = try? JSONDecoder.init().decode(PolylineInfo.self, from: propData)
//self.view?.render(overlay: polyline, info: polylineInfo)
}
// Check if it is MKPointAnnotation
// if let annotation = geometry as? MKPointAnnotation {
// let info = try? JSONDecoder.init().decode(Info.self, from: propData)
// let storeAnnotation = StoreAnnotation.init(title: info?.name,
// subtitle: info?.subTitle,
// website: info?.website,
// coordinate: annotation.coordinate)
// self.view?.setAnnotations(annotations: [storeAnnotation])
// }
}
}
func makeUIView(context: Context) -> MKMapView {
currentMapLayer = nil
mapView.delegate = context.coordinator
self.configureMap(mapView: mapView)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
// Set MBTiles overlay layer
setMbtilesOverlay(mapView: mapView)
// Set selected map base layer
setMapBaseLayer(mapView: mapView)
// Set map overlay layer
// Set map tile server and weather overlay layers
setMapOverlays(mapView: mapView)
let latest = positions
.filter { $0.latest == true }
.sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 }
// Node Route Lines
if showRouteLines {
// Remove all existing PolyLine Overlays
@ -437,7 +500,14 @@ struct MapViewSwiftUI: UIViewRepresentable {
renderer.lineWidth = 8
return renderer
}
return MKOverlayRenderer()
if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.purple.withAlphaComponent(0.2)
renderer.strokeColor = .purple.withAlphaComponent(0.7)
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
}
}

View file

@ -54,7 +54,7 @@ struct NodeDetail: View {
var body: some View {
let hwModelString = node.user?.hwModel ?? "UNSET"
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
NavigationStack {
GeometryReader { bounds in
VStack {
@ -84,19 +84,21 @@ struct NodeDetail: View {
VStack(alignment: .leading) {
Spacer()
HStack(alignment: .bottom, spacing: 1) {
// Picker("Map Type", selection: $mapType) {
// ForEach(MeshMapTypes.allCases) { map in
// Text(map.description)
// .tag(map.MKMapTypeValue())
// }
// }
// .onChange(of: (mapType)) { newMapType in
// UserDefaults.mapType = Int(newMapType.rawValue)
// }
// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
// .pickerStyle(.menu)
// .padding(5)
Picker("Map Type", selection: $selectedMapLayer) {
ForEach(MapLayer.allCases, id: \.self) { layer in
if layer == MapLayer.offline && UserDefaults.enableOfflineMaps {
Text(layer.localized)
} else if layer != MapLayer.offline {
Text(layer.localized)
}
}
}
.onChange(of: (selectedMapLayer)) { newMapLayer in
UserDefaults.mapLayer = newMapLayer
}
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.pickerStyle(.menu)
.padding(5)
VStack {
Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
.font(.caption)
@ -151,7 +153,7 @@ struct NodeDetail: View {
if self.bleManager.connectedPeripheral != nil && node.metadata != nil {
HStack {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
if node.metadata?.canShutdown ?? false {
Button(action: {

View file

@ -27,6 +27,8 @@ struct NodeList: View {
var body: some View {
NavigationSplitView {
let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
List(nodes, id: \.self, selection: $selection) { node in
if nodes.count == 0 {
Text("no.nodes").font(.title)
@ -42,7 +44,7 @@ struct NodeList: View {
.fontWeight(.medium)
.font(.callout)
if connected {
HStack {
HStack(alignment: .bottom) {
Image(systemName: "repeat.circle.fill")
.font(.callout)
.symbolRenderingMode(.hierarchical)
@ -80,6 +82,11 @@ struct NodeList: View {
LastHeardText(lastHeard: node.lastHeard)
.font(.caption)
}
if !connected {
HStack(alignment: .bottom) { let preset = ModemPresets(rawValue: Int(connectedNode?.loRaConfig?.modemPreset ?? 0))
LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}

View file

@ -178,14 +178,14 @@ struct NodeMap: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onChange(of: (enableOfflineMaps)) { newEnableOfflineMaps in
UserDefaults.enableOfflineMaps = newEnableOfflineMaps
if !newEnableOfflineMaps {
UserDefaults.enableOfflineMaps = enableOfflineMaps
if !enableOfflineMaps {
if self.selectedMapLayer == .offline {
self.selectedMapLayer = .standard
}
}
}
if UserDefaults.enableOfflineMaps {
if enableOfflineMaps {
VStack (alignment: .leading) {
if !enableOfflineMapsMBTiles {

View file

@ -262,54 +262,42 @@ struct DeviceConfig: View {
}
}
.onChange(of: deviceRole) { newRole in
if node != nil && node!.deviceConfig != nil {
if node != nil && node?.deviceConfig != nil {
if newRole != node!.deviceConfig!.role { hasChanges = true }
}
}
.onChange(of: serialEnabled) { newSerial in
if node != nil && node!.deviceConfig != nil {
if node != nil && node?.deviceConfig != nil {
if newSerial != node!.deviceConfig!.serialEnabled { hasChanges = true }
}
}
.onChange(of: debugLogEnabled) { newDebugLog in
if node != nil && node!.deviceConfig != nil {
if node != nil && node?.deviceConfig != nil {
if newDebugLog != node!.deviceConfig!.debugLogEnabled { hasChanges = true }
}
}
.onChange(of: buttonGPIO) { newButtonGPIO in
if node != nil && node!.deviceConfig != nil {
if node != nil && node?.deviceConfig != nil {
if newButtonGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true }
}
}
.onChange(of: buzzerGPIO) { newBuzzerGPIO in
if node != nil && node!.deviceConfig != nil {
if node != nil && node?.deviceConfig != nil {
if newBuzzerGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true }
}
}
.onChange(of: rebroadcastMode) { newRebroadcastMode in
if node != nil && node!.deviceConfig != nil {
if node != nil && node?.deviceConfig != nil {
if newRebroadcastMode != node!.deviceConfig!.rebroadcastMode { hasChanges = true }
}
}
.onChange(of: doubleTapAsButtonPress) { newDoubleTapAsButtonPress in
if node != nil && node!.deviceConfig != nil {
if node != nil && node?.deviceConfig != nil {
if newDoubleTapAsButtonPress != node!.deviceConfig!.doubleTapAsButtonPress { hasChanges = true }
}
}
.onChange(of: isManaged) { newIsManaged in
if node != nil && node!.deviceConfig != nil {
if node != nil && node?.deviceConfig != nil {
if newIsManaged != node!.deviceConfig!.isManaged { hasChanges = true }
}
}