From d55a528f7fb3f759bc1d157f7936dd54895b163f Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:25:30 -0700 Subject: [PATCH 01/12] Update factory reset and restart intents --- Localizable.xcstrings | 14 ++++++++++++++ Meshtastic.xcodeproj/project.pbxproj | 4 ---- Meshtastic/AppIntents/FactoryResetNodeIntent.swift | 14 +++++++++++--- Meshtastic/AppIntents/RestartNodeIntent.swift | 1 - 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9a6c28c1..08ef7fcb 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -13633,6 +13633,9 @@ } } } + }, + "Hard Reset" : { + }, "Hardware" : { "localizations" : { @@ -14972,6 +14975,9 @@ } } } + }, + "In addition to Config, Keys and BLE bonds will be wiped" : { + }, "Include" : { "localizations" : { @@ -23026,6 +23032,9 @@ } } } + }, + "Provide Confirmation" : { + }, "Public Key" : { "localizations" : { @@ -26425,6 +26434,7 @@ } }, "Send a Direct Message" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -26513,6 +26523,7 @@ } }, "Send a message to a certain meshtastic node" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -28178,6 +28189,9 @@ } } } + }, + "Show a confirmation dialog before performing the factory reset" : { + }, "Show alerts" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6f72607e..7584a821 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -59,7 +59,6 @@ B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; }; B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */; }; - BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */; }; BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */; }; BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; @@ -325,7 +324,6 @@ B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.swift; sourceTree = ""; }; - BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = ""; }; BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelSettingsIntent.swift; sourceTree = ""; }; BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = ""; }; @@ -694,7 +692,6 @@ BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */, BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */, BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */, - BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */, BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */, ); path = AppIntents; @@ -1378,7 +1375,6 @@ DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */, DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */, DDA9515C2BC6631200CEA535 /* TelemetryEnums.swift in Sources */, - BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */, DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */, DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */, diff --git a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift index 60946b00..9994c19d 100644 --- a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift +++ b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift @@ -11,11 +11,19 @@ import AppIntents struct FactoryResetNodeIntent: AppIntent { static var title: LocalizedStringResource = "Factory Reset" static var description: IntentDescription = "Perform a factory reset on the node you are connected to" + + @Parameter(title: "Hard Reset", description: "In addition to Config, Keys and BLE bonds will be wiped", default: false) + var hardReset: Bool + + @Parameter(title: "Provide Confirmation", description: "Show a confirmation dialog before performing the factory reset", default: true) + var provideConfirmation: Bool func perform() async throws -> some IntentResult { // Request user confirmation before performing the factory reset - try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"), confirmationActionName: ConfirmationActionName - .custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true)) + if provideConfirmation { + try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"), confirmationActionName: ConfirmationActionName + .custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true)) + } // Ensure the node is connected if !BLEManager.shared.isConnected { @@ -29,7 +37,7 @@ struct FactoryResetNodeIntent: AppIntent { let toUser = connectedNode.user { // Attempt to send a factory reset command, throw an error if it fails - if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser) { + if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser, resetDevice: hardReset) { throw AppIntentErrors.AppIntentError.message("Failed to perform factory reset") } } else { diff --git a/Meshtastic/AppIntents/RestartNodeIntent.swift b/Meshtastic/AppIntents/RestartNodeIntent.swift index 3859c114..bff6affb 100644 --- a/Meshtastic/AppIntents/RestartNodeIntent.swift +++ b/Meshtastic/AppIntents/RestartNodeIntent.swift @@ -15,7 +15,6 @@ struct RestartNodeIntent: AppIntent { func perform() async throws -> some IntentResult { - try await requestConfirmation(result: .result(dialog: "Reboot node?")) if !BLEManager.shared.isConnected { throw AppIntentErrors.AppIntentError.notConnected From bd88e1fdd325f81b8d0a9af6b53fdd54b4af3532 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Jun 2025 20:19:42 -0700 Subject: [PATCH 02/12] Bump version, add channel help to settings channel update locks to be red only if precise location is being shared on an unencrypted channel --- Localizable.xcstrings | 14 ++++++- Meshtastic.xcodeproj/project.pbxproj | 4 ++ Meshtastic/Helpers/BLEManager.swift | 6 +-- Meshtastic/Views/Helpers/ChannelLock.swift | 35 ++++++++++++++++ .../Views/Helpers/Help/ChannelsHelp.swift | 33 ++++++++++++--- Meshtastic/Views/Messages/ChannelList.swift | 10 +---- Meshtastic/Views/Settings/Channels.swift | 33 +++++++++++---- Meshtastic/Views/Settings/ShareChannels.swift | 40 ++++++++++++++----- 8 files changed, 138 insertions(+), 37 deletions(-) create mode 100644 Meshtastic/Views/Helpers/ChannelLock.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 28dd7f9c..ba0d7976 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1693,7 +1693,7 @@ } } }, - "A channel index of 0 indicates the primary channel where all broadcast packets are sent from." : { + "A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward." : { }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { @@ -1757,7 +1757,10 @@ } } }, - "A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted." : { + "A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key." : { + + }, + "A red open lock with a warning means the channel is not securely encrypted and is used for precise location data which is being uplinked to the internet via MQTT, it uses either no key at all or a 1 byte known key." : { }, "A Trace Route was sent, no response has been received." : { @@ -1781,6 +1784,9 @@ } } } + }, + "A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key." : { + }, "About" : { "localizations" : { @@ -28238,6 +28244,7 @@ } }, "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity." : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -28252,6 +28259,9 @@ } } } + }, + "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to start the live activity." : { + }, "Shut Down" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 956f7e9f..876c41c5 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */; }; DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */; }; DD1BEF502E0528AA0090CE24 /* PersistantTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */; }; + DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -380,6 +381,7 @@ DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = ""; }; DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsHelp.swift; sourceTree = ""; }; DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistantTips.swift; sourceTree = ""; }; + DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelLock.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -1057,6 +1059,7 @@ DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */, DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */, DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */, + DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */, DD47E3D526F17ED900029299 /* CircleText.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */, @@ -1384,6 +1387,7 @@ 25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */, 259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */, BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */, + DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */, 259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */, 259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */, DDDB444829F8A9C900EE2349 /* String.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index ffe00410..389e812a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -725,7 +725,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let message = CocoaMQTTMessage(topic: decodedInfo.mqttClientProxyMessage.topic, payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), retained: decodedInfo.mqttClientProxyMessage.retained) mqttManager.mqttClientProxy?.publish(message) } else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) { - var path = "meshtastic:///settings/debugLogs" if decodedInfo.clientNotification.hasReplyID { /// Set Sent bool on TraceRouteEntity to false if we got rate limited @@ -740,8 +739,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let nsError = error as NSError Logger.data.error("đŸ’Ĩ [TraceRouteEntity] Error Updating Core Data: \(nsError, privacy: .public)") } - } else if decodedInfo.clientNotification.message.starts(with: "You Device is configured with a low entropy") || decodedInfo.clientNotification.message.starts(with: "Compromised keys detected") - || decodedInfo.clientNotification.message.starts(with: "Remote device"){ + } + if decodedInfo.clientNotification.payloadVariant == ClientNotification.OneOf_PayloadVariant.lowEntropyKey(decodedInfo.clientNotification.lowEntropyKey) || + decodedInfo.clientNotification.payloadVariant == ClientNotification.OneOf_PayloadVariant.duplicatedPublicKey(decodedInfo.clientNotification.duplicatedPublicKey) { path = "meshtastic:///settings/security" } } diff --git a/Meshtastic/Views/Helpers/ChannelLock.swift b/Meshtastic/Views/Helpers/ChannelLock.swift new file mode 100644 index 00000000..3a66dc5a --- /dev/null +++ b/Meshtastic/Views/Helpers/ChannelLock.swift @@ -0,0 +1,35 @@ +// +// ChannelLock.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/22/25. +// +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)) { + Image(systemName: "lock.open.trianglebadge.exclamationmark.fill") + .foregroundColor(.red) + // Using precise location + } else if preciseLoction ~= (Int(channel.positionPrecision)) { + Image(systemName: "lock.open.fill") + .foregroundColor(.red) + // Just unencrypted without any location or MQTT + } else { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + } + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } +} diff --git a/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift index 830fe3cd..ad8b3b06 100644 --- a/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift +++ b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift @@ -21,25 +21,46 @@ struct ChannelsHelp: View { CircleText(text: String(0), color: .accentColor) .brightness(0.2) .offset(y: -10) - Text("A channel index of 0 indicates the primary channel where all broadcast packets are sent from.") + Text("A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward.") .fixedSize(horizontal: false, vertical: true) .padding(.bottom) + .padding(.leading, 7) } HStack { Image(systemName: "lock.fill") - .padding(.bottom) + .padding(.leading) + .padding(.trailing, 7) .foregroundColor(Color.green) - .font(.largeTitle) + .font(.title) Text("A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key.") .fixedSize(horizontal: false, vertical: true) .padding(.bottom) } HStack { - Image(systemName: "lock.slash.fill") + Image(systemName: "lock.open.fill") + .padding(.leading) + .foregroundColor(Color.yellow) + .font(.title) + Text("A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key.") + .fixedSize(horizontal: false, vertical: true) .padding(.bottom) + } + HStack { + Image(systemName: "lock.open.fill") + .padding(.leading) .foregroundColor(Color.red) - .font(.largeTitle) - Text("A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted.") + .font(.title) + Text("A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "lock.open.trianglebadge.exclamationmark.fill") + .padding(.leading) + .symbolRenderingMode(.multicolor) + .foregroundColor(Color.red) + .font(.title) + Text("A red open lock with a warning means the channel is not securely encrypted and is used for precise location data which is being uplinked to the internet via MQTT, it uses either no key at all or a 1 byte known key.") .fixedSize(horizontal: false, vertical: true) .padding(.bottom) } diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 940396b9..835c662d 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -58,13 +58,7 @@ struct ChannelList: View { VStack(alignment: .leading) { HStack { - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash.fill") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + ChannelLock(channel: channel) if channel.name?.isEmpty ?? false { if channel.role == 1 { Text(String("PrimaryChannel").camelCaseToWords()) @@ -173,7 +167,7 @@ struct ChannelList: View { } .sheet(isPresented: $showingHelp) { ChannelsHelp() - .presentationDetents([.medium, .large]) + .presentationDetents([.large]) .presentationDragIndicator(.visible) } .safeAreaInset(edge: .bottom, alignment: .leading) { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index d479403f..8e38f27b 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -47,6 +47,7 @@ struct Channels: View { /// Minimum Version for granular position configuration @State var minimumVersion = "2.2.24" + @State private var showingHelp = false @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), @@ -124,13 +125,7 @@ struct Channels: View { .brightness(0.1) VStack { HStack { - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash.fill") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + ChannelLock(channel: channel) if channel.name?.isEmpty ?? false { if channel.role == 1 { Text(String("PrimaryChannel").camelCaseToWords()).font(.headline) @@ -246,6 +241,7 @@ struct Channels: View { #endif } } + if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { Button { @@ -286,6 +282,29 @@ struct Channels: View { .padding() } } + .sheet(isPresented: $showingHelp) { + ChannelsHelp() + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } + .safeAreaInset(edge: .bottom, alignment: .leading) { + HStack { + Button(action: { + withAnimation { + showingHelp = !showingHelp + } + }) { + Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) .navigationTitle("Channels") .navigationBarItems(trailing: ZStack { diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 7e4068e2..a3788ff0 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -49,6 +49,7 @@ struct ShareChannels: View { var node: NodeInfoEntity? @State private var channelsUrl = "https://www.meshtastic.org/e/#" var qrCodeImage = QrCodeImage() + @State private var showingHelp = false var body: some View { @@ -82,13 +83,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()) - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash.fill") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + ChannelLock(channel: channel) } else if channel.index == 1 && channel.role > 0 { Toggle("Channel 1 Included", isOn: $includeChannel1) .toggleStyle(.switch) @@ -216,16 +211,39 @@ struct ShareChannels: View { .resizable() .scaledToFit() .frame( - minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), - maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6), + minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), + maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6), alignment: .top ) } } } } + .sheet(isPresented: $showingHelp) { + ChannelsHelp() + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } + .safeAreaInset(edge: .bottom, alignment: .leading) { + HStack { + Button(action: { + withAnimation { + showingHelp = !showingHelp + } + }) { + Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) .navigationTitle("Generate QR Code") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(trailing: From bd6f4ad7ca23ba184ad3ae62ab0442f336df1694 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 22 Jun 2025 20:34:13 -0700 Subject: [PATCH 03/12] Bump version --- 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 876c41c5..55581bc9 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1828,7 +1828,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1861,7 +1861,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1892,7 +1892,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1924,7 +1924,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.8; + MARKETING_VERSION = 2.6.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From fc206bbc05e1d609a939d50939f54ea9bb6cf03a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 23 Jun 2025 09:25:39 -0700 Subject: [PATCH 04/12] Reboot on key changes --- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index e252819a..1897bf43 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -349,6 +349,14 @@ struct SecurityConfig: View { } } hasChanges = false + if keyUpdated { + if !bleManager.sendReboot( + fromUser: fromUser, + toUser: toUser + ) { + Logger.mesh.warning("Reboot Failed") + } + } goBack() } } From aa73a82404da25f88c9099bf55e100ebd28982bb Mon Sep 17 00:00:00 2001 From: dborup Date: Tue, 24 Jun 2025 17:18:04 +0200 Subject: [PATCH 05/12] Add TracerouteIntent for iOS Shortcuts integration This commit adds a new AppIntent called TracerouteIntent, allowing users to send a traceroute request to a specific Meshtastic node directly via iOS Shortcuts or Siri. The intent calls BLEManager.shared.sendTraceRouteRequest(destNum:wantResponse:) and provides basic validation to ensure the device is connected. No other files or logic were modified. This follows the same structural pattern as MessageNodeIntent.swift. --- Meshtastic/AppIntents/TracerouteIntent.swift | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Meshtastic/AppIntents/TracerouteIntent.swift diff --git a/Meshtastic/AppIntents/TracerouteIntent.swift b/Meshtastic/AppIntents/TracerouteIntent.swift new file mode 100644 index 00000000..99c19348 --- /dev/null +++ b/Meshtastic/AppIntents/TracerouteIntent.swift @@ -0,0 +1,27 @@ +import Foundation +import AppIntents + +struct TracerouteIntent: AppIntent { + static var title: LocalizedStringResource = "Send a Traceroute" + + static var description: IntentDescription = "Send a traceroute request to a certain Meshtastic node" + + @Parameter(title: "Node Number") + var nodeNumber: Int + + static var parameterSummary: some ParameterSummary { + Summary("Send traceroute to \(\.$nodeNumber)") + } + + func perform() async throws -> some IntentResult { + if !BLEManager.shared.isConnected { + throw AppIntentErrors.AppIntentError.notConnected + } + + if !BLEManager.shared.sendTraceRouteRequest(destNum: Int64(nodeNumber), wantResponse: true) { + throw AppIntentErrors.AppIntentError.message("Failed to send traceroute request") + } + + return .result() + } +} From d78ab886009ac4912cbfbf0183a04264335c7758 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 26 Jun 2025 07:16:06 -0700 Subject: [PATCH 06/12] Bump version --- Localizable.xcstrings | 2 +- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ba0d7976..3be1addc 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15045,7 +15045,7 @@ } } }, - "incomplete" : { + "Incomplete" : { "localizations" : { "de" : { "stringUnit" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 55581bc9..d18f130e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1828,7 +1828,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1861,7 +1861,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1892,7 +1892,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1924,7 +1924,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.9; + MARKETING_VERSION = 2.6.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index eb4c37b0..3c20a9e2 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -79,7 +79,7 @@ struct NodeInfoItem: View { if user.hwModel != "UNSET" { Text(String(node.user?.hwDisplayName ?? (node.user?.hwModel ?? "Unset".localized))) } else { - Text(String("incomplete".localized)) + Text(String("Incomplete".localized)) } } .accessibilityElement(children: .combine) From e83347d1d89883638b0253309aa1fa7898af6aa5 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 26 Jun 2025 16:59:29 -0500 Subject: [PATCH 07/12] Fix uint32 overflows and add safeint32 methods for re-use --- Meshtastic/Persistence/UpdateCoreData.swift | 26 ++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 6d085b21..a42b2937 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -8,6 +8,19 @@ import CoreData import MeshtasticProtobufs import OSLog +// MARK: - Safe Conversion Helpers +private func safeInt32(from value: UInt32) -> Int32 { + return Int32(clamping: value) +} + +private func safeInt32(from value: Int) -> Int32 { + return Int32(clamping: value) +} + +private func safeInt32(from value: UInt64) -> Int32 { + return Int32(clamping: value) +} + public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) -> Bool { var nodeExpireTime: TimeInterval { return TimeInterval(-nodeExpireDays * 86400) @@ -1367,6 +1380,7 @@ 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 @@ -1498,23 +1512,23 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod if !fetchedNode.isEmpty { if fetchedNode[0].telemetryConfig == nil { let newTelemetryConfig = TelemetryConfigEntity(context: context) - newTelemetryConfig.deviceUpdateInterval = Int32(config.deviceUpdateInterval) - newTelemetryConfig.environmentUpdateInterval = Int32(config.environmentUpdateInterval) + newTelemetryConfig.deviceUpdateInterval = safeInt32(from: config.deviceUpdateInterval) + newTelemetryConfig.environmentUpdateInterval = safeInt32(from: config.environmentUpdateInterval) newTelemetryConfig.environmentMeasurementEnabled = config.environmentMeasurementEnabled newTelemetryConfig.environmentScreenEnabled = config.environmentScreenEnabled newTelemetryConfig.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit newTelemetryConfig.powerMeasurementEnabled = config.powerMeasurementEnabled - newTelemetryConfig.powerUpdateInterval = Int32(config.powerUpdateInterval) + newTelemetryConfig.powerUpdateInterval = safeInt32(from: config.powerUpdateInterval) newTelemetryConfig.powerScreenEnabled = config.powerScreenEnabled fetchedNode[0].telemetryConfig = newTelemetryConfig } else { - fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(config.deviceUpdateInterval) - fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(config.environmentUpdateInterval) + fetchedNode[0].telemetryConfig?.deviceUpdateInterval = safeInt32(from: config.deviceUpdateInterval) + fetchedNode[0].telemetryConfig?.environmentUpdateInterval = safeInt32(from: config.environmentUpdateInterval) fetchedNode[0].telemetryConfig?.environmentMeasurementEnabled = config.environmentMeasurementEnabled fetchedNode[0].telemetryConfig?.environmentScreenEnabled = config.environmentScreenEnabled fetchedNode[0].telemetryConfig?.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit fetchedNode[0].telemetryConfig?.powerMeasurementEnabled = config.powerMeasurementEnabled - fetchedNode[0].telemetryConfig?.powerUpdateInterval = Int32(config.powerUpdateInterval) + fetchedNode[0].telemetryConfig?.powerUpdateInterval = safeInt32(from: config.powerUpdateInterval) fetchedNode[0].telemetryConfig?.powerScreenEnabled = config.powerScreenEnabled } if sessionPasskey != nil { From c014baf9866fb37488a0ec6fa382c9644b9ce3ee Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 27 Jun 2025 06:26:24 -0500 Subject: [PATCH 08/12] Add sync device svgs job --- .github/workflows/sync_device_svgs.yml | 161 +++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 .github/workflows/sync_device_svgs.yml diff --git a/.github/workflows/sync_device_svgs.yml b/.github/workflows/sync_device_svgs.yml new file mode 100644 index 00000000..e1a1fd94 --- /dev/null +++ b/.github/workflows/sync_device_svgs.yml @@ -0,0 +1,161 @@ +name: Sync Device SVGs + +on: + schedule: + # Run nightly at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: + # Allow manual triggering + +jobs: + sync-device-svgs: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install dependencies + run: | + npm install -g svgo + + - name: Download and process SVGs + run: | + #!/bin/bash + set -e + + # Create temporary directory + mkdir -p temp_svgs + cd temp_svgs + + # Clone web-flasher repo (shallow clone for speed) + git clone --depth 1 https://github.com/meshtastic/web-flasher.git + + # Navigate to SVG directory + cd web-flasher/public/img/devices + + # Create output directory + mkdir -p ../../../../processed_svgs + + # Process each SVG file + for svg_file in *.svg; do + if [ -f "$svg_file" ]; then + # Get filename without extension + filename=$(basename "$svg_file" .svg) + + # Optimize SVG + svgo "$svg_file" --output "../../../../processed_svgs/${filename}.svg" + + echo "Processed: $filename" + fi + done + + cd ../../../../ + ls -la processed_svgs/ + + - name: Update Xcode Assets + run: | + #!/bin/bash + set -e + + ASSETS_DIR="Meshtastic/Assets.xcassets" + + # Ensure assets directory exists + mkdir -p "$ASSETS_DIR" + + # Process each SVG + for svg_file in processed_svgs/*.svg; do + if [ -f "$svg_file" ]; then + # Get filename without extension + filename=$(basename "$svg_file" .svg) + + # Create imageset directory + imageset_dir="${ASSETS_DIR}/${filename}.imageset" + mkdir -p "$imageset_dir" + + # Copy SVG to imageset + cp "$svg_file" "${imageset_dir}/${filename}.svg" + + # Create Contents.json for the imageset + cat > "${imageset_dir}/Contents.json" << EOF + { + "images" : [ + { + "filename" : "${filename}.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } + } + EOF + + echo "Created imageset: ${filename}" + fi + done + + - name: Check for changes + id: check_changes + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.check_changes.outputs.has_changes == 'true' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add Meshtastic/Assets.xcassets/ + git commit -m "🤖 Sync device SVGs from web-flasher repo + + - Updated device images from meshtastic/web-flasher + - Automatically synced on $(date -u) + - Source: https://github.com/meshtastic/web-flasher/tree/main/public/img/devices" + git push + + - name: Create PR (alternative to direct push) + if: steps.check_changes.outputs.has_changes == 'true' && false # Set to true if you prefer PRs + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "🤖 Sync device SVGs from web-flasher repo" + title: "Sync device SVGs from web-flasher" + body: | + This PR automatically syncs device SVG images from the [meshtastic/web-flasher](https://github.com/meshtastic/web-flasher) repository. + + **Changes:** + - Updated device images from web-flasher repo + - Source: https://github.com/meshtastic/web-flasher/tree/main/public/img/devices + - Automatically generated on $(date -u) + + The SVGs have been optimized and converted to Xcode asset format. + branch: sync-device-svgs + delete-branch: true + + - name: Cleanup + if: always() + run: | + rm -rf temp_svgs processed_svgs + + - name: Summary + run: | + if [ "${{ steps.check_changes.outputs.has_changes }}" == "true" ]; then + echo "✅ Device SVGs updated successfully" + else + echo "â„šī¸ No changes detected - SVGs are up to date" + fi \ No newline at end of file From b47a259337334d9f4785831a26fab0771d3b0c55 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 27 Jun 2025 16:03:02 -0700 Subject: [PATCH 09/12] Use 9 meters instead of 15 for position log --- Meshtastic/Persistence/UpdateCoreData.swift | 2 +- protobufs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index a42b2937..bc0093a8 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -467,7 +467,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) { - if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 15.0 { + if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 9.0 { mutablePositions.remove(mostRecent) } } else if mutablePositions.count > 0 { diff --git a/protobufs b/protobufs index 816595c8..27fac391 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 +Subproject commit 27fac39141d99fe727a0a1824c5397409b1aea75 From f14f8c97e2611eb37539d3ce46fb33e0509d75f4 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:52:51 -0700 Subject: [PATCH 10/12] QR code improvements --- Localizable.xcstrings | 5 +- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Helpers/BLEManager.swift | 3 +- Meshtastic/Helpers/ContactURLHandler.swift | 86 ++++++ Meshtastic/MeshtasticApp.swift | 108 ++----- Meshtastic/Views/Messages/MessageText.swift | 58 ++++ .../Views/Settings/SaveChannelQRCode.swift | 292 ++++++++++++++++-- 7 files changed, 453 insertions(+), 103 deletions(-) create mode 100644 Meshtastic/Helpers/ContactURLHandler.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ba0d7976..c8cd9692 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -16445,6 +16445,9 @@ } } } + }, + "LoRa Config Changes:" : { + }, "LoRa config received: %@" : { "localizations" : { @@ -35088,4 +35091,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 55581bc9..3d3b7e7c 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; }; BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; }; BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; }; + BCD7448D2E0F2FAA00F265A2 /* ContactURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */; }; BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */; }; BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */; }; BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; }; @@ -337,6 +338,7 @@ BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = ""; }; BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = ""; }; BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = ""; }; + BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactURLHandler.swift; sourceTree = ""; }; BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectNodeIntent.swift; sourceTree = ""; }; BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = ""; }; @@ -1079,6 +1081,7 @@ DDC2E1A526CEB32B0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( + BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */, DDD43FE12A78C86B0083A3E9 /* Mqtt */, DDAF8C5226EB1DF10058C060 /* BLEManager.swift */, DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, @@ -1490,6 +1493,7 @@ DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */, DDB8F4142A9EE5F000230ECE /* ChannelList.swift in Sources */, DDD43FE32A78C8900083A3E9 /* MqttClientProxyManager.swift in Sources */, + BCD7448D2E0F2FAA00F265A2 /* ContactURLHandler.swift in Sources */, DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */, DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */, DDDB444629F8A96500EE2349 /* Character.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 389e812a..42561a4d 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1755,7 +1755,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } - public func saveChannelSet(base64UrlString: String, addChannels: Bool = false) -> Bool { + public func saveChannelSet(base64UrlString: String, addChannels: Bool = false, okToMQTT: Bool = false) -> Bool { if isConnected { var i: Int32 = 0 @@ -1837,6 +1837,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Save the LoRa Config and the device will reboot var adminPacket = AdminMessage() adminPacket.setConfig.lora = channelSet.loraConfig + adminPacket.setConfig.lora.configOkToMqtt = okToMQTT // Preserve users okToMQTT choice var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedPeripheral.num) meshPacket.from = UInt32(connectedPeripheral.num) diff --git a/Meshtastic/Helpers/ContactURLHandler.swift b/Meshtastic/Helpers/ContactURLHandler.swift new file mode 100644 index 00000000..749c8cbf --- /dev/null +++ b/Meshtastic/Helpers/ContactURLHandler.swift @@ -0,0 +1,86 @@ +// +// URLHandler.swift +// Meshtastic +// +// Created by Benjamin Faershtein on 6/27/25. +// +import SwiftUI +import CoreData +import OSLog +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", + message: "In order to import contacts via a QR code you need firmware version 2.6.9 or greater.", + preferredStyle: .alert + ) + alertController.addAction(UIAlertAction( + title: "Close", + style: .cancel, + handler: nil + )) + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alertController, animated: true) + } + Logger.services.debug("User Alerted that a firmware upgrade is required to import contacts.") + } else { + let components = url.absoluteString.components(separatedBy: "#") + if let contactData = components.last { + let decodedString = contactData.base64urlToBase64() + if let decodedData = Data(base64Encoded: decodedString) { + do { + let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) + let alertController = UIAlertController( + title: "Add Contact", + message: "Would you like to add \(contact.user.longName) as a contact?", + preferredStyle: .alert + ) + alertController.addAction(UIAlertAction( + title: "Yes", + style: .default, + handler: { _ in + let success = bleManager.addContactFromURL(base64UrlString: contactData) + Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") + } + )) + alertController.addAction(UIAlertAction( + title: "No", + style: .cancel, + handler: nil + )) + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alertController, animated: true) + } + Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") + } catch { + Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + let errorAlert = UIAlertController( + title: "Error", + message: "Could not process contact information. Invalid format.", + preferredStyle: .alert + ) + errorAlert.addAction(UIAlertAction(title: "OK", style: .default)) + rootViewController.present(errorAlert, animated: true) + } + } + } + } + } + } +} diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 1512cae2..b87aeee3 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -20,7 +20,6 @@ struct MeshtasticAppleApp: App { @State var incomingUrl: URL? @State var channelSettings: String? @State var addChannels = false - public var minimumContactVersion = "2.6.9" init() { let persistenceController = PersistenceController.shared @@ -44,20 +43,31 @@ struct MeshtasticAppleApp: App { appState: appState, router: appState.router ) - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environmentObject(appState) - .environmentObject(BLEManager.shared) - .sheet(isPresented: $saveChannels) { - SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: BLEManager.shared) - .presentationDetents([.large]) - .presentationDragIndicator(.visible) + .sheet(isPresented: Binding( + get: { + saveChannels && !(channelSettings == nil) + }, + set: { newValue in + saveChannels = newValue + if !newValue { + channelSettings = nil + } + } + )) { + SaveChannelQRCode( + channelSetLink: channelSettings ?? "Empty Channel URL", + addChannels: addChannels, + bleManager: BLEManager.shared + ) + .presentationDetents([.large]) + .presentationDragIndicator(.visible) } .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in Logger.mesh.debug("URL received \(userActivity, privacy: .public)") self.incomingUrl = userActivity.webpageURL self.saveChannels = false if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true { - handleContactUrl(url: self.incomingUrl!) + ContactURLHandler.handleContactUrl(url: self.incomingUrl!, bleManager: BLEManager.shared) } else if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/") == true { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false @@ -74,7 +84,7 @@ struct MeshtasticAppleApp: App { } Logger.services.debug("Add Channel \(self.addChannels, privacy: .public)") } - self.saveChannels = true + self.saveChannels = true Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")") } if self.saveChannels { @@ -85,7 +95,7 @@ struct MeshtasticAppleApp: App { Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)") self.incomingUrl = url if url.absoluteString.lowercased().contains("meshtastic.org/v/#") { - handleContactUrl(url: url) + ContactURLHandler.handleContactUrl(url: url, bleManager: BLEManager.shared) } else if url.absoluteString.lowercased().contains("meshtastic.org/e/") { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false @@ -102,7 +112,7 @@ struct MeshtasticAppleApp: App { } Logger.services.debug("Add Channel \(self.addChannels, privacy: .public)") } - self.saveChannels = true + self.saveChannels = true Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link", privacy: .public)") } else if url.absoluteString.lowercased().contains("meshtastic:///") { appState.router.route(url: url) @@ -141,77 +151,9 @@ struct MeshtasticAppleApp: App { Logger.services.error("🍎 [App] Apple must have changed something") } } + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(appState) + .environmentObject(BLEManager.shared) } - func handleContactUrl(url: URL) { - let supportedVersion = UserDefaults.firmwareVersion == "0.0.0" || self.minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedAscending || minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame - if !supportedVersion { - // Show an alert letting the user know they need to upgrade their firmware to use the contact import. - let alertController = UIAlertController( - title: "Firmware Upgrade Required", - message: "In order to import contacts via a QR code you need firmware version 2.6.9 or greater.", - preferredStyle: .alert - ) - alertController.addAction(UIAlertAction( - title: "Close", - style: .cancel, - handler: nil - )) - // Present the alert - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(alertController, animated: true) - } - Logger.services.debug("User Alerted that a firmware upgrade is required to import contacts.") - } else { - let components = url.absoluteString.components(separatedBy: "#") - // Extract contact information from the URL - if let contactData = components.last { - let decodedString = contactData.base64urlToBase64() - if let decodedData = Data(base64Encoded: decodedString) { - do { - let contact = try MeshtasticProtobufs.SharedContact(serializedBytes: decodedData) - // Show an alert to confirm adding the contact - let alertController = UIAlertController( - title: "Add Contact", - message: "Would you like to add \(contact.user.longName) as a contact?", - preferredStyle: .alert - ) - alertController.addAction(UIAlertAction( - title: "Yes", - style: .default, - handler: { _ in - let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData) - Logger.services.debug("Contact added from URL: \(success ? "success" : "failed")") - } - )) - alertController.addAction(UIAlertAction( - title: "No", - style: .cancel, - handler: nil - )) - // Present the alert - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(alertController, animated: true) - } - Logger.services.debug("Contact data extracted from URL: \(contactData, privacy: .public)") - } catch { - Logger.services.error("Failed to parse contact data: \(error.localizedDescription, privacy: .public)") - // Show error alert to user - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - let errorAlert = UIAlertController( - title: "Error", - message: "Could not process contact information. Invalid format.", - preferredStyle: .alert - ) - errorAlert.addAction(UIAlertAction(title: "OK", style: .default)) - rootViewController.present(errorAlert, animated: true) - } - } - } - } - } - } } diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index c8a994c3..ac033b1f 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -17,6 +17,10 @@ struct MessageText: View { let tapBackDestination: MessageDestination let isCurrentUser: Bool let onReply: () -> Void + // State for handling channel URL sheet + @State private var saveChannels = false + @State private var channelSettings: String? + @State private var addChannels = false @State private var isShowingDeleteConfirmation = false @@ -83,6 +87,60 @@ struct MessageText: View { onReply: onReply ) } + .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 + .sheet(isPresented: Binding( + get: { + saveChannels && !(channelSettings == nil) + }, + set: { newValue in + saveChannels = newValue + if !newValue { + channelSettings = nil + } + } + )) { + SaveChannelQRCode( + channelSetLink: channelSettings ?? "Empty Channel URL", + addChannels: addChannels, + bleManager: BLEManager.shared + ) + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + } .confirmationDialog( "Are you sure you want to delete this message?", isPresented: $isShowingDeleteConfirmation, diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index 892df6eb..49c78b0c 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -5,16 +5,24 @@ // Copyright(c) Garth Vander Houwen 7/13/22. // import SwiftUI +import CoreData +import OSLog +import MeshtasticProtobufs struct SaveChannelQRCode: View { - @Environment(\.dismiss) private var dismiss + @Environment(\.managedObjectContext) var context - var channelSetLink: String + let channelSetLink: String var addChannels: Bool = false var bleManager: BLEManager - @State var showError: Bool = false - @State var connectedToDevice = false + + @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 { @@ -26,20 +34,50 @@ struct SaveChannelQRCode: View { .font(.title3) .padding() + if !loraChanges.isEmpty { + VStack(alignment: .leading) { + Text("LoRa Config Changes:") + .font(.headline) + .padding(.bottom, 5) + ForEach(loraChanges, id: \.self) { change in + Text("â€ĸ \(change)") + .font(.callout) + .foregroundColor(.orange) + } + } + .padding() + } + if showError { - Text("Channels being added from the QR code did not save. When adding channels the names must be unique.") + 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) .foregroundColor(.red) .font(.callout) .padding() } + HStack { if !showError { Button { - let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannels: addChannels) + // Extract channel data if it's a full URL + let channelData: String + if channelSetLink.hasPrefix("http") || channelSetLink.hasPrefix("meshtastic://") { + guard let extractedData = extractChannelDataFromURL(channelSetLink) else { + Logger.data.error("Failed to extract channel data from URL during save: \(channelSetLink)") + errorMessage = "Invalid channel URL format" + showError = true + return + } + channelData = extractedData + } else { + channelData = channelSetLink + } + + let success = bleManager.saveChannelSet(base64UrlString: channelData, addChannels: addChannels, okToMQTT: okToMQTT) if success { dismiss() } else { + errorMessage = "Failed to save channel configuration" showError = true } } label: { @@ -50,24 +88,23 @@ struct SaveChannelQRCode: View { .controlSize(.large) .padding() .disabled(!connectedToDevice) -#if targetEnvironment(macCatalyst) - Button { - dismiss() - } label: { - Label("Cancel", systemImage: "xmark") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() -#endif + #if targetEnvironment(macCatalyst) + Button { + dismiss() + } label: { + Label("Cancel", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + #endif } else { Button { dismiss() } label: { Label("Cancel", systemImage: "xmark") - } .buttonStyle(.bordered) .buttonBorderShape(.capsule) @@ -77,7 +114,226 @@ struct SaveChannelQRCode: View { } } .onAppear { + Logger.data.info("Ch set link \(channelSetLink)") connectedToDevice = bleManager.connectToPreferredPeripheral() + 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 { + Logger.data.info("Extracted fragment from URL: \(fragment)") + return fragment + } + } + + // Fallback: manually extract everything after the last # + if let hashIndex = urlString.lastIndex(of: "#") { + let startIndex = urlString.index(after: hashIndex) + let channelData = String(urlString[startIndex...]) + if !channelData.isEmpty { + Logger.data.info("Extracted channel data manually: \(channelData)") + return channelData + } + } + + Logger.data.error("Failed to extract channel data from URL: \(urlString)") + return nil + } + + private func fetchLoRaConfigChanges() { + var currentLoRaConfig: Config.LoRaConfig? + + // First, extract the actual channel data from the URL if it's a full URL + let channelData: String + if channelSetLink.hasPrefix("http") || channelSetLink.hasPrefix("meshtastic://") { + guard let extractedData = extractChannelDataFromURL(channelSetLink) else { + Logger.data.error("Failed to extract channel data from URL: \(channelSetLink)") + errorMessage = "Invalid channel URL format" + showError = true + return + } + channelData = extractedData + } else { + // 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 { + currentLoRaConfig = node.loRaConfig?.toProto() + } + } 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 { + Logger.data.error("Invalid base64 for ChannelSet data: \(channelData, privacy: .public)") + errorMessage = "Invalid channel data format" + showError = true + return + } + + do { + let channelSet = try ChannelSet(serializedBytes: decodedData) + let newLoRaConfig = channelSet.loraConfig + var changes: [String] = [] + + // Preserve user's current okToMQTT setting + okToMQTT = currentLoRaConfig?.configOkToMqtt ?? false + + if let current = currentLoRaConfig { + // Compare each field and track changes + if current.hopLimit != newLoRaConfig.hopLimit { + changes.append("Hop Limit: \(current.hopLimit) -> \(newLoRaConfig.hopLimit)") + } + if current.region != newLoRaConfig.region { + let currentRegionDesc = RegionCodes(rawValue: Int(current.region.rawValue))?.description ?? "Unknown" + let newRegionDesc = RegionCodes(rawValue: Int(newLoRaConfig.region.rawValue))?.description ?? "Unknown" + changes.append("Region: \(currentRegionDesc) -> \(newRegionDesc)") + } + if current.modemPreset != newLoRaConfig.modemPreset { + let currentPresetDesc = ModemPresets(rawValue: Int(current.modemPreset.rawValue))?.description ?? "Unknown" + let newPresetDesc = ModemPresets(rawValue: Int(newLoRaConfig.modemPreset.rawValue))?.description ?? "Unknown" + changes.append("Modem Preset: \(currentPresetDesc) -> \(newPresetDesc)") + } + if current.usePreset != newLoRaConfig.usePreset { + changes.append("Use Preset: \(current.usePreset) -> \(newLoRaConfig.usePreset)") + } + if current.txEnabled != newLoRaConfig.txEnabled { + changes.append("Transmit Enabled: \(current.txEnabled) -> \(newLoRaConfig.txEnabled)") + } + if current.txPower != newLoRaConfig.txPower { + changes.append("Transmit Power: \(current.txPower)dBm -> \(newLoRaConfig.txPower)dBm") + } + if current.channelNum != newLoRaConfig.channelNum { + changes.append("Channel Number: \(current.channelNum) -> \(newLoRaConfig.channelNum)") + } + if current.bandwidth != newLoRaConfig.bandwidth { + changes.append("Bandwidth: \(current.bandwidth) -> \(newLoRaConfig.bandwidth)") + } + if current.codingRate != newLoRaConfig.codingRate { + changes.append("Coding Rate: \(current.codingRate) -> \(newLoRaConfig.codingRate)") + } + if current.spreadFactor != newLoRaConfig.spreadFactor { + changes.append("Spread Factor: \(current.spreadFactor) -> \(newLoRaConfig.spreadFactor)") + } + if current.sx126XRxBoostedGain != newLoRaConfig.sx126XRxBoostedGain { + changes.append("RX Boosted Gain: \(current.sx126XRxBoostedGain) -> \(newLoRaConfig.sx126XRxBoostedGain)") + } + if current.overrideFrequency != newLoRaConfig.overrideFrequency { + changes.append("Override Frequency: \(current.overrideFrequency) -> \(newLoRaConfig.overrideFrequency)") + } + if current.ignoreMqtt != newLoRaConfig.ignoreMqtt { + changes.append("Ignore MQTT: \(current.ignoreMqtt) -> \(newLoRaConfig.ignoreMqtt)") + } + } 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)") + } + if newLoRaConfig.region != defaultConfig.region { + let newRegionDesc = RegionCodes(rawValue: Int(newLoRaConfig.region.rawValue))?.description ?? "Unknown" + changes.append("Region: Unset -> \(newRegionDesc)") + } + if newLoRaConfig.modemPreset != defaultConfig.modemPreset { + let newPresetDesc = ModemPresets(rawValue: Int(newLoRaConfig.modemPreset.rawValue))?.description ?? "Unknown" + changes.append("Modem Preset: Long Fast -> \(newPresetDesc)") + } + if newLoRaConfig.usePreset != defaultConfig.usePreset { + changes.append("Use Preset: \(defaultConfig.usePreset) -> \(newLoRaConfig.usePreset)") + } + if newLoRaConfig.txEnabled != defaultConfig.txEnabled { + changes.append("Transmit Enabled: \(defaultConfig.txEnabled) -> \(newLoRaConfig.txEnabled)") + } + if newLoRaConfig.txPower != defaultConfig.txPower { + changes.append("Transmit Power: \(defaultConfig.txPower)dBm -> \(newLoRaConfig.txPower)dBm") + } + if newLoRaConfig.channelNum != defaultConfig.channelNum { + changes.append("Channel Number: \(defaultConfig.channelNum) -> \(newLoRaConfig.channelNum)") + } + if newLoRaConfig.bandwidth != defaultConfig.bandwidth { + changes.append("Bandwidth: \(defaultConfig.bandwidth) -> \(newLoRaConfig.bandwidth)") + } + if newLoRaConfig.codingRate != defaultConfig.codingRate { + changes.append("Coding Rate: \(defaultConfig.codingRate) -> \(newLoRaConfig.codingRate)") + } + if newLoRaConfig.spreadFactor != defaultConfig.spreadFactor { + changes.append("Spread Factor: \(defaultConfig.spreadFactor) -> \(newLoRaConfig.spreadFactor)") + } + if newLoRaConfig.sx126XRxBoostedGain != defaultConfig.sx126XRxBoostedGain { + changes.append("RX Boosted Gain: \(defaultConfig.sx126XRxBoostedGain) -> \(newLoRaConfig.sx126XRxBoostedGain)") + } + if newLoRaConfig.overrideFrequency != defaultConfig.overrideFrequency { + changes.append("Override Frequency: \(defaultConfig.overrideFrequency) -> \(newLoRaConfig.overrideFrequency)") + } + if newLoRaConfig.ignoreMqtt != defaultConfig.ignoreMqtt { + 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 + config.region = .unset + config.modemPreset = .longFast + config.usePreset = true + config.txEnabled = true + config.txPower = 0 + config.channelNum = 0 + config.bandwidth = 0 + config.codingRate = 0 + config.spreadFactor = 0 + config.sx126XRxBoostedGain = false + config.overrideFrequency = 0.0 + config.ignoreMqtt = false + config.configOkToMqtt = false + return config + } +} + +extension LoRaConfigEntity { + func toProto() -> Config.LoRaConfig { + var config = Config.LoRaConfig() + config.hopLimit = UInt32(self.hopLimit) + config.region = Config.LoRaConfig.RegionCode(rawValue: Int(self.regionCode)) ?? .unset + config.modemPreset = Config.LoRaConfig.ModemPreset(rawValue: Int(self.modemPreset)) ?? .longFast + config.usePreset = self.usePreset + config.txEnabled = self.txEnabled + config.txPower = Int32(self.txPower) + config.channelNum = UInt32(self.channelNum) + config.bandwidth = UInt32(self.bandwidth) + config.codingRate = UInt32(self.codingRate) + config.spreadFactor = UInt32(self.spreadFactor) + config.sx126XRxBoostedGain = self.sx126xRxBoostedGain + config.overrideFrequency = self.overrideFrequency + config.ignoreMqtt = self.ignoreMqtt + config.configOkToMqtt = self.okToMqtt + return config + } } From d7ce318b4d974c2fb9b27c0888d993ff39daebaf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 28 Jun 2025 10:11:31 -0700 Subject: [PATCH 11/12] on change for node change --- Localizable.xcstrings | 5 ++++- Meshtastic/Views/Settings/Config/SecurityConfig.swift | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 0a05a052..cc76d94e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1328,6 +1328,9 @@ } } } + }, + "â€ĸ %@" : { + }, "< 1%" : { "localizations" : { @@ -35105,4 +35108,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 1897bf43..0153bb49 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -230,6 +230,9 @@ struct SecurityConfig: View { name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" ) }) + .onChange(of: node) { _, newNode in + setSecurityValues() + } .onChange(of: isManaged) { _, newIsManaged in if newIsManaged != node?.securityConfig?.isManaged { hasChanges = true } } From 202c558c661dc74027203336d0d43e48543ba64c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 28 Jun 2025 10:13:24 -0700 Subject: [PATCH 12/12] Dont translate string replacement --- Localizable.xcstrings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index cc76d94e..219f123e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1330,7 +1330,7 @@ } }, "â€ĸ %@" : { - + "shouldTranslate" : false }, "< 1%" : { "localizations" : {