mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
commit
9fd6f72b0f
15 changed files with 699 additions and 124 deletions
161
.github/workflows/sync_device_svgs.yml
vendored
Normal file
161
.github/workflows/sync_device_svgs.yml
vendored
Normal file
|
|
@ -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
|
||||
|
|
@ -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" : {
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = "<group>"; };
|
||||
BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.swift; sourceTree = "<group>"; };
|
||||
BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = "<group>"; };
|
||||
BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = "<group>"; };
|
||||
BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelSettingsIntent.swift; sourceTree = "<group>"; };
|
||||
BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = "<group>"; };
|
||||
BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = "<group>"; };
|
||||
BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = "<group>"; };
|
||||
BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = "<group>"; };
|
||||
BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactURLHandler.swift; sourceTree = "<group>"; };
|
||||
BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectNodeIntent.swift; sourceTree = "<group>"; };
|
||||
BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
|
||||
BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -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 = "";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
27
Meshtastic/AppIntents/TracerouteIntent.swift
Normal file
27
Meshtastic/AppIntents/TracerouteIntent.swift
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
86
Meshtastic/Helpers/ContactURLHandler.swift
Normal file
86
Meshtastic/Helpers/ContactURLHandler.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705
|
||||
Subproject commit 27fac39141d99fe727a0a1824c5397409b1aea75
|
||||
Loading…
Add table
Add a link
Reference in a new issue