From 940ebab3adc0cdaaf8e14daca177941ff2ae1292 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 16 Jul 2025 15:15:33 -0700 Subject: [PATCH 01/10] get canned messages after the module is saved in all cases --- Meshtastic/Helpers/BLEManager.swift | 5 +---- Meshtastic/Views/Helpers/ConnectedDevice.swift | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index cc290f39..9d3fdc33 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -818,10 +818,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate nowKnown = true moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: cp.num), nodeLongName: cp.longName) if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { - if decodedInfo.moduleConfig.cannedMessage.enabled { - _ = self.getCannedMessageModuleMessages(destNum: cp.num, wantResponse: true) - - } + _ = self.getCannedMessageModuleMessages(destNum: cp.num, wantResponse: true) } if decodedInfo.config.payloadVariant == Config.OneOf_PayloadVariant.device(decodedInfo.config.device) { var dc = decodedInfo.config.device diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index e0dc8a02..a42d3877 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -54,13 +54,13 @@ struct ConnectedDevice: View { } else { // Create a container for Bluetooth off state HStack { - Text("bluetooth.off".localized) + Text("Bluetooth is off".localized) .font(.subheadline) .foregroundColor(.red) .accessibilityHidden(true) } .accessibilityElement(children: .ignore) - .accessibilityLabel("bluetooth.off".localized) + .accessibilityLabel("Bluetooth is off".localized) } } } From bb6320a03353ac0f4e084359e844304b022da5a5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 16 Jul 2025 15:19:53 -0700 Subject: [PATCH 02/10] Rename app smart position --- Settings.bundle/Root.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Settings.bundle/Root.plist b/Settings.bundle/Root.plist index e67ebad7..681fc817 100644 --- a/Settings.bundle/Root.plist +++ b/Settings.bundle/Root.plist @@ -62,7 +62,7 @@ Type PSToggleSwitchSpecifier Title - Smart Position + Accurate Locations Only Key enableSmartPosition DefaultValue From dde0ea080f21833e498933571e1b94e9918c49d3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 16 Jul 2025 15:20:25 -0700 Subject: [PATCH 03/10] Bump positions --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 828dcafb..54a3aa75 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1856,7 +1856,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.12; + MARKETING_VERSION = 2.6.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1889,7 +1889,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.12; + MARKETING_VERSION = 2.6.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1920,7 +1920,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.12; + MARKETING_VERSION = 2.6.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1952,7 +1952,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.12; + MARKETING_VERSION = 2.6.13; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 2b0ce47f2297ab49656911eb7d12e6adb71d8929 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 16 Jul 2025 15:54:53 -0700 Subject: [PATCH 04/10] Fixing linting errors caused by the bens --- Meshtastic.xcodeproj/project.pbxproj | 4 +- .../xcschemes/Meshtastic.xcscheme | 2 +- .../xcschemes/WidgetsExtension.xcscheme | 2 +- .../AppIntents/FactoryResetNodeIntent.swift | 2 - Meshtastic/AppIntents/RestartNodeIntent.swift | 1 - .../AppIntents/SendWaypointIntent.swift | 4 +- Meshtastic/Extensions/UserDefaults.swift | 5 +- Meshtastic/Helpers/BLEManager.swift | 3 - Meshtastic/Helpers/ContactURLHandler.swift | 4 - Meshtastic/MeshtasticApp.swift | 2 - Meshtastic/MeshtasticAppDelegate.swift | 6 +- Meshtastic/Persistence/UpdateCoreData.swift | 1 - Meshtastic/Views/Helpers/ChannelLock.swift | 7 +- Meshtastic/Views/Messages/MessageText.swift | 78 ++++++++----------- .../Views/Nodes/Helpers/NodeDetail.swift | 39 ++-------- Meshtastic/Views/Settings/Channels.swift | 1 - .../Views/Settings/SaveChannelQRCode.swift | 23 ------ 17 files changed, 54 insertions(+), 130 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 54a3aa75..7d6f5b32 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1295,7 +1295,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1540; - LastUpgradeCheck = 1630; + LastUpgradeCheck = 1640; TargetAttributes = { 25F5D5C62C4375A8008036E3 = { CreatedOnToolsVersion = 15.4; @@ -1713,6 +1713,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -1778,6 +1779,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; diff --git a/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme b/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme index 53ea5ca7..50dc8316 100644 --- a/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme +++ b/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme @@ -1,6 +1,6 @@ some IntentResult { - if !BLEManager.shared.isConnected { throw AppIntentErrors.AppIntentError.notConnected } diff --git a/Meshtastic/AppIntents/SendWaypointIntent.swift b/Meshtastic/AppIntents/SendWaypointIntent.swift index fb0f97c3..ba589ee6 100644 --- a/Meshtastic/AppIntents/SendWaypointIntent.swift +++ b/Meshtastic/AppIntents/SendWaypointIntent.swift @@ -11,7 +11,7 @@ import AppIntents import MeshtasticProtobufs struct SendWaypointIntent: AppIntent { - + var defaultDate = Date.now.addingTimeInterval(60 * 480) static var title = LocalizedStringResource("Send a Waypoint") @@ -83,11 +83,9 @@ struct SendWaypointIntent: AppIntent { newWaypoint.icon = unicode newWaypoint.name = name newWaypoint.description_p = description - if let expirationDate = expiration { newWaypoint.expire = UInt32(expirationDate.timeIntervalSince1970) } - if isLocked { if let connectedPeripheral = BLEManager.shared.connectedPeripheral { newWaypoint.lockedTo = UInt32(connectedPeripheral.num) diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 0b124ac5..a8f550e7 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -158,12 +158,13 @@ extension UserDefaults { @UserDefault(.mapReportingOptIn, defaultValue: false) static var mapReportingOptIn: Bool - + @UserDefault(.usageDataAndCrashReporting, defaultValue: true) static var usageDataAndCrashReporting: Bool + @UserDefault(.firstLaunch, defaultValue: true) static var firstLaunch: Bool - + @UserDefault(.showDeviceOnboarding, defaultValue: false) static var showDeviceOnboarding: Bool diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 9d3fdc33..3c50bd26 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1142,8 +1142,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate sendWantConfig() } - - // MARK: Share Location Position Update Timer // Use context to pass the radio name with the timer // Use a RunLoop to prevent the timer from running on the main UI thread @@ -1159,7 +1157,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == NONCE_ONLY_DB { Logger.mesh.info("🤜 [BLE] Want Config DB Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") } - case FROMNUM_UUID: Logger.services.info("🗞️ [BLE] (Notify) characteristic value will be read next") default: diff --git a/Meshtastic/Helpers/ContactURLHandler.swift b/Meshtastic/Helpers/ContactURLHandler.swift index 749c8cbf..68334fc0 100644 --- a/Meshtastic/Helpers/ContactURLHandler.swift +++ b/Meshtastic/Helpers/ContactURLHandler.swift @@ -11,15 +11,11 @@ import TipKit import MeshtasticProtobufs struct ContactURLHandler { - static var minimumContactVersion = "2.6.9" - - static func handleContactUrl(url: URL, bleManager: BLEManager) { let supportedVersion = UserDefaults.firmwareVersion == "0.0.0" || minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedAscending || minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame - if !supportedVersion { let alertController = UIAlertController( title: "Firmware Upgrade Required", diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index d4de2bf7..19a001e1 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -46,9 +46,7 @@ struct MeshtasticAppleApp: App { trackingConsent: UserDefaults.usageDataAndCrashReporting ? .granted : .notGranted, ) DatadogCrashReporting.CrashReporting.enable() - Logs.enable() - Trace.enable( with: Trace.Configuration( sampleRate: 100, networkInfoEnabled: true // 100% sampling for development/testing, reduce for production diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 0c7ada6e..bfa2ed1e 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -50,7 +50,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat case "messageNotification.thumbsUpAction": if let channel = userInfo["channel"] as? Int32, let replyID = userInfo["messageId"] as? Int64 { - let tapbackResponse = !BLEManager.shared.sendMessage ( + let tapbackResponse = !BLEManager.shared.sendMessage( message: Tapbacks.thumbsUp.emojiString, toUserNum: userInfo["userNum"] as? Int64 ?? 0, channel: channel, @@ -64,7 +64,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat case "messageNotification.thumbsDownAction": if let channel = userInfo["channel"] as? Int32, let replyID = userInfo["messageId"] as? Int64 { - let tapbackResponse = !BLEManager.shared.sendMessage ( + let tapbackResponse = !BLEManager.shared.sendMessage( message: Tapbacks.thumbsDown.emojiString, toUserNum: userInfo["userNum"] as? Int64 ?? 0, channel: channel, @@ -79,7 +79,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat if let userInput = (response as? UNTextInputNotificationResponse)?.userText, let channel = userInfo["channel"] as? Int32, let replyID = userInfo["messageId"] as? Int64 { - let tapbackResponse = !BLEManager.shared.sendMessage ( + let tapbackResponse = !BLEManager.shared.sendMessage( message: userInput, toUserNum: userInfo["userNum"] as? Int64 ?? 0, channel: channel, diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 63f9efbc..3c762a49 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -1369,7 +1369,6 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod do { try context.save() Logger.data.info("💾 [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - } catch { context.rollback() let nsError = error as NSError diff --git a/Meshtastic/Views/Helpers/ChannelLock.swift b/Meshtastic/Views/Helpers/ChannelLock.swift index 3a66dc5a..2621311a 100644 --- a/Meshtastic/Views/Helpers/ChannelLock.swift +++ b/Meshtastic/Views/Helpers/ChannelLock.swift @@ -7,19 +7,18 @@ import SwiftUI struct ChannelLock: View { - + @ObservedObject var channel: ChannelEntity - var body: some View { /// Unencrypted - using no key at all or a known 1 byte key if channel.psk?.hexDescription.count ?? 0 < 3 { let preciseLoction = 17...32 // Using precise location and have MQTT uplink enabled - if channel.uplinkEnabled && preciseLoction ~= (Int(channel.positionPrecision)) { + if channel.uplinkEnabled && preciseLoction ~= (Int(channel.positionPrecision)) { Image(systemName: "lock.open.trianglebadge.exclamationmark.fill") .foregroundColor(.red) // Using precise location - } else if preciseLoction ~= (Int(channel.positionPrecision)) { + } else if preciseLoction ~= (Int(channel.positionPrecision)) { Image(systemName: "lock.open.fill") .foregroundColor(.red) // Just unencrypted without any location or MQTT diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index ac033b1f..b93f413f 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -10,9 +10,7 @@ struct MessageText: View { locale: Locale.current ) static let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss:a") - @Environment(\.managedObjectContext) var context - let message: MessageEntity let tapBackDestination: MessageDestination let isCurrentUser: Bool @@ -21,7 +19,6 @@ struct MessageText: View { @State private var saveChannels = false @State private var channelSettings: String? @State private var addChannels = false - @State private var isShowingDeleteConfirmation = false var body: some View { @@ -41,10 +38,10 @@ struct MessageText: View { HStack { Spacer() Image(systemName: "lock.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .green) - .font(.system(size: 20)) - .offset(x: 8, y: 8) + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .green) + .font(.system(size: 20)) + .offset(x: 8, y: 8) } } } @@ -56,10 +53,10 @@ struct MessageText: View { HStack { Spacer() Image(systemName: "envelope.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .gray) - .font(.system(size: 20)) - .offset(x: 8, y: 8) + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .gray) + .font(.system(size: 20)) + .offset(x: 8, y: 8) } } } @@ -89,39 +86,32 @@ struct MessageText: View { } .environment(\.openURL, OpenURLAction { url in channelSettings = nil - - if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { - // Handle contact URL - ContactURLHandler.handleContactUrl(url: url, bleManager: BLEManager.shared) - return .handled // Prevent default browser opening - } else if url.absoluteString.lowercased().contains("meshtastic.org/e/") { - // Handle channel URL - let components = url.absoluteString.components(separatedBy: "#") - guard !components.isEmpty, let lastComponent = components.last else { - Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)") - return .discarded - } - - self.addChannels = Bool(url.query?.contains("add=true") ?? false) - guard let lastComponent = components.last else { - Logger.services.error("Channel URL missing fragment component: \(url.absoluteString, privacy: .public)") - self.channelSettings = nil - return .discarded - } - - self.channelSettings = lastComponent.components(separatedBy: "?").first ?? "" - - - Logger.services.debug("Add Channel: \(self.addChannels, privacy: .public)") - self.saveChannels = true - Logger.mesh.debug("Opening Channel Settings URL: \(url.absoluteString, privacy: .public)") - return .handled // Prevent default browser opening - } - - return .systemAction // Open other URLs in browser -}) - - // Display sheet for channel settings + if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { + // Handle contact URL + ContactURLHandler.handleContactUrl(url: url, bleManager: BLEManager.shared) + return .handled // Prevent default browser opening + } else if url.absoluteString.lowercased().contains("meshtastic.org/e/") { + // Handle channel URL + let components = url.absoluteString.components(separatedBy: "#") + guard !components.isEmpty, let lastComponent = components.last else { + Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)") + return .discarded + } + self.addChannels = Bool(url.query?.contains("add=true") ?? false) + guard let lastComponent = components.last else { + Logger.services.error("Channel URL missing fragment component: \(url.absoluteString, privacy: .public)") + self.channelSettings = nil + return .discarded + } + self.channelSettings = lastComponent.components(separatedBy: "?").first ?? "" + Logger.services.debug("Add Channel: \(self.addChannels, privacy: .public)") + self.saveChannels = true + Logger.mesh.debug("Opening Channel Settings URL: \(url.absoluteString, privacy: .public)") + return .handled // Prevent default browser opening + } + return .systemAction // Open other URLs in browser + }) + // Display sheet for channel settings .sheet(isPresented: Binding( get: { saveChannels && !(channelSettings == nil) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index d98941bb..24835516 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -19,22 +19,17 @@ struct NodeDetail: View { var modemPreset: ModemPresets = ModemPresets( rawValue: UserDefaults.modemPreset ) ?? ModemPresets.longFast - @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false @State private var dateFormatRelative: Bool = true - // The node the device is currently connected to var connectedNode: NodeInfoEntity? - // The node information being displayed on the detail screen @ObservedObject var node: NodeInfoEntity - var columnVisibility = NavigationSplitViewVisibility.all - var body: some View { NavigationStack { List { @@ -42,7 +37,6 @@ struct NodeDetail: View { id: bleManager.connectedPeripheral?.num ?? -1, context: context ) - Section("Hardware") { NodeInfoItem(node: node) } @@ -106,10 +100,9 @@ struct NodeDetail: View { } Spacer() Text(String(node.num)) - .textSelection(.enabled) + .textSelection(.enabled) } .accessibilityElement(children: .combine) - HStack { Label { Text("User Id") @@ -119,10 +112,9 @@ struct NodeDetail: View { } Spacer() Text(node.num.toHex()) - .textSelection(.enabled) + .textSelection(.enabled) } .accessibilityElement(children: .combine) - if node.user?.keyMatch ?? false { if let publicKey = node.user?.publicKey { HStack { @@ -134,7 +126,7 @@ struct NodeDetail: View { } Spacer() Button(action: { - context.perform{ + context.perform { UIPasteboard.general.string = publicKey.base64EncodedString() } }) { @@ -147,7 +139,6 @@ struct NodeDetail: View { .accessibilityElement(children: .combine) } } - if let metadata = node.metadata { HStack { Label { @@ -157,12 +148,10 @@ struct NodeDetail: View { .symbolRenderingMode(.multicolor) } Spacer() - Text(metadata.firmwareVersion ?? "Unknown".localized) } .accessibilityElement(children: .combine) } - if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) { HStack { Label { @@ -189,7 +178,6 @@ struct NodeDetail: View { } .accessibilityElement(children: .combine) } - if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds { HStack { Label { @@ -200,7 +188,6 @@ struct NodeDetail: View { .symbolRenderingMode(.hierarchical) } Spacer() - let now = Date.now let later = now + TimeInterval(uptimeSeconds) let uptime = (now.. 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { HStack { Label { @@ -232,7 +218,6 @@ struct NodeDetail: View { dateFormatRelative.toggle() } } - if let lastHeard = node.lastHeard, lastHeard.timeIntervalSince1970 > 0 && lastHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! { HStack { Label { @@ -242,7 +227,6 @@ struct NodeDetail: View { .symbolRenderingMode(.multicolor) } Spacer() - if dateFormatRelative, let text = Self.relativeFormatter.string(for: lastHeard) { if lastHeard.formatted() != "Unknown Age".localized { Text(text) @@ -259,7 +243,6 @@ struct NodeDetail: View { } } } - // Note, as you add widgets, you should add to the `hasDataForLatestPositions` array // This will make sure the "Environment" section is only displayed when the node has a position // to use with WeatherKit, or has actual data in the most recent EnvironmentMetrics entity @@ -298,7 +281,7 @@ struct NodeDetail: View { let windGust = node.latestEnvironmentMetrics?.windGust.map { Measurement(value: Double($0), unit: UnitSpeed.metersPerSecond) } let direction = cardinalValue(from: Double(node.latestEnvironmentMetrics?.windDirection ?? 0)) WindCompactWidget(speed: windSpeedMeasurement.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))), - gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction) + gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction) } if let rainfall1h = node.latestEnvironmentMetrics?.rainfall1H { let locale = NSLocale.current as NSLocale @@ -370,7 +353,6 @@ struct NodeDetail: View { } } .disabled(!node.hasDeviceMetrics) - NavigationLink { NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num) } label: { @@ -382,7 +364,6 @@ struct NodeDetail: View { } } .disabled(!node.hasPositions) - NavigationLink { PositionLog(node: node) } label: { @@ -394,7 +375,6 @@ struct NodeDetail: View { } } .disabled(!node.hasPositions) - NavigationLink { EnvironmentMetricsLog(node: node) } label: { @@ -406,7 +386,6 @@ struct NodeDetail: View { } } .disabled(!node.hasEnvironmentMetrics) - NavigationLink { TraceRouteLog(node: node) } label: { @@ -418,7 +397,6 @@ struct NodeDetail: View { } } .disabled(node.traceRoutes?.count ?? 0 == 0) - NavigationLink { PowerMetricsLog(node: node) } label: { @@ -430,7 +408,6 @@ struct NodeDetail: View { } } .disabled(!node.hasPowerMetrics) - NavigationLink { DetectionSensorLog(node: node) } label: { @@ -442,7 +419,6 @@ struct NodeDetail: View { } } .disabled(!node.hasDetectionSensorMetrics) - if node.hasPax { NavigationLink { PaxCounterLog(node: node) @@ -457,7 +433,6 @@ struct NodeDetail: View { .disabled(!node.hasPax) } } - Section("Actions") { if let user = node.user { NodeAlertsButton( @@ -466,7 +441,6 @@ struct NodeDetail: View { user: user ) } - if let connectedNode { FavoriteNodeButton( bleManager: bleManager, @@ -491,7 +465,7 @@ struct NodeDetail: View { } if node.hasPositions { NavigateToButton(node: node) - } + } IgnoreNodeButton( bleManager: bleManager, context: context, @@ -506,7 +480,6 @@ struct NodeDetail: View { } } } - if let metadata = node.metadata, let connectedNode, self.bleManager.connectedPeripheral != nil { @@ -529,7 +502,6 @@ struct NodeDetail: View { } } } - if metadata.canShutdown { Button { showingShutdownConfirm = true @@ -549,7 +521,6 @@ struct NodeDetail: View { } } } - Button { showingRebootConfirm = true } label: { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 8e38f27b..b1a2518c 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -241,7 +241,6 @@ struct Channels: View { #endif } } - if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { Button { diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index 49c78b0c..0a83c900 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -12,18 +12,14 @@ import MeshtasticProtobufs struct SaveChannelQRCode: View { @Environment(\.dismiss) private var dismiss @Environment(\.managedObjectContext) var context - let channelSetLink: String var addChannels: Bool = false var bleManager: BLEManager - @State private var showError: Bool = false @State private var errorMessage: String = "" @State private var connectedToDevice: Bool = false @State private var loraChanges: [String] = [] @State private var okToMQTT: Bool = false - - var body: some View { VStack { Text("\(addChannels ? "Add" : "Replace all") Channels?") @@ -47,7 +43,6 @@ struct SaveChannelQRCode: View { } .padding() } - if showError { Text(errorMessage.isEmpty ? "Channels being added from the QR code did not save. When adding channels the names must be unique." : errorMessage) .fixedSize(horizontal: false, vertical: true) @@ -55,7 +50,6 @@ struct SaveChannelQRCode: View { .font(.callout) .padding() } - HStack { if !showError { Button { @@ -72,7 +66,6 @@ struct SaveChannelQRCode: View { } else { channelData = channelSetLink } - let success = bleManager.saveChannelSet(base64UrlString: channelData, addChannels: addChannels, okToMQTT: okToMQTT) if success { dismiss() @@ -119,11 +112,8 @@ struct SaveChannelQRCode: View { fetchLoRaConfigChanges() } } - private func extractChannelDataFromURL(_ urlString: String) -> String? { Logger.data.info("Extracting channel data from URL: \(urlString)") - - if let url = URL(string: urlString) { // Get the fragment (part after #) if let fragment = url.fragment, !fragment.isEmpty { @@ -131,7 +121,6 @@ struct SaveChannelQRCode: View { return fragment } } - // Fallback: manually extract everything after the last # if let hashIndex = urlString.lastIndex(of: "#") { let startIndex = urlString.index(after: hashIndex) @@ -141,11 +130,9 @@ struct SaveChannelQRCode: View { return channelData } } - Logger.data.error("Failed to extract channel data from URL: \(urlString)") return nil } - private func fetchLoRaConfigChanges() { var currentLoRaConfig: Config.LoRaConfig? @@ -163,13 +150,10 @@ struct SaveChannelQRCode: View { // Assume it's already the base64 data channelData = channelSetLink } - Logger.data.info("Processing channel data: \(channelData)") - // Fetch current LoRa config from Core Data let fetchRequest = NodeInfoEntity.fetchRequest() fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? 0)) - do { let nodes = try context.fetch(fetchRequest) if let node = nodes.first { @@ -178,7 +162,6 @@ struct SaveChannelQRCode: View { } catch { Logger.data.error("Failed to fetch NodeInfoEntity: \(error.localizedDescription, privacy: .public)") } - // Decode base64url string let decodedString = channelData.base64urlToBase64() guard let decodedData = Data(base64Encoded: decodedString) else { @@ -187,7 +170,6 @@ struct SaveChannelQRCode: View { showError = true return } - do { let channelSet = try ChannelSet(serializedBytes: decodedData) let newLoRaConfig = channelSet.loraConfig @@ -244,7 +226,6 @@ struct SaveChannelQRCode: View { } else { // Compare against default values when no current config exists let defaultConfig = getDefaultLoRaConfig() - if newLoRaConfig.hopLimit != defaultConfig.hopLimit { changes.append("Hop Limit: \(defaultConfig.hopLimit) -> \(newLoRaConfig.hopLimit)") } @@ -287,16 +268,13 @@ struct SaveChannelQRCode: View { changes.append("Ignore MQTT: \(defaultConfig.ignoreMqtt) -> \(newLoRaConfig.ignoreMqtt)") } } - loraChanges = changes - } catch { Logger.data.error("Failed to decode ChannelSet: \(error.localizedDescription, privacy: .public)") errorMessage = "Failed to decode channel configuration" showError = true } } - private func getDefaultLoRaConfig() -> Config.LoRaConfig { var config = Config.LoRaConfig() config.hopLimit = 3 @@ -316,7 +294,6 @@ struct SaveChannelQRCode: View { return config } } - extension LoRaConfigEntity { func toProto() -> Config.LoRaConfig { var config = Config.LoRaConfig() From 61768319306c3d8a82870a606ff67a46951c33c1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 16 Jul 2025 23:12:47 -0700 Subject: [PATCH 05/10] Simplify router confirmatin warning. --- .../Views/Settings/Config/DeviceConfig.swift | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 3085192d..4445e017 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -31,7 +31,6 @@ struct DeviceConfig: View { @State var tripleClickAsAdHocPing = true @State var tzdef = "" @State private var showRouterWarning = false - @State private var confirmWarning = false var body: some View { VStack { @@ -45,15 +44,9 @@ struct DeviceConfig: View { Text(dr.name).tag(dr.rawValue as Int) } } - .onChange(of: deviceRole) { oldValue, newValue in - if !confirmWarning { - if [2, 11].contains(newValue) { - pendingDeviceRole = newValue - deviceRole = oldValue // Reset selection until confirmed - showRouterWarning = true - } - } else { - confirmWarning = false + .onChange(of: deviceRole) { _, newRole in + if hasChanges && [2, 4, 11].contains(newRole) { + showRouterWarning = true } } .confirmationDialog( @@ -63,15 +56,13 @@ struct DeviceConfig: View { ) { Button("Confirm") { - deviceRole = pendingDeviceRole - pendingDeviceRole = 0 - confirmWarning = true + hasChanges = true } Button("Cancel", role: .cancel) { - pendingDeviceRole = 0 + setDeviceValues() } } message: { - Text("The Router roles are designed for high vantage locations like mountaintops and towers. This node needs to be able to have a good direct connection to most of the nodes on the network or else this will significantly hurt the network.") + Text("The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh.") } Text(DeviceRoles(rawValue: deviceRole)?.description ?? "") .foregroundColor(.gray) @@ -334,5 +325,6 @@ struct DeviceConfig: View { self.tripleClickAsAdHocPing = node?.deviceConfig?.tripleClickAsAdHocPing ?? false self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true self.tzdef = node?.deviceConfig?.tzdef ?? "" + hasChanges = false } } From 6a75793f57dbbbf6c69f75812f5ef2d22b0e1785 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 16 Jul 2025 23:29:23 -0700 Subject: [PATCH 06/10] Add scroll view to notification and location permissions --- Localizable.xcstrings | 4 + .../Views/Onboarding/DeviceOnboarding.swift | 192 +++++++++--------- 2 files changed, 102 insertions(+), 94 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 8c321273..8cfe0005 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -36647,6 +36647,7 @@ } }, "The Router roles are designed for high vantage locations like mountaintops and towers. This node needs to be able to have a good direct connection to most of the nodes on the network or else this will significantly hurt the network." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -36673,6 +36674,9 @@ } } } + }, + "The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh." : { + }, "The secondary public key authorized to send admin messages to this node." : { "localizations" : { diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index d77b0192..2dce2ce2 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -78,110 +78,114 @@ struct DeviceOnboarding: View { } var notificationView: some View { - VStack { + ScrollView(.vertical, showsIndicators: false) { VStack { - Text("App Notifications") - .font(.largeTitle.bold()) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - } - Spacer() - VStack(alignment: .leading, spacing: 16) { - Text("Send Notifications") - .font(.title2.bold()) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - makeRow( - icon: "message", - title: "Incoming Messages", - subtitle: "Meshtastic notifications for channel messages and direct messages" - ) - makeRow( - icon: "flipphone", - title: "New Nodes", - subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device." - ) - makeRow( - icon: "battery.25percent", - title: "Low Battery", - subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device." - ) - Text("Critical Alerts") - .font(.title2.bold()) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - makeRow( - icon: "exclamationmark.triangle.fill", - subtitle: "Select packets sent as critical will ignore the mute switch and Do Not Disturb settings in the OS notification center." - ) - } - .padding() - Spacer() - Button { - Task { - await requestNotificationsPermissions() - await goToNextStep(after: .notifications) + VStack { + Text("App Notifications") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } - } label: { - Text("Configure notification permissions") - .frame(maxWidth: .infinity) + Spacer() + VStack(alignment: .leading, spacing: 16) { + Text("Send Notifications") + .font(.title2.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "message", + title: "Incoming Messages", + subtitle: "Meshtastic notifications for channel messages and direct messages" + ) + makeRow( + icon: "flipphone", + title: "New Nodes", + subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device." + ) + makeRow( + icon: "battery.25percent", + title: "Low Battery", + subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device." + ) + Text("Critical Alerts") + .font(.title2.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "exclamationmark.triangle.fill", + subtitle: "Select packets sent as critical will ignore the mute switch and Do Not Disturb settings in the OS notification center." + ) + } + .padding() + Spacer() + Button { + Task { + await requestNotificationsPermissions() + await goToNextStep(after: .notifications) + } + } label: { + Text("Configure notification permissions") + .frame(maxWidth: .infinity) + } + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) } - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) } } var locationView: some View { - VStack { + ScrollView(.vertical, showsIndicators: false) { VStack { - Text("Phone Location") - .font(.largeTitle.bold()) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - } - VStack(alignment: .leading, spacing: 16) { - Text("Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from Settings > App Settings > Open Settings.") - .font(.body.bold()) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - makeRow( - icon: "location", - title: "Share Location", - subtitle: "Use your phone GPS to send locations to your node to instead of using a hardware GPS on your node." - ) - makeRow( - icon: "lines.measurement.horizontal", - title: "Distance Measurements", - subtitle: "Used to display the distance between your phone and other Meshtastic nodes where positions are available." - ) - makeRow( - icon: "line.3.horizontal.decrease.circle", - title: "Distance Filters", - subtitle: "Filter the node list and mesh map based on proximity to your phone." - ) - makeRow( - icon: "mappin", - title: "Mesh Map Location", - subtitle: "Enables the blue location dot for your phone in the mesh map." - ) - } - .padding() - Spacer() - Button { - Task { - await requestLocationPermissions() + VStack { + Text("Phone Location") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } - } label: { - Text("Configure Location Permissions") - .frame(maxWidth: .infinity) + VStack(alignment: .leading, spacing: 16) { + Text("Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from Settings > App Settings > Open Settings.") + .font(.body.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "location", + title: "Share Location", + subtitle: "Use your phone GPS to send locations to your node to instead of using a hardware GPS on your node." + ) + makeRow( + icon: "lines.measurement.horizontal", + title: "Distance Measurements", + subtitle: "Used to display the distance between your phone and other Meshtastic nodes where positions are available." + ) + makeRow( + icon: "line.3.horizontal.decrease.circle", + title: "Distance Filters", + subtitle: "Filter the node list and mesh map based on proximity to your phone." + ) + makeRow( + icon: "mappin", + title: "Mesh Map Location", + subtitle: "Enables the blue location dot for your phone in the mesh map." + ) + } + .padding() + Spacer() + Button { + Task { + await requestLocationPermissions() + } + } label: { + Text("Configure Location Permissions") + .frame(maxWidth: .infinity) + } + .padding() + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) } - .padding() - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) } } From 472534a1baf6e29a82992eef477a445ebe32603e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 16 Jul 2025 23:33:18 -0700 Subject: [PATCH 07/10] Show scroll view indicators --- Meshtastic/Views/Onboarding/DeviceOnboarding.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index 2dce2ce2..e9ce4593 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -32,7 +32,7 @@ struct DeviceOnboarding: View { var welcomeView: some View { VStack { - ScrollView(.vertical, showsIndicators: false) { + ScrollView(.vertical) { VStack { // Title title @@ -78,7 +78,7 @@ struct DeviceOnboarding: View { } var notificationView: some View { - ScrollView(.vertical, showsIndicators: false) { + ScrollView(.vertical) { VStack { VStack { Text("App Notifications") @@ -136,7 +136,7 @@ struct DeviceOnboarding: View { } var locationView: some View { - ScrollView(.vertical, showsIndicators: false) { + ScrollView(.vertical) { VStack { VStack { Text("Phone Location") From 78d3dbc8c386779a81880c374b8d0e54a3c12b9d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 16 Jul 2025 23:35:20 -0700 Subject: [PATCH 08/10] Remove stale translation key --- Localizable.xcstrings | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 8cfe0005..a5670f11 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -36646,35 +36646,6 @@ } } }, - "The Router roles are designed for high vantage locations like mountaintops and towers. This node needs to be able to have a good direct connection to most of the nodes on the network or else this will significantly hurt the network." : { - "extractionState" : "stale", - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "I ruoli di router sono progettati per posizioni elevate, come le cime delle montagne e le torri. Questo nodo deve essere in grado di avere una buona connessione diretta con la maggior parte dei nodi della rete, altrimenti danneggia significativamente la rete." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ルーター役割は山頂や塔のような見晴らしの良い高所での使用を想定して設計されています。このノードは、ネットワーク内の大部分のノードと良好な直接接続を保持できる必要があります。そうでなければ、ネットワークに深刻な影響を与えることになります。" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Рутер улоге су намењене локацијама са високим положајем као што су врхови планина и куле. Овај чвор мора имати добру директну везу са већином чворова у мрежи, иначе ће то значајно оштетити мрежу." - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "Router 模式專為高處位置(如山頂或塔台)設計。此節點必須能夠與網路中的大多數節點保持良好的直接連線,否則將會嚴重影響整體網路效能。" - } - } - } - }, "The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh." : { }, From b5ea0239f69f21436f70187202878e1b8b10259e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 16 Jul 2025 23:36:39 -0700 Subject: [PATCH 09/10] remove pending device role --- Meshtastic/Views/Settings/Config/DeviceConfig.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 4445e017..a9443028 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -21,7 +21,6 @@ struct DeviceConfig: View { @State private var isPresentingFactoryResetConfirm = false @State var hasChanges = false @State var deviceRole = 0 - @State private var pendingDeviceRole = 0 @State var buzzerGPIO = 0 @State var buttonGPIO = 0 @State var rebroadcastMode = 0 From 5c186286a0df37e2b56025686c78982a05ef0fa8 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 16 Jul 2025 23:39:07 -0700 Subject: [PATCH 10/10] Remove magic values --- Meshtastic/Views/Settings/Config/DeviceConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index a9443028..8b8881b3 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -44,7 +44,7 @@ struct DeviceConfig: View { } } .onChange(of: deviceRole) { _, newRole in - if hasChanges && [2, 4, 11].contains(newRole) { + if hasChanges && [DeviceRoles.router.rawValue, DeviceRoles.routerLate.rawValue, DeviceRoles.repeater.rawValue].contains(newRole) { showRouterWarning = true } }