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 diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ba0d7976..219f123e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1329,6 +1329,9 @@ } } }, + "â€ĸ %@" : { + "shouldTranslate" : false + }, "< 1%" : { "localizations" : { "it" : { @@ -13647,6 +13650,9 @@ } } } + }, + "Hard Reset" : { + }, "Hardware" : { "localizations" : { @@ -14986,6 +14992,9 @@ } } } + }, + "In addition to Config, Keys and BLE bonds will be wiped" : { + }, "Include" : { "localizations" : { @@ -15045,7 +15054,7 @@ } } }, - "incomplete" : { + "Incomplete" : { "localizations" : { "de" : { "stringUnit" : { @@ -16445,6 +16454,9 @@ } } } + }, + "LoRa Config Changes:" : { + }, "LoRa config received: %@" : { "localizations" : { @@ -22981,6 +22993,9 @@ } } } + }, + "Provide Confirmation" : { + }, "Public Key" : { "localizations" : { @@ -26383,6 +26398,7 @@ } }, "Send a Direct Message" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -26471,6 +26487,7 @@ } }, "Send a message to a certain meshtastic node" : { + "extractionState" : "stale", "localizations" : { "it" : { "stringUnit" : { @@ -28074,6 +28091,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 55581bc9..84c37c28 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -59,12 +59,12 @@ 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 */; }; 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 */; }; @@ -330,13 +330,13 @@ 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 = ""; }; 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 = ""; }; @@ -704,7 +704,6 @@ BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */, BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */, BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */, - BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */, BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */, ); path = AppIntents; @@ -1079,6 +1078,7 @@ DDC2E1A526CEB32B0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( + BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */, DDD43FE12A78C86B0083A3E9 /* Mqtt */, DDAF8C5226EB1DF10058C060 /* BLEManager.swift */, DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, @@ -1394,7 +1394,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 */, @@ -1490,6 +1489,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 */, @@ -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/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 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() + } +} 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/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 6d085b21..bc0093a8 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) @@ -454,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 { @@ -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 { 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/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) 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 } } 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 + } } diff --git a/protobufs b/protobufs index 816595c8..27fac391 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 +Subproject commit 27fac39141d99fe727a0a1824c5397409b1aea75