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 1/9] 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 aa73a82404da25f88c9099bf55e100ebd28982bb Mon Sep 17 00:00:00 2001 From: dborup Date: Tue, 24 Jun 2025 17:18:04 +0200 Subject: [PATCH 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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" : {