mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge remote-tracking branch 'refs/remotes/origin/main'
This commit is contained in:
commit
a8a00a9605
19 changed files with 843 additions and 159 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" : {
|
||||
|
|
@ -1693,7 +1696,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"A channel index of 0 indicates the primary channel where all broadcast packets are sent from." : {
|
||||
"A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward." : {
|
||||
|
||||
},
|
||||
"A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : {
|
||||
|
|
@ -1757,7 +1760,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted." : {
|
||||
"A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key." : {
|
||||
|
||||
},
|
||||
"A red open lock with a warning means the channel is not securely encrypted and is used for precise location data which is being uplinked to the internet via MQTT, it uses either no key at all or a 1 byte known key." : {
|
||||
|
||||
},
|
||||
"A Trace Route was sent, no response has been received." : {
|
||||
|
|
@ -1781,6 +1787,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key." : {
|
||||
|
||||
},
|
||||
"About" : {
|
||||
"localizations" : {
|
||||
|
|
@ -13641,6 +13650,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hard Reset" : {
|
||||
|
||||
},
|
||||
"Hardware" : {
|
||||
"localizations" : {
|
||||
|
|
@ -14980,6 +14992,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"In addition to Config, Keys and BLE bonds will be wiped" : {
|
||||
|
||||
},
|
||||
"Include" : {
|
||||
"localizations" : {
|
||||
|
|
@ -15039,7 +15054,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"incomplete" : {
|
||||
"Incomplete" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
|
|
@ -16439,6 +16454,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoRa Config Changes:" : {
|
||||
|
||||
},
|
||||
"LoRa config received: %@" : {
|
||||
"localizations" : {
|
||||
|
|
@ -22975,6 +22993,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Provide Confirmation" : {
|
||||
|
||||
},
|
||||
"Public Key" : {
|
||||
"localizations" : {
|
||||
|
|
@ -26377,6 +26398,7 @@
|
|||
}
|
||||
},
|
||||
"Send a Direct Message" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
|
|
@ -26465,6 +26487,7 @@
|
|||
}
|
||||
},
|
||||
"Send a message to a certain meshtastic node" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
|
|
@ -28068,6 +28091,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Show a confirmation dialog before performing the factory reset" : {
|
||||
|
||||
},
|
||||
"Show alerts" : {
|
||||
"localizations" : {
|
||||
|
|
@ -28238,6 +28264,7 @@
|
|||
}
|
||||
},
|
||||
"Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity." : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
|
|
@ -28252,6 +28279,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to start the live activity." : {
|
||||
|
||||
},
|
||||
"Shut Down" : {
|
||||
"localizations" : {
|
||||
|
|
|
|||
|
|
@ -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 */; };
|
||||
|
|
@ -101,6 +101,7 @@
|
|||
DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */; };
|
||||
DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */; };
|
||||
DD1BEF502E0528AA0090CE24 /* PersistantTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */; };
|
||||
DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */; };
|
||||
DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; };
|
||||
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; };
|
||||
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; };
|
||||
|
|
@ -329,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>"; };
|
||||
|
|
@ -380,6 +381,7 @@
|
|||
DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = "<group>"; };
|
||||
DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsHelp.swift; sourceTree = "<group>"; };
|
||||
DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistantTips.swift; sourceTree = "<group>"; };
|
||||
DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelLock.swift; sourceTree = "<group>"; };
|
||||
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = "<group>"; };
|
||||
DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = "<group>"; };
|
||||
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -702,7 +704,6 @@
|
|||
BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */,
|
||||
BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */,
|
||||
BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */,
|
||||
BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */,
|
||||
BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */,
|
||||
);
|
||||
path = AppIntents;
|
||||
|
|
@ -1057,6 +1058,7 @@
|
|||
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */,
|
||||
DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */,
|
||||
DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */,
|
||||
DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */,
|
||||
DD47E3D526F17ED900029299 /* CircleText.swift */,
|
||||
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */,
|
||||
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */,
|
||||
|
|
@ -1076,6 +1078,7 @@
|
|||
DDC2E1A526CEB32B0042C5E4 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */,
|
||||
DDD43FE12A78C86B0083A3E9 /* Mqtt */,
|
||||
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */,
|
||||
DD1BEF492E0292220090CE24 /* KeychainHelper.swift */,
|
||||
|
|
@ -1384,13 +1387,13 @@
|
|||
25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */,
|
||||
259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */,
|
||||
BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */,
|
||||
DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */,
|
||||
259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */,
|
||||
259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */,
|
||||
DDDB444829F8A9C900EE2349 /* String.swift in Sources */,
|
||||
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 */,
|
||||
|
|
@ -1486,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 */,
|
||||
|
|
@ -1824,7 +1828,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.8;
|
||||
MARKETING_VERSION = 2.6.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1857,7 +1861,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.8;
|
||||
MARKETING_VERSION = 2.6.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1888,7 +1892,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.8;
|
||||
MARKETING_VERSION = 2.6.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1920,7 +1924,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.8;
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -725,7 +725,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let message = CocoaMQTTMessage(topic: decodedInfo.mqttClientProxyMessage.topic, payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), retained: decodedInfo.mqttClientProxyMessage.retained)
|
||||
mqttManager.mqttClientProxy?.publish(message)
|
||||
} else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) {
|
||||
|
||||
var path = "meshtastic:///settings/debugLogs"
|
||||
if decodedInfo.clientNotification.hasReplyID {
|
||||
/// Set Sent bool on TraceRouteEntity to false if we got rate limited
|
||||
|
|
@ -740,8 +739,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let nsError = error as NSError
|
||||
Logger.data.error("💥 [TraceRouteEntity] Error Updating Core Data: \(nsError, privacy: .public)")
|
||||
}
|
||||
} else if decodedInfo.clientNotification.message.starts(with: "You Device is configured with a low entropy") || decodedInfo.clientNotification.message.starts(with: "Compromised keys detected")
|
||||
|| decodedInfo.clientNotification.message.starts(with: "Remote device"){
|
||||
}
|
||||
if decodedInfo.clientNotification.payloadVariant == ClientNotification.OneOf_PayloadVariant.lowEntropyKey(decodedInfo.clientNotification.lowEntropyKey) ||
|
||||
decodedInfo.clientNotification.payloadVariant == ClientNotification.OneOf_PayloadVariant.duplicatedPublicKey(decodedInfo.clientNotification.duplicatedPublicKey) {
|
||||
path = "meshtastic:///settings/security"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
35
Meshtastic/Views/Helpers/ChannelLock.swift
Normal file
35
Meshtastic/Views/Helpers/ChannelLock.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// ChannelLock.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 6/22/25.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
struct ChannelLock: View {
|
||||
|
||||
@ObservedObject var channel: ChannelEntity
|
||||
|
||||
var body: some View {
|
||||
/// Unencrypted - using no key at all or a known 1 byte key
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
let preciseLoction = 17...32
|
||||
// Using precise location and have MQTT uplink enabled
|
||||
if channel.uplinkEnabled && preciseLoction ~= (Int(channel.positionPrecision)) {
|
||||
Image(systemName: "lock.open.trianglebadge.exclamationmark.fill")
|
||||
.foregroundColor(.red)
|
||||
// Using precise location
|
||||
} else if preciseLoction ~= (Int(channel.positionPrecision)) {
|
||||
Image(systemName: "lock.open.fill")
|
||||
.foregroundColor(.red)
|
||||
// Just unencrypted without any location or MQTT
|
||||
} else {
|
||||
Image(systemName: "lock.open.fill")
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,25 +21,46 @@ struct ChannelsHelp: View {
|
|||
CircleText(text: String(0), color: .accentColor)
|
||||
.brightness(0.2)
|
||||
.offset(y: -10)
|
||||
Text("A channel index of 0 indicates the primary channel where all broadcast packets are sent from.")
|
||||
Text("A channel index of 0 indicates the primary channel where broadcast packets are sent from. Location data is broadcast from the first channel where it is enabled with firmware 2.7 forward.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.bottom)
|
||||
.padding(.leading, 7)
|
||||
}
|
||||
HStack {
|
||||
Image(systemName: "lock.fill")
|
||||
.padding(.bottom)
|
||||
.padding(.leading)
|
||||
.padding(.trailing, 7)
|
||||
.foregroundColor(Color.green)
|
||||
.font(.largeTitle)
|
||||
.font(.title)
|
||||
Text("A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.bottom)
|
||||
}
|
||||
HStack {
|
||||
Image(systemName: "lock.slash.fill")
|
||||
Image(systemName: "lock.open.fill")
|
||||
.padding(.leading)
|
||||
.foregroundColor(Color.yellow)
|
||||
.font(.title)
|
||||
Text("A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.bottom)
|
||||
}
|
||||
HStack {
|
||||
Image(systemName: "lock.open.fill")
|
||||
.padding(.leading)
|
||||
.foregroundColor(Color.red)
|
||||
.font(.largeTitle)
|
||||
Text("A red lock with a slash means the channel is not securely encrypted, it uses either no key at all or a 1 byte known key. Traffic on this channel is easily intercepted.")
|
||||
.font(.title)
|
||||
Text("A red open lock means the channel is not securely encrypted and is used for precise location data, it uses either no key at all or a 1 byte known key.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.bottom)
|
||||
}
|
||||
HStack {
|
||||
Image(systemName: "lock.open.trianglebadge.exclamationmark.fill")
|
||||
.padding(.leading)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.foregroundColor(Color.red)
|
||||
.font(.title)
|
||||
Text("A red open lock with a warning means the channel is not securely encrypted and is used for precise location data which is being uplinked to the internet via MQTT, it uses either no key at all or a 1 byte known key.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,13 +58,7 @@ struct ChannelList: View {
|
|||
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash.fill")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
ChannelLock(channel: channel)
|
||||
if channel.name?.isEmpty ?? false {
|
||||
if channel.role == 1 {
|
||||
Text(String("PrimaryChannel").camelCaseToWords())
|
||||
|
|
@ -173,7 +167,7 @@ struct ChannelList: View {
|
|||
}
|
||||
.sheet(isPresented: $showingHelp) {
|
||||
ChannelsHelp()
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .leading) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ struct Channels: View {
|
|||
|
||||
/// Minimum Version for granular position configuration
|
||||
@State var minimumVersion = "2.2.24"
|
||||
@State private var showingHelp = false
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
|
||||
|
|
@ -124,13 +125,7 @@ struct Channels: View {
|
|||
.brightness(0.1)
|
||||
VStack {
|
||||
HStack {
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash.fill")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
ChannelLock(channel: channel)
|
||||
if channel.name?.isEmpty ?? false {
|
||||
if channel.role == 1 {
|
||||
Text(String("PrimaryChannel").camelCaseToWords()).font(.headline)
|
||||
|
|
@ -246,6 +241,7 @@ struct Channels: View {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil {
|
||||
|
||||
Button {
|
||||
|
|
@ -286,6 +282,29 @@ struct Channels: View {
|
|||
.padding()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingHelp) {
|
||||
ChannelsHelp()
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .leading) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
showingHelp = !showingHelp
|
||||
}
|
||||
}) {
|
||||
Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.navigationTitle("Channels")
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -349,6 +352,14 @@ struct SecurityConfig: View {
|
|||
}
|
||||
}
|
||||
hasChanges = false
|
||||
if keyUpdated {
|
||||
if !bleManager.sendReboot(
|
||||
fromUser: fromUser,
|
||||
toUser: toUser
|
||||
) {
|
||||
Logger.mesh.warning("Reboot Failed")
|
||||
}
|
||||
}
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ struct ShareChannels: View {
|
|||
var node: NodeInfoEntity?
|
||||
@State private var channelsUrl = "https://www.meshtastic.org/e/#"
|
||||
var qrCodeImage = QrCodeImage()
|
||||
@State private var showingHelp = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
|
@ -82,13 +83,7 @@ struct ShareChannels: View {
|
|||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords())
|
||||
if channel.psk?.hexDescription.count ?? 0 < 3 {
|
||||
Image(systemName: "lock.slash.fill")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
ChannelLock(channel: channel)
|
||||
} else if channel.index == 1 && channel.role > 0 {
|
||||
Toggle("Channel 1 Included", isOn: $includeChannel1)
|
||||
.toggleStyle(.switch)
|
||||
|
|
@ -216,16 +211,39 @@ struct ShareChannels: View {
|
|||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(
|
||||
minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6),
|
||||
maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6),
|
||||
minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6),
|
||||
maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.8 : 0.6),
|
||||
minWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6),
|
||||
maxWidth: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6),
|
||||
minHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6),
|
||||
maxHeight: smallest * (UIDevice.current.userInterfaceIdiom == .phone ? 0.75 : 0.6),
|
||||
alignment: .top
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingHelp) {
|
||||
ChannelsHelp()
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .leading) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
showingHelp = !showingHelp
|
||||
}
|
||||
}) {
|
||||
Image(systemName: !showingHelp ? "questionmark.circle" : "questionmark.circle.fill")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.navigationTitle("Generate QR Code")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue