mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge branch 'main'
#Conflicts: # Meshtastic/AppIntents/RestartNodeIntent.swift # Meshtastic/Extensions/UserDefaults.swift # Meshtastic/MeshtasticApp.swift
This commit is contained in:
commit
536975eff5
135 changed files with 14637 additions and 3569 deletions
21
.github/workflows/macos-dSYM.yml
vendored
Normal file
21
.github/workflows/macos-dSYM.yml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
name: Upload dSYM Files
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Generate/Download dSYM Files
|
||||
uses: ./release.sh
|
||||
|
||||
- name: Upload dSYMs to Datadog
|
||||
uses: DataDog/upload-dsyms-github-action@v1
|
||||
with:
|
||||
api_key: ${{ secrets.DATADOG_API_KEY }}
|
||||
site: datadoghq.com
|
||||
dsym_paths: |
|
||||
path/to/dsyms/folder
|
||||
path/to/zip/dsyms.zip
|
||||
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
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,12 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
102B5EAB2E172F41003D191E /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAA2E172F41003D191E /* DatadogCore */; };
|
||||
102B5EAD2E172F41003D191E /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAC2E172F41003D191E /* DatadogCrashReporting */; };
|
||||
102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAE2E172F41003D191E /* DatadogLogs */; };
|
||||
102B5EB12E172F41003D191E /* DatadogRUM in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EB02E172F41003D191E /* DatadogRUM */; };
|
||||
108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */; };
|
||||
108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */; };
|
||||
231B3F212D087A4C0069A07D /* MetricTableColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */; };
|
||||
231B3F222D087A4C0069A07D /* MetricsColumnList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */; };
|
||||
231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */; };
|
||||
|
|
@ -28,6 +34,7 @@
|
|||
2373AE132D0A216C0086C749 /* MetricsChartSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */; };
|
||||
2373AE152D0A24930086C749 /* MetricsSeriesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE142D0A24930086C749 /* MetricsSeriesList.swift */; };
|
||||
2373AE172D0A26620086C749 /* EnvironmentDefaultSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */; };
|
||||
237B46962DC8F1C100B22D99 /* RateLimitedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */; };
|
||||
251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */; };
|
||||
251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */; };
|
||||
2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */; };
|
||||
|
|
@ -55,12 +62,14 @@
|
|||
8D3F8A412D44C2A6009EAAA4 /* PowerMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */; };
|
||||
B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; };
|
||||
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; };
|
||||
BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */; };
|
||||
BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC10380E2DD4333C00B00BFA /* AddContactIntent.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 */; };
|
||||
BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; };
|
||||
|
|
@ -92,6 +101,11 @@
|
|||
DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0EA2C601795008C0C70 /* CLLocation.swift */; };
|
||||
DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */; };
|
||||
DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */; };
|
||||
DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF492E0292220090CE24 /* KeychainHelper.swift */; };
|
||||
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 */; };
|
||||
|
|
@ -274,6 +288,8 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = "<group>"; };
|
||||
108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = "<group>"; };
|
||||
231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsColumnList.swift; sourceTree = "<group>"; };
|
||||
231B3F202D087A4C0069A07D /* MetricTableColumn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricTableColumn.swift; sourceTree = "<group>"; };
|
||||
231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultColumns.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -295,6 +311,7 @@
|
|||
2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsChartSeries.swift; sourceTree = "<group>"; };
|
||||
2373AE142D0A24930086C749 /* MetricsSeriesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsSeriesList.swift; sourceTree = "<group>"; };
|
||||
2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultSeries.swift; sourceTree = "<group>"; };
|
||||
237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimitedButton.swift; sourceTree = "<group>"; };
|
||||
251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteNodeButton.swift; sourceTree = "<group>"; };
|
||||
251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAlertsButton.swift; sourceTree = "<group>"; };
|
||||
251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangePositionsButton.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -317,13 +334,15 @@
|
|||
8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = "<group>"; };
|
||||
BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.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>"; };
|
||||
BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -343,6 +362,7 @@
|
|||
DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 52.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 46.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorEnums.swift; sourceTree = "<group>"; };
|
||||
DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 28.xcdatamodel"; sourceTree = "<group>"; };
|
||||
|
|
@ -361,6 +381,12 @@
|
|||
DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatters.swift; sourceTree = "<group>"; };
|
||||
DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = "<group>"; };
|
||||
DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
|
|
@ -410,6 +436,7 @@
|
|||
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = "<group>"; };
|
||||
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = "<group>"; };
|
||||
DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = "<group>"; };
|
||||
DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 51.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DD6D5A322CA1178300ED3032 /* TraceRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRoute.swift; sourceTree = "<group>"; };
|
||||
DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 45.xcdatamodel"; sourceTree = "<group>"; };
|
||||
|
|
@ -571,7 +598,11 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
102B5EAD2E172F41003D191E /* DatadogCrashReporting in Frameworks */,
|
||||
25A978BA2C13F8ED0003AAE7 /* MeshtasticProtobufs in Frameworks */,
|
||||
102B5EAB2E172F41003D191E /* DatadogCore in Frameworks */,
|
||||
102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */,
|
||||
102B5EB12E172F41003D191E /* DatadogRUM in Frameworks */,
|
||||
DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
@ -672,6 +703,7 @@
|
|||
BCB6137F2C6728E700485544 /* AppIntents */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */,
|
||||
BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */,
|
||||
BCB613802C67290800485544 /* SendWaypointIntent.swift */,
|
||||
BCB613822C672A2600485544 /* MessageChannelIntent.swift */,
|
||||
|
|
@ -682,7 +714,7 @@
|
|||
BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */,
|
||||
BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */,
|
||||
BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */,
|
||||
BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */,
|
||||
BCD93CB92D9E11A2006C9214 /* DisconnectNodeIntent.swift */,
|
||||
);
|
||||
path = AppIntents;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -701,6 +733,7 @@
|
|||
DD007BB12AA59B9A00F5FA12 /* CoreData */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */,
|
||||
2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */,
|
||||
DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */,
|
||||
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */,
|
||||
|
|
@ -754,6 +787,7 @@
|
|||
children = (
|
||||
DDD5BB0E2C285F92007E03CA /* Logs */,
|
||||
DD93800C2BA74CE3008BEC06 /* Channels */,
|
||||
DD61937A2863876A00E59241 /* Config */,
|
||||
DD97E96728EFE9A00056DDA4 /* About.swift */,
|
||||
DDD5BB152C28B1E4007E03CA /* AppData.swift */,
|
||||
DDD5BB082C285DDC007E03CA /* AppLog.swift */,
|
||||
|
|
@ -767,7 +801,6 @@
|
|||
DD3501882852FC3B000FC853 /* Settings.swift */,
|
||||
DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */,
|
||||
DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */,
|
||||
DD61937A2863876A00E59241 /* Config */,
|
||||
DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
|
|
@ -788,6 +821,7 @@
|
|||
DD61937A2863876A00E59241 /* Config */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD61937B2863877A00E59241 /* Module */,
|
||||
D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */,
|
||||
D93069072B81DF040066FBC8 /* SaveConfigButton.swift */,
|
||||
DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */,
|
||||
|
|
@ -798,7 +832,6 @@
|
|||
DD2553582855B52700E55709 /* PositionConfig.swift */,
|
||||
D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */,
|
||||
DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */,
|
||||
DD61937B2863877A00E59241 /* Module */,
|
||||
);
|
||||
path = Config;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -832,6 +865,7 @@
|
|||
DD6F65772C6EAB860053C113 /* Help */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */,
|
||||
DD6F65752C6EA5490053C113 /* AckErrors.swift */,
|
||||
DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */,
|
||||
DD6F657A2C6EC2900053C113 /* LockLegend.swift */,
|
||||
|
|
@ -850,6 +884,7 @@
|
|||
DD7709392AA1ABA1007A8BF0 /* Tips */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD1BEF4F2E0528A80090CE24 /* PersistantTips.swift */,
|
||||
DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */,
|
||||
DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */,
|
||||
DDC1B8192AB5377B00C71E39 /* MessagesTips.swift */,
|
||||
|
|
@ -870,6 +905,7 @@
|
|||
DD8ED9C6289CE4A100B3B0AB /* Enums */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */,
|
||||
DDA951592BC6624100CEA535 /* TelemetryWeather.swift */,
|
||||
DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */,
|
||||
DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */,
|
||||
|
|
@ -1041,6 +1077,7 @@
|
|||
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */,
|
||||
DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */,
|
||||
DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */,
|
||||
DD1BEF512E08E9AE0090CE24 /* ChannelLock.swift */,
|
||||
DD47E3D526F17ED900029299 /* CircleText.swift */,
|
||||
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */,
|
||||
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */,
|
||||
|
|
@ -1052,6 +1089,7 @@
|
|||
DD5E523D298F5A7D00D21B61 /* Weather */,
|
||||
DD6F65712C6AB8EC0053C113 /* SecureInput.swift */,
|
||||
8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */,
|
||||
237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1059,8 +1097,10 @@
|
|||
DDC2E1A526CEB32B0042C5E4 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */,
|
||||
DDD43FE12A78C86B0083A3E9 /* Mqtt */,
|
||||
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */,
|
||||
DD1BEF492E0292220090CE24 /* KeychainHelper.swift */,
|
||||
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */,
|
||||
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */,
|
||||
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */,
|
||||
|
|
@ -1109,6 +1149,7 @@
|
|||
DDDB26402AABEF7B003AFCB7 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */,
|
||||
231B3F232D087C020069A07D /* Metrics Columns */,
|
||||
DDAD49EB2AFAE82500B4425D /* Map */,
|
||||
DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */,
|
||||
|
|
@ -1214,6 +1255,10 @@
|
|||
packageProductDependencies = (
|
||||
DD0D3D212A55CEB10066DB71 /* CocoaMQTT */,
|
||||
25A978B92C13F8ED0003AAE7 /* MeshtasticProtobufs */,
|
||||
102B5EAA2E172F41003D191E /* DatadogCore */,
|
||||
102B5EAC2E172F41003D191E /* DatadogCrashReporting */,
|
||||
102B5EAE2E172F41003D191E /* DatadogLogs */,
|
||||
102B5EB02E172F41003D191E /* DatadogRUM */,
|
||||
);
|
||||
productName = MeshtasticClient;
|
||||
productReference = DDC2E15426CE248E0042C5E4 /* Meshtastic.app */;
|
||||
|
|
@ -1277,12 +1322,14 @@
|
|||
se,
|
||||
sr,
|
||||
it,
|
||||
ja,
|
||||
);
|
||||
mainGroup = DDC2E14B26CE248E0042C5E4;
|
||||
packageReferences = (
|
||||
DD0D3D202A55CEB10066DB71 /* XCRemoteSwiftPackageReference "CocoaMQTT" */,
|
||||
25A978B82C13F8ED0003AAE7 /* XCLocalSwiftPackageReference "MeshtasticProtobufs" */,
|
||||
259792242C2F10B600AD1659 /* XCRemoteSwiftPackageReference "swift-protobuf" */,
|
||||
102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */,
|
||||
);
|
||||
productRefGroup = DDC2E15526CE248E0042C5E4 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -1365,13 +1412,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 */,
|
||||
|
|
@ -1404,6 +1451,7 @@
|
|||
DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */,
|
||||
233E99C12D849D6000CC3A77 /* DistanceCompactWidget.swift in Sources */,
|
||||
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */,
|
||||
108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */,
|
||||
DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */,
|
||||
DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */,
|
||||
DDDB445229F8ACF900EE2349 /* Date.swift in Sources */,
|
||||
|
|
@ -1414,15 +1462,19 @@
|
|||
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */,
|
||||
DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */,
|
||||
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
|
||||
DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */,
|
||||
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
|
||||
BCD93CBA2D9E11A2006C9214 /* DisconnectNodeIntent.swift in Sources */,
|
||||
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */,
|
||||
231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */,
|
||||
25F5D5BE2C3F6D87008036E3 /* NavigationState.swift in Sources */,
|
||||
2373AE152D0A24930086C749 /* MetricsSeriesList.swift in Sources */,
|
||||
DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */,
|
||||
DD1BEF502E0528AA0090CE24 /* PersistantTips.swift in Sources */,
|
||||
DDDB445429F8AD1600EE2349 /* Data.swift in Sources */,
|
||||
DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */,
|
||||
DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */,
|
||||
237B46962DC8F1C100B22D99 /* RateLimitedButton.swift in Sources */,
|
||||
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,
|
||||
DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */,
|
||||
DDD5BB0B2C285E45007E03CA /* LogDetail.swift in Sources */,
|
||||
|
|
@ -1434,6 +1486,7 @@
|
|||
233E99C32D849D7A00CC3A77 /* WeightCompactWidget.swift in Sources */,
|
||||
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
|
||||
DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */,
|
||||
DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */,
|
||||
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
|
||||
DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */,
|
||||
DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */,
|
||||
|
|
@ -1462,6 +1515,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 */,
|
||||
|
|
@ -1494,6 +1548,7 @@
|
|||
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */,
|
||||
DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */,
|
||||
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */,
|
||||
108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */,
|
||||
233E99C52D84A0B600CC3A77 /* CompactWidget.swift in Sources */,
|
||||
DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */,
|
||||
DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */,
|
||||
|
|
@ -1551,6 +1606,7 @@
|
|||
2344A2AB2D66974300170A77 /* ManagedAttributePropertyWrapper.swift in Sources */,
|
||||
BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */,
|
||||
D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */,
|
||||
BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */,
|
||||
DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */,
|
||||
DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */,
|
||||
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
|
||||
|
|
@ -1565,6 +1621,7 @@
|
|||
233E99C72D84A70900CC3A77 /* SoilCompactWidgets.swift in Sources */,
|
||||
BCE2D3C92C7C377F008E6199 /* FactoryResetNodeIntent.swift in Sources */,
|
||||
DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */,
|
||||
DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */,
|
||||
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */,
|
||||
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */,
|
||||
DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */,
|
||||
|
|
@ -1792,12 +1849,12 @@
|
|||
INFOPLIST_FILE = Meshtastic/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.2;
|
||||
MARKETING_VERSION = 2.6.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1825,12 +1882,12 @@
|
|||
INFOPLIST_FILE = Meshtastic/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.2;
|
||||
MARKETING_VERSION = 2.6.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1855,13 +1912,13 @@
|
|||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.2;
|
||||
MARKETING_VERSION = 2.6.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1887,13 +1944,13 @@
|
|||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.2;
|
||||
MARKETING_VERSION = 2.6.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1954,6 +2011,14 @@
|
|||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/DataDog/dd-sdk-ios.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.29.0;
|
||||
};
|
||||
};
|
||||
259792242C2F10B600AD1659 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apple/swift-protobuf.git";
|
||||
|
|
@ -1973,6 +2038,26 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
102B5EAA2E172F41003D191E /* DatadogCore */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */;
|
||||
productName = DatadogCore;
|
||||
};
|
||||
102B5EAC2E172F41003D191E /* DatadogCrashReporting */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */;
|
||||
productName = DatadogCrashReporting;
|
||||
};
|
||||
102B5EAE2E172F41003D191E /* DatadogLogs */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */;
|
||||
productName = DatadogLogs;
|
||||
};
|
||||
102B5EB02E172F41003D191E /* DatadogRUM */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 102B5EA92E172F41003D191E /* XCRemoteSwiftPackageReference "dd-sdk-ios" */;
|
||||
productName = DatadogRUM;
|
||||
};
|
||||
25A978B92C13F8ED0003AAE7 /* MeshtasticProtobufs */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MeshtasticProtobufs;
|
||||
|
|
@ -1992,6 +2077,9 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */,
|
||||
DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */,
|
||||
DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */,
|
||||
233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */,
|
||||
8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */,
|
||||
DDA28B1B2D32C89200EF726F /* MeshtasticDataModelV 48.xcdatamodel */,
|
||||
|
|
@ -2043,7 +2131,7 @@
|
|||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = 233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */;
|
||||
currentVersion = DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */;
|
||||
name = Meshtastic.xcdatamodeld;
|
||||
path = Meshtastic/Meshtastic.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
|||
48
Meshtastic/AppIntents/AddContactIntent.swift
Normal file
48
Meshtastic/AppIntents/AddContactIntent.swift
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// AddContactIntent.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Benjamin Faershtein on 5/13/25.
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import MeshtasticProtobufs
|
||||
|
||||
struct AddContactIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Add Contact"
|
||||
static var description: IntentDescription = "Takes a Meshtastic contact URL and saves it to the nodes database"
|
||||
|
||||
@Parameter(title: "Contact URL", description: "The URL for the node to add")
|
||||
var contactUrl: URL
|
||||
|
||||
// Define the function that performs the main logic
|
||||
func perform() async throws -> some IntentResult {
|
||||
// Ensure the BLE Manager is connected
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
|
||||
if contactUrl.absoluteString.lowercased().contains("meshtastic.org/v/#") {
|
||||
let components = self.contactUrl.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 success = BLEManager.shared.addContactFromURL(base64UrlString: contactData)
|
||||
if !success {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to add contact")
|
||||
}
|
||||
|
||||
} catch {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to parse contact data: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return a success result
|
||||
return .result()
|
||||
} else {
|
||||
throw AppIntentErrors.AppIntentError.message("The URL is not a valid Meshtastic contact link")
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Meshtastic/AppIntents/DisconnectNodeIntent.swift
Normal file
30
Meshtastic/AppIntents/DisconnectNodeIntent.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// DisconnectNodeIntent.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Benjamin Faershtein on 4/2/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppIntents
|
||||
|
||||
struct DisconnectNodeIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Disconnect Node"
|
||||
|
||||
static var description: IntentDescription = "Disconnect the currently connected node"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
|
||||
if let connectedPeripheral = BLEManager.shared.connectedPeripheral,
|
||||
connectedPeripheral.peripheral.state == .connected {
|
||||
BLEManager.shared.disconnectPeripheral(reconnect: false)
|
||||
} else {
|
||||
throw AppIntentErrors.AppIntentError.message("Error disconnecting node")
|
||||
}
|
||||
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -24,11 +23,10 @@ struct RestartNodeIntent: AppIntent {
|
|||
if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num,
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
|
||||
let fromUser = connectedNode.user,
|
||||
let toUser = connectedNode.user,
|
||||
let adminIndex = connectedNode.myInfo?.adminIndex {
|
||||
let toUser = connectedNode.user {
|
||||
|
||||
// Attempt to send shutdown, throw an error if it fails
|
||||
if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) {
|
||||
if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser) {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to restart")
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import AppIntents
|
|||
import MeshtasticProtobufs
|
||||
|
||||
struct SendWaypointIntent: AppIntent {
|
||||
|
||||
var defaultDate = Date.now.addingTimeInterval(60 * 480)
|
||||
|
||||
static var title = LocalizedStringResource("Send a Waypoint")
|
||||
|
||||
|
|
@ -23,13 +25,24 @@ struct SendWaypointIntent: AppIntent {
|
|||
@Parameter(title: "Emoji", default: "📍")
|
||||
var emojiParameter: String?
|
||||
|
||||
@Parameter(title: "Location")
|
||||
var locationParameter: CLPlacemark
|
||||
// Replace CLPlacemark with latitude and longitude parameters
|
||||
@Parameter(title: "Latitude", description: "Latitude in degrees (e.g., 37.7749)")
|
||||
var latitudeParameter: Double
|
||||
|
||||
@Parameter(title: "Longitude", description: "Longitude in degrees (e.g., -122.4194)")
|
||||
var longitudeParameter: Double
|
||||
|
||||
@Parameter(title: "Locked", default: false)
|
||||
var isLocked: Bool
|
||||
|
||||
@Parameter(title: "Expiration")
|
||||
var expiration: Date?
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
|
||||
// Provide default values if parameters are nil
|
||||
let name = nameParameter ?? "Dropped Pin"
|
||||
let description = descriptionParameter ?? ""
|
||||
|
|
@ -50,24 +63,39 @@ struct SendWaypointIntent: AppIntent {
|
|||
throw $emojiParameter.needsValueError("Must be a single emoji")
|
||||
}
|
||||
|
||||
// Validate latitude and longitude
|
||||
guard abs(latitudeParameter) <= 90 else {
|
||||
throw $latitudeParameter.needsValueError("Latitude must be between -90 and 90 degrees")
|
||||
}
|
||||
guard abs(longitudeParameter) <= 180 else {
|
||||
throw $longitudeParameter.needsValueError("Longitude must be between -180 and 180 degrees")
|
||||
}
|
||||
|
||||
var newWaypoint = Waypoint()
|
||||
|
||||
if let latitude = locationParameter.location?.coordinate.latitude {
|
||||
newWaypoint.latitudeI = Int32(latitude * 10_000_000)
|
||||
}
|
||||
|
||||
if let longitude = locationParameter.location?.coordinate.longitude {
|
||||
newWaypoint.longitudeI = Int32(longitude * 10_000_000)
|
||||
}
|
||||
// Set latitude and longitude directly
|
||||
newWaypoint.latitudeI = Int32(latitudeParameter * 10_000_000)
|
||||
newWaypoint.longitudeI = Int32(longitudeParameter * 10_000_000)
|
||||
|
||||
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = emoji.unicodeScalars
|
||||
// First element as an UInt32
|
||||
let unicode = unicodeScalers[unicodeScalers.startIndex].value
|
||||
newWaypoint.icon = unicode
|
||||
newWaypoint.name = name
|
||||
newWaypoint.description_p = description
|
||||
|
||||
if let expirationDate = expiration {
|
||||
newWaypoint.expire = UInt32(expirationDate.timeIntervalSince1970)
|
||||
}
|
||||
|
||||
if isLocked {
|
||||
if let connectedPeripheral = BLEManager.shared.connectedPeripheral {
|
||||
newWaypoint.lockedTo = UInt32(connectedPeripheral.num)
|
||||
} else {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
}
|
||||
|
||||
if !BLEManager.shared.sendWaypoint(waypoint: newWaypoint) {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to Send Waypoint")
|
||||
}
|
||||
|
|
@ -75,11 +103,9 @@ struct SendWaypointIntent: AppIntent {
|
|||
}
|
||||
|
||||
private func isValidSingleEmoji(_ emoji: String) -> Bool {
|
||||
// This regex pattern is for matching a single emoji
|
||||
let emojiPattern = "^([\\p{So}\\p{Cn}])$"
|
||||
let regex = try? NSRegularExpression(pattern: emojiPattern, options: [])
|
||||
let matches = regex?.matches(in: emoji, options: [], range: NSRange(location: 0, length: emoji.utf16.count))
|
||||
|
||||
return matches?.count == 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,5 +32,12 @@ struct ShortcutsProvider: AppShortcutsProvider {
|
|||
"Send a \(.applicationName) group message"],
|
||||
shortTitle: "Group Message",
|
||||
systemImageName: "message")
|
||||
AppShortcut(intent: DisconnectNodeIntent(),
|
||||
phrases: ["Disconnect \(.applicationName) node",
|
||||
"Disconnect my \(.applicationName) node",
|
||||
"Disconnect from \(.applicationName)",
|
||||
"Disconnect \(.applicationName)"],
|
||||
shortTitle: "Disconnect",
|
||||
systemImageName: "antenna.radiowaves.left.and.right.slash")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,10 @@ struct ShutDownNodeIntent: AppIntent {
|
|||
if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num,
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
|
||||
let fromUser = connectedNode.user,
|
||||
let toUser = connectedNode.user,
|
||||
let adminIndex = connectedNode.myInfo?.adminIndex {
|
||||
let toUser = connectedNode.user {
|
||||
|
||||
// Attempt to send shutdown, throw an error if it fails
|
||||
if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) {
|
||||
if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser) {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to shut down")
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
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()
|
||||
}
|
||||
}
|
||||
12
Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json
vendored
Normal file
12
Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "heltec_mesh_pocket.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
196
Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg
vendored
Normal file
196
Meshtastic/Assets.xcassets/HELTECMESHPOCKET.imageset/heltec_mesh_pocket.svg
vendored
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="379.51 218.53 478.45 660.45"
|
||||
version="1.1"
|
||||
id="svg72"
|
||||
sodipodi:docname="heltec_mesh_pocket.svg"
|
||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview72"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.78391112"
|
||||
inkscape:cx="4.4647919"
|
||||
inkscape:cy="146.06248"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="890"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_2" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<style
|
||||
id="style1">.cls-1{fill:#d5bd0a;}.cls-2{fill:#eceded;}.cls-3{fill:#e3e1e0;}.cls-4{fill:#1d1d1b;}.cls-12,.cls-5,.cls-7{fill:none;}.cls-5,.cls-7{stroke:#1d1d1b;stroke-miterlimit:10;}.cls-5{stroke-width:0.87px;}.cls-6{fill:#fff;}.cls-7{stroke-width:3.49px;}.cls-8{fill:#2b293d;}.cls-9{fill:#2ea358;}.cls-10{fill:#efefef;}.cls-11{fill:#acd087;}.cls-12{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.31px;}.cls-13{fill:#2590d0;}.cls-14{fill:#4e4d4e;}</style>
|
||||
</defs>
|
||||
<g
|
||||
id="Layer_2"
|
||||
data-name="Layer 2">
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M784.42,219.19H419.9a39.73,39.73,0,0,0-39.74,39.73V838.6a39.74,39.74,0,0,0,39.74,39.73H784.42a39.73,39.73,0,0,0,39.73-39.73V258.92A39.73,39.73,0,0,0,784.42,219.19Zm31,619.41a31,31,0,0,1-31,31H419.9a31,31,0,0,1-31-31V258.92a31,31,0,0,1,31-31H784.42a31,31,0,0,1,31,31Z"
|
||||
id="path1" />
|
||||
<rect
|
||||
class="cls-2"
|
||||
x="388.9"
|
||||
y="227.92"
|
||||
width="426.51"
|
||||
height="641.67"
|
||||
rx="31"
|
||||
id="rect1"
|
||||
style="fill:#fcfcfc;fill-opacity:1" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M857.3,302.32V633.56a7.53,7.53,0,0,1-1.75,4.84l-31.4,37.77V260a67.74,67.74,0,0,1,32.06,36.41A16.78,16.78,0,0,1,857.3,302.32Z"
|
||||
id="path2" />
|
||||
<rect
|
||||
class="cls-3"
|
||||
x="439.49"
|
||||
y="398.62"
|
||||
width="324.87"
|
||||
height="163.57"
|
||||
id="rect2"
|
||||
style="fill:#bfbfbf;fill-opacity:1" />
|
||||
<rect
|
||||
class="cls-8"
|
||||
x="513.04"
|
||||
y="724.36"
|
||||
width="166.81"
|
||||
height="33.44"
|
||||
rx="3.87"
|
||||
id="rect48" />
|
||||
<path
|
||||
class="cls-9"
|
||||
d="M558.92,724.36V757.8h-42a3.87,3.87,0,0,1-3.87-3.87V728.24a3.88,3.88,0,0,1,3.87-3.88Z"
|
||||
id="path48" />
|
||||
<rect
|
||||
class="cls-10"
|
||||
x="514.95"
|
||||
y="740.28"
|
||||
width="20.46"
|
||||
height="2.8"
|
||||
transform="translate(-383.39 761.4) rotate(-55.94)"
|
||||
id="rect49" />
|
||||
<path
|
||||
class="cls-10"
|
||||
d="M551.65,750.93,541.54,736l-10.12,14.93-2.31-1.56L540,733.29a1.8,1.8,0,0,1,1.54-.82h0a1.85,1.85,0,0,1,1.57.87l10.85,16Zm-10.88-16.07s0,0,0,0Zm1.57,0,0,0Z"
|
||||
id="path49" />
|
||||
<path
|
||||
class="cls-10"
|
||||
d="M566.72,731.61a31.88,31.88,0,0,1,5.15-.39c2.66,0,4.6.6,5.84,1.68a5.43,5.43,0,0,1,1.82,4.25,5.93,5.93,0,0,1-1.61,4.35,8.72,8.72,0,0,1-6.36,2.23,9.52,9.52,0,0,1-2.16-.18v8.14h-2.68Zm2.68,9.8a9.32,9.32,0,0,0,2.23.21c3.24,0,5.21-1.54,5.21-4.34s-1.94-4-4.9-4a11.68,11.68,0,0,0-2.54.21Z"
|
||||
id="path50" />
|
||||
<path
|
||||
class="cls-10"
|
||||
d="M595.8,744.27c0,5.4-3.83,7.75-7.44,7.75-4,0-7.16-2.89-7.16-7.51,0-4.88,3.27-7.75,7.41-7.75S595.8,739.81,595.8,744.27Zm-11.85.15c0,3.2,1.88,5.61,4.54,5.61s4.53-2.38,4.53-5.67c0-2.47-1.26-5.61-4.47-5.61S584,741.65,584,744.42Z"
|
||||
id="path51" />
|
||||
<path
|
||||
class="cls-10"
|
||||
d="M600.1,737.1l2,7.41c.43,1.63.84,3.14,1.11,4.65h.1c.34-1.48.83-3,1.32-4.62L607,737.1h2.29l2.31,7.29c.56,1.75,1,3.29,1.33,4.77h.09a44.65,44.65,0,0,1,1.14-4.74l2.13-7.32H619l-4.82,14.59h-2.47l-2.28-7a48.12,48.12,0,0,1-1.33-4.79h-.06a40.69,40.69,0,0,1-1.36,4.82l-2.41,6.94h-2.47l-4.5-14.59Z"
|
||||
id="path52" />
|
||||
<path
|
||||
class="cls-10"
|
||||
d="M623,744.87c.06,3.59,2.41,5.07,5.12,5.07a10.08,10.08,0,0,0,4.14-.75l.46,1.9a12.41,12.41,0,0,1-5,.9c-4.6,0-7.35-3-7.35-7.36s2.66-7.87,7-7.87c4.88,0,6.18,4.19,6.18,6.88a9.56,9.56,0,0,1-.1,1.23Zm8-1.89c0-1.69-.71-4.32-3.76-4.32-2.75,0-4,2.48-4.17,4.32Z"
|
||||
id="path53" />
|
||||
<path
|
||||
class="cls-10"
|
||||
d="M636.9,741.65c0-1.72,0-3.2-.12-4.55h2.38l.09,2.86h.12a4.48,4.48,0,0,1,4.14-3.2,3,3,0,0,1,.77.09v2.51a4.19,4.19,0,0,0-.93-.09,3.77,3.77,0,0,0-3.64,3.4,7.56,7.56,0,0,0-.12,1.24v7.78H636.9Z"
|
||||
id="path54" />
|
||||
<path
|
||||
class="cls-10"
|
||||
d="M648.2,744.87c.07,3.59,2.41,5.07,5.13,5.07a10.11,10.11,0,0,0,4.14-.75l.46,1.9a12.44,12.44,0,0,1-5,.9c-4.6,0-7.35-3-7.35-7.36s2.66-7.87,7-7.87c4.88,0,6.17,4.19,6.17,6.88a10,10,0,0,1-.09,1.23Zm8-1.89c0-1.69-.71-4.32-3.77-4.32-2.75,0-3.95,2.48-4.17,4.32Z"
|
||||
id="path55" />
|
||||
<path
|
||||
class="cls-10"
|
||||
d="M675.07,730.28v17.64c0,1.3,0,2.77.12,3.77h-2.44l-.12-2.53h-.07a5.55,5.55,0,0,1-5.09,2.86c-3.61,0-6.39-3-6.39-7.42,0-4.85,3.06-7.84,6.7-7.84a5.05,5.05,0,0,1,4.51,2.23h.06v-8.71ZM672.35,743a4.21,4.21,0,0,0-.13-1.11,4,4,0,0,0-3.91-3.08c-2.81,0-4.48,2.42-4.48,5.64,0,3,1.48,5.4,4.41,5.4a4.06,4.06,0,0,0,4-3.17,4.15,4.15,0,0,0,.13-1.14Z"
|
||||
id="path56" />
|
||||
<rect
|
||||
class="cls-11"
|
||||
x="561.8"
|
||||
y="768.06"
|
||||
width="65.15"
|
||||
height="6.66"
|
||||
rx="3.33"
|
||||
id="rect56" />
|
||||
<path
|
||||
class="cls-12"
|
||||
d="M784.42,219.19H419.9a39.73,39.73,0,0,0-39.74,39.73V838.6a39.74,39.74,0,0,0,39.74,39.73H784.42a39.73,39.73,0,0,0,39.73-39.73V258.92A39.73,39.73,0,0,0,784.42,219.19Zm31,619.41a31,31,0,0,1-31,31H419.9a31,31,0,0,1-31-31V258.92a31,31,0,0,1,31-31H784.42a31,31,0,0,1,31,31Z"
|
||||
id="path57" />
|
||||
<path
|
||||
class="cls-12"
|
||||
d="M824.15,260a67.74,67.74,0,0,1,32.06,36.41,16.78,16.78,0,0,1,1.09,5.87V633.56a7.53,7.53,0,0,1-1.75,4.84l-31.4,37.77"
|
||||
id="path58" />
|
||||
<path
|
||||
class="cls-13"
|
||||
d="M714.73,253.2s0,0,0,0l-.24,1-7.2,29a4.47,4.47,0,0,0,.7,3.69l.06.07a15.25,15.25,0,0,1-1.59-.42,9.75,9.75,0,0,1-3.94-2.24,6.35,6.35,0,0,1-1.63-6.06l.67-2.7L705,261.64h-4.77l-1.67,6.73c-.52,2.1-.6,3,.25,5a1.18,1.18,0,0,1,0,.25l-2.48-1.32a5.56,5.56,0,0,1-2.95-6.41l4.11-16.56a.09.09,0,0,0,0,0,4.73,4.73,0,0,0-.86-4.11l1.69.75.76.34a5.79,5.79,0,0,1,3.27,6.67l-1,4h4.76l2.07-8.35a4.52,4.52,0,0,0,0-2,6.32,6.32,0,0,0-1.17-2.68,13,13,0,0,1,2.1.71C711.81,245.69,715.73,248.21,714.73,253.2Z"
|
||||
id="path59" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M724.83,258.46l-.59,2.36h5l-1.12,4.54h-5l-.76,3.06h5L726.25,273h-9.88l1.13-4.54.76-3.06,1.12-4.54.59-2.36.71-2.86a2.23,2.23,0,0,1,1.27-1.5h0a2.22,2.22,0,0,1,.87-.18H731l-1.13,4.54Z"
|
||||
id="path60" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M765.52,258.46l-.59,2.36h5l-1.13,4.54h-5l-.76,3.06h5L766.94,273h-9.88l1.13-4.54.76-3.06,1.13-4.54.58-2.36.71-2.86a2.22,2.22,0,0,1,1.28-1.5h0a2.15,2.15,0,0,1,.87-.18h8.14l-1.13,4.54Z"
|
||||
id="path61" />
|
||||
<polygon
|
||||
class="cls-14"
|
||||
points="758.02 253.92 757.07 257.81 753.56 257.81 753.39 258.46 752.81 260.82 749.8 272.96 744.93 272.96 746.07 268.42 746.82 265.36 747.95 260.82 748.53 258.46 748.7 257.81 745.1 257.81 745.91 254.53 746.06 253.92 758.02 253.92"
|
||||
id="polygon61" />
|
||||
<polygon
|
||||
class="cls-14"
|
||||
points="741.13 268.42 740 272.96 730.13 272.96 731.26 268.42 732.01 265.36 733.14 260.82 733.73 258.46 734.7 254.53 734.85 253.92 739.71 253.92 738.59 258.46 738 260.82 736.12 268.42 741.13 268.42"
|
||||
id="polygon62" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M788.73,255l-1.5,6.26a3.48,3.48,0,0,0-1.3-2.36c-1.86-1.4-4.88-.52-6.78,2s-1.92,5.66-.07,7.05,4.88.52,6.77-2c.15-.2.28-.4.41-.61l-1.73,7.24a7.65,7.65,0,0,1-6.77.91c-4.31-1.62-6.17-7.27-4.16-12.64C775.55,255.69,781.94,250.72,788.73,255Z"
|
||||
id="path62" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M718.84,282.11h-2.13l-.55,1h-1.92l3.81-6.16h2.05l.76,6.16h-2Zm-.06-1.33-.12-2.22-1.21,2.22Z"
|
||||
id="path63" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M726.65,277h1.88l-.91,3.67a3.57,3.57,0,0,1-.43,1,3.2,3.2,0,0,1-1.61,1.36,4.44,4.44,0,0,1-1.37.2,8.21,8.21,0,0,1-1-.06,2,2,0,0,1-.82-.25,1.53,1.53,0,0,1-.51-.53,1.47,1.47,0,0,1-.22-.71,3.41,3.41,0,0,1,.08-1l.91-3.67h1.87l-.93,3.75a1,1,0,0,0,.08.79.81.81,0,0,0,.7.29,1.32,1.32,0,0,0,.83-.28,1.4,1.4,0,0,0,.47-.8Z"
|
||||
id="path64" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M729.34,277h5.72l-.38,1.52h-1.92l-1.15,4.64h-1.88l1.15-4.64H729Z"
|
||||
id="path65" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M734.62,280.05A4.25,4.25,0,0,1,736,277.7a3.87,3.87,0,0,1,2.52-.84,2.49,2.49,0,0,1,2.13.83,2.69,2.69,0,0,1,.25,2.31,4.83,4.83,0,0,1-.8,1.77,3.77,3.77,0,0,1-1.3,1.08,3.93,3.93,0,0,1-1.79.38,3.32,3.32,0,0,1-1.62-.33,1.81,1.81,0,0,1-.83-1A3.25,3.25,0,0,1,734.62,280.05Zm1.87,0a1.91,1.91,0,0,0,0,1.34.89.89,0,0,0,.83.41,1.46,1.46,0,0,0,1-.4,3,3,0,0,0,.69-1.43,1.78,1.78,0,0,0,0-1.28.94.94,0,0,0-.85-.4,1.48,1.48,0,0,0-1,.41A2.67,2.67,0,0,0,736.49,280.06Z"
|
||||
id="path66" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M742.67,277h2.47l0,3.75L747,277h2.47L748,283.13h-1.54l1.17-4.7-2.35,4.7h-1.39l0-4.7-1.17,4.7h-1.54Z"
|
||||
id="path67" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M753.17,282.11H751l-.55,1h-1.92l3.82-6.16h2.05l.75,6.16h-2Zm-.06-1.33-.12-2.22-1.21,2.22Z"
|
||||
id="path68" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M756.31,277H762l-.38,1.52h-1.92l-1.15,4.64h-1.88l1.16-4.64h-1.92Z"
|
||||
id="path69" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M762.94,277h1.89l-1.53,6.16h-1.89Z"
|
||||
id="path70" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M765.17,280.05a4.21,4.21,0,0,1,1.42-2.35,3.86,3.86,0,0,1,2.51-.84,2.5,2.5,0,0,1,2.14.83,2.69,2.69,0,0,1,.25,2.31,4.83,4.83,0,0,1-.8,1.77,3.8,3.8,0,0,1-1.31,1.08,3.89,3.89,0,0,1-1.78.38,3.32,3.32,0,0,1-1.62-.33,1.77,1.77,0,0,1-.83-1A3.23,3.23,0,0,1,765.17,280.05Zm1.88,0a1.91,1.91,0,0,0,0,1.34.89.89,0,0,0,.83.41,1.47,1.47,0,0,0,1-.4,3,3,0,0,0,.68-1.43,1.68,1.68,0,0,0,0-1.28.91.91,0,0,0-.84-.4,1.47,1.47,0,0,0-1,.41A2.67,2.67,0,0,0,767.05,280.06Z"
|
||||
id="path71" />
|
||||
<path
|
||||
class="cls-14"
|
||||
d="M773.26,277H775l1.44,3.41.85-3.41h1.77l-1.53,6.16h-1.77l-1.44-3.39-.84,3.39h-1.77Z"
|
||||
id="path72" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
12
Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json
vendored
Normal file
12
Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "seeed_solar.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg
vendored
Normal file
1
Meshtastic/Assets.xcassets/SEEEDSOLARNODE.imageset/seeed_solar.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 98 KiB |
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"symbols" : [
|
||||
{
|
||||
"filename" : "progress.ring.dashed.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 341-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3300 2200">
|
||||
<!--glyph: "", point size: 100.0, font version: "20.0d10e1", template writer version: "138.0.0"-->
|
||||
<style>.defaults {-sfsymbols-wiggle-style:clockwise;-sfsymbols-rotates-clockwise:true}
|
||||
|
||||
.monochrome-0 {-sfsymbols-variable-threshold:0.93;-sfsymbols-motion-group:12;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-1 {-sfsymbols-variable-threshold:0.86;-sfsymbols-motion-group:11;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-2 {-sfsymbols-variable-threshold:0.78;-sfsymbols-motion-group:10;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-3 {-sfsymbols-variable-threshold:0.7;-sfsymbols-motion-group:9;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-4 {-sfsymbols-variable-threshold:0.63;-sfsymbols-motion-group:8;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-5 {-sfsymbols-variable-threshold:0.55;-sfsymbols-motion-group:7;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-6 {-sfsymbols-variable-threshold:0.47;-sfsymbols-motion-group:6;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-7 {-sfsymbols-variable-threshold:0.39;-sfsymbols-motion-group:5;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-8 {-sfsymbols-variable-threshold:0.32;-sfsymbols-motion-group:4;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-9 {-sfsymbols-variable-threshold:0.24;-sfsymbols-motion-group:3;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-10 {-sfsymbols-variable-threshold:0.16;-sfsymbols-motion-group:2;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.monochrome-11 {-sfsymbols-variable-threshold:0.09;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
|
||||
.multicolor-0:tintColor {-sfsymbols-variable-threshold:0.93;-sfsymbols-motion-group:12;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-1:tintColor {-sfsymbols-variable-threshold:0.86;-sfsymbols-motion-group:11;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-2:tintColor {-sfsymbols-variable-threshold:0.78;-sfsymbols-motion-group:10;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-3:tintColor {-sfsymbols-clear-behind:true;-sfsymbols-variable-threshold:0.7;-sfsymbols-motion-group:9;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-4:tintColor {-sfsymbols-variable-threshold:0.63;-sfsymbols-motion-group:8;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-5:tintColor {-sfsymbols-variable-threshold:0.55;-sfsymbols-motion-group:7;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-6:tintColor {-sfsymbols-variable-threshold:0.47;-sfsymbols-motion-group:6;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-7:tintColor {-sfsymbols-variable-threshold:0.39;-sfsymbols-motion-group:5;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-8:tintColor {-sfsymbols-variable-threshold:0.32;-sfsymbols-motion-group:4;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-9:tintColor {-sfsymbols-variable-threshold:0.24;-sfsymbols-motion-group:3;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-10:tintColor {-sfsymbols-variable-threshold:0.16;-sfsymbols-motion-group:2;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.multicolor-11:tintColor {-sfsymbols-variable-threshold:0.09;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
|
||||
.hierarchical-0:primary {-sfsymbols-variable-threshold:0.93;-sfsymbols-motion-group:12;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-1:primary {-sfsymbols-variable-threshold:0.86;-sfsymbols-motion-group:11;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-2:primary {-sfsymbols-variable-threshold:0.78;-sfsymbols-motion-group:10;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-3:primary {-sfsymbols-clear-behind:true;-sfsymbols-variable-threshold:0.7;-sfsymbols-motion-group:9;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-4:secondary {-sfsymbols-variable-threshold:0.63;-sfsymbols-motion-group:8;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-5:tertiary {-sfsymbols-variable-threshold:0.55;-sfsymbols-motion-group:7;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-6:primary {-sfsymbols-variable-threshold:0.47;-sfsymbols-motion-group:6;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-7:primary {-sfsymbols-variable-threshold:0.39;-sfsymbols-motion-group:5;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-8:primary {-sfsymbols-variable-threshold:0.32;-sfsymbols-motion-group:4;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-9:primary {-sfsymbols-variable-threshold:0.24;-sfsymbols-motion-group:3;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-10:primary {-sfsymbols-variable-threshold:0.16;-sfsymbols-motion-group:2;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
.hierarchical-11:primary {-sfsymbols-variable-threshold:0.09;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-4ed3e2b5f369b4bf}
|
||||
|
||||
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
|
||||
</style>
|
||||
<g id="Notes">
|
||||
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
|
||||
<g transform="matrix(0.2 0 0 0.2 263 1933)">
|
||||
<path d="m46.2402 4.15039c21.7773 0 39.4531-17.627 39.4531-39.4043s-17.6758-39.4043-39.4531-39.4043c-21.7285 0-39.4043 17.627-39.4043 39.4043s17.6758 39.4043 39.4043 39.4043Zm0-7.42188c-17.6758 0-31.9336-14.3066-31.9336-31.9824s14.2578-31.9824 31.9336-31.9824 31.9824 14.3066 31.9824 31.9824-14.3066 31.9824-31.9824 31.9824Zm-17.9688-31.9824c0 2.14844 1.51367 3.61328 3.75977 3.61328h10.498v10.5957c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094v-10.5957h10.5957c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-10.5957v-10.5469c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v10.5469h-10.498c-2.24609 0-3.75977 1.51367-3.75977 3.71094Z"/>
|
||||
</g>
|
||||
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
|
||||
<path d="m58.5449 14.5508c27.4902 0 49.8047-22.3145 49.8047-49.8047s-22.3145-49.8047-49.8047-49.8047-49.8047 22.3145-49.8047 49.8047 22.3145 49.8047 49.8047 49.8047Zm0-8.30078c-22.9492 0-41.5039-18.5547-41.5039-41.5039s18.5547-41.5039 41.5039-41.5039 41.5039 18.5547 41.5039 41.5039-18.5547 41.5039-41.5039 41.5039Zm-22.6562-41.5039c0 2.39258 1.66016 4.00391 4.15039 4.00391h14.3555v14.4043c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039v-14.4043h14.4043c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-14.4043v-14.3555c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v14.3555h-14.3555c-2.49023 0-4.15039 1.70898-4.15039 4.15039Z"/>
|
||||
</g>
|
||||
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
|
||||
<path d="m74.8535 28.3203c35.1074 0 63.623-28.4668 63.623-63.5742s-28.5156-63.623-63.623-63.623-63.5742 28.5156-63.5742 63.623 28.4668 63.5742 63.5742 63.5742Zm0-9.08203c-30.127 0-54.4922-24.3652-54.4922-54.4922s24.3652-54.4922 54.4922-54.4922 54.4922 24.3652 54.4922 54.4922-24.3652 54.4922-54.4922 54.4922Zm-28.8574-54.4922c0 2.58789 1.85547 4.39453 4.58984 4.39453h19.7266v19.7754c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984v-19.7754h19.7754c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-19.7754v-19.7266c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v19.7266h-19.7266c-2.73438 0-4.58984 1.85547-4.58984 4.58984Z"/>
|
||||
</g>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
|
||||
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
|
||||
<g transform="matrix(0.2 0 0 0.2 776 1933)">
|
||||
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l6.29883-17.2363h28.8086l6.29883 17.2363c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm13.4766-28.3691 11.8652-32.8613h0.244141l11.8652 32.8613Z"/>
|
||||
</g>
|
||||
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
|
||||
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
|
||||
<path d="m14.209 9.32617 8.49609 8.54492c4.29688 4.3457 9.22852 4.05273 13.8672-1.07422l53.4668-58.9355-4.83398-4.88281-53.0762 58.3984c-1.75781 2.00195-3.41797 2.49023-5.76172 0.146484l-5.85938-5.81055c-2.34375-2.29492-1.80664-4.00391 0.195312-5.81055l57.373-54.0039-4.88281-4.83398-57.959 54.4434c-4.93164 4.58984-5.32227 9.47266-1.02539 13.8184Zm32.0801-90.9668c-2.09961 2.05078-2.24609 4.93164-1.07422 6.88477 1.17188 1.80664 3.4668 2.97852 6.68945 2.14844 7.32422-1.70898 14.9414-2.00195 22.0703 2.68555l-2.92969 7.27539c-1.70898 4.15039-0.830078 7.08008 1.85547 9.81445l11.4746 11.5723c2.44141 2.44141 4.49219 2.53906 7.32422 2.05078l5.32227-0.976562 3.32031 3.36914-0.195312 2.7832c-0.195312 2.49023 0.439453 4.39453 2.88086 6.78711l3.80859 3.71094c2.39258 2.39258 5.46875 2.53906 7.8125 0.195312l14.5508-14.5996c2.34375-2.34375 2.24609-5.32227-0.146484-7.71484l-3.85742-3.80859c-2.39258-2.39258-4.24805-3.17383-6.64062-2.97852l-2.88086 0.244141-3.22266-3.17383 1.2207-5.61523c0.634766-2.83203-0.146484-5.0293-3.07617-7.95898l-10.9863-10.9375c-16.6992-16.6016-38.8672-16.2109-53.3203-1.75781Zm7.4707 1.85547c12.1582-8.88672 28.6133-7.37305 39.7461 3.75977l12.1582 12.0605c1.17188 1.17188 1.36719 2.09961 1.02539 3.80859l-1.61133 7.42188 7.51953 7.42188 4.93164-0.292969c1.26953-0.0488281 1.66016 0.0488281 2.63672 1.02539l2.88086 2.88086-12.207 12.207-2.88086-2.88086c-0.976562-0.976562-1.12305-1.36719-1.07422-2.68555l0.341797-4.88281-7.4707-7.42188-7.61719 1.26953c-1.61133 0.341797-2.34375 0.195312-3.56445-0.976562l-10.0098-10.0098c-1.26953-1.17188-1.41602-2.00195-0.634766-3.85742l4.39453-10.4492c-7.8125-7.27539-17.9688-10.4004-28.125-7.42188-0.78125 0.195312-1.07422-0.439453-0.439453-0.976562Z"/>
|
||||
</g>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
|
||||
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.6.0</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 16 or greater</text>
|
||||
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from progress.ring.dashed</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
|
||||
</g>
|
||||
<g id="Guides">
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
|
||||
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
|
||||
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
|
||||
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
|
||||
<line id="right-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2984.21" x2="2984.21" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2882.59" x2="2882.59" y1="600.785" y2="720.121"/>
|
||||
<line id="right-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1499.82" x2="1499.82" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1399.87" x2="1399.87" y1="600.785" y2="720.121"/>
|
||||
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="607.643" x2="607.643" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="511.779" x2="511.779" y1="600.785" y2="720.121"/>
|
||||
</g>
|
||||
<g id="Symbols">
|
||||
<g id="Black-S" transform="matrix(1 0 0 1 2882.59 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:primary SFSymbolsPreviewWireframe" d="M40.5762-61.1816C42.5293-61.9141 44.5801-62.4512 46.7285-62.793C47.9492-62.9883 48.8281-63.916 48.8281-65.1855L48.8281-76.9043C48.8281-78.4668 47.6562-79.5898 46.0938-79.4434C41.4062-78.9551 36.9141-77.7344 32.7637-75.8301C31.3477-75.1953 30.9082-73.6328 31.6895-72.3145L37.5488-62.1582C38.1836-61.084 39.4043-60.6934 40.5762-61.1816Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M29.0039-52.5879C30.3223-54.248 31.7871-55.7129 33.4473-57.0312C34.4727-57.8125 34.7656-59.082 34.1309-60.1562L28.2715-70.3125C27.4902-71.6797 25.8789-72.0703 24.6094-71.1426C20.8984-68.4082 17.627-65.1367 14.8926-61.4258C13.9648-60.1074 14.3555-58.5449 15.7227-57.7637L25.8789-51.9043C26.9531-51.2695 28.2227-51.5625 29.0039-52.5879Z"/>
|
||||
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M23.291-39.3555C23.584-41.4551 24.1211-43.5059 24.9023-45.4102C25.3906-46.6309 25-47.8516 23.877-48.4863L13.7207-54.3457C12.4023-55.127 10.8398-54.6875 10.2051-53.2715C8.30078-49.1211 7.08008-44.6289 6.5918-39.9414C6.44531-38.3789 7.56836-37.207 9.13086-37.207L20.8984-37.207C22.168-37.207 23.0957-38.0859 23.291-39.3555Z"/>
|
||||
<path class="monochrome-3 multicolor-3:tintColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M24.8535-25.0977C24.1211-27.002 23.584-29.0039 23.291-31.1523C23.0957-32.4219 22.168-33.3008 20.8984-33.3008L9.13086-33.3008C7.56836-33.3008 6.44531-32.1289 6.5918-30.5664C7.08008-25.8789 8.30078-21.3867 10.2051-17.2363C10.8398-15.8203 12.4023-15.3809 13.7207-16.1621L23.877-22.0215C25-22.6562 25.3418-23.877 24.8535-25.0977Z"/>
|
||||
<path class="monochrome-4 multicolor-4:tintColor hierarchical-4:secondary SFSymbolsPreviewWireframe" d="M33.4473-13.4766C31.8359-14.7949 30.3223-16.2598 29.0039-17.9199C28.2227-18.9453 26.9531-19.2383 25.8789-18.6035L15.7227-12.7441C14.3555-11.9629 13.9648-10.4004 14.8926-9.08203C17.627-5.37109 20.8984-2.05078 24.6094 0.634766C25.8789 1.5625 27.4902 1.12305 28.2715-0.195312L34.1309-10.3516C34.7656-11.4258 34.4727-12.6953 33.4473-13.4766Z"/>
|
||||
<path class="monochrome-5 multicolor-5:tintColor hierarchical-5:tertiary SFSymbolsPreviewWireframe" d="M46.7285-7.71484C44.5801-8.05664 42.5293-8.59375 40.5762-9.32617C39.4043-9.81445 38.1836-9.42383 37.5488-8.34961L31.6895 1.80664C30.9082 3.125 31.3477 4.6875 32.7637 5.32227C36.9141 7.22656 41.4062 8.44727 46.0938 8.93555C47.6562 9.08203 48.8281 7.95898 48.8281 6.39648L48.8281-5.32227C48.8281-6.5918 47.9492-7.51953 46.7285-7.71484Z"/>
|
||||
<path class="monochrome-6 multicolor-6:tintColor hierarchical-6:primary SFSymbolsPreviewWireframe" d="M61.0352-9.32617C59.082-8.59375 57.0312-8.05664 54.8828-7.71484C53.6133-7.51953 52.7344-6.5918 52.7344-5.32227L52.7344 6.39648C52.7344 7.95898 53.9062 9.08203 55.4688 8.93555C60.1562 8.44727 64.6484 7.22656 68.8477 5.32227C70.2637 4.6875 70.7031 3.07617 69.9219 1.75781L64.0625-8.34961C63.4277-9.42383 62.207-9.81445 61.0352-9.32617Z"/>
|
||||
<path class="monochrome-7 multicolor-7:tintColor hierarchical-7:primary SFSymbolsPreviewWireframe" d="M72.6074-17.9199C71.2891-16.2598 69.8242-14.7949 68.1641-13.4766C67.1387-12.6953 66.8457-11.4258 67.4805-10.3516L73.3398-0.195312C74.1211 1.17188 75.7324 1.5625 77.002 0.634766C80.7129-2.09961 83.9844-5.37109 86.7188-9.13086C87.5977-10.4004 87.207-11.9629 85.8887-12.7441L75.7324-18.6035C74.6582-19.2383 73.3887-18.9453 72.6074-17.9199Z"/>
|
||||
<path class="monochrome-8 multicolor-8:tintColor hierarchical-8:primary SFSymbolsPreviewWireframe" d="M78.3203-31.1523C78.0273-29.0039 77.4902-27.002 76.709-25.0488C76.2207-23.877 76.6113-22.6562 77.6855-22.0215L87.8418-16.1621C89.1602-15.3809 90.7715-15.8203 91.4062-17.2852C93.2617-21.3867 94.4824-25.8789 94.9707-30.5664C95.1172-32.1289 93.9453-33.3008 92.4316-33.3008L80.7129-33.3008C79.4434-33.3008 78.5156-32.4219 78.3203-31.1523Z"/>
|
||||
<path class="monochrome-9 multicolor-9:tintColor hierarchical-9:primary SFSymbolsPreviewWireframe" d="M76.709-45.459C77.4902-43.5059 78.0273-41.5039 78.3203-39.3555C78.5156-38.0859 79.4434-37.207 80.7129-37.207L92.4316-37.207C93.9453-37.207 95.1172-38.3789 94.9707-39.9414C94.5312-44.5801 93.2617-49.0723 91.4062-53.2227C90.7715-54.6875 89.1602-55.127 87.8418-54.3457L77.6855-48.4863C76.6113-47.8516 76.2207-46.6309 76.709-45.459Z"/>
|
||||
<path class="monochrome-10 multicolor-10:tintColor hierarchical-10:primary SFSymbolsPreviewWireframe" d="M68.1641-57.0312C69.7754-55.7129 71.2891-54.248 72.6074-52.5879C73.3887-51.5625 74.6582-51.2695 75.7324-51.9043L85.8887-57.7637C87.207-58.5449 87.5977-60.1074 86.7188-61.377C83.9844-65.1367 80.7129-68.457 77.002-71.1426C75.7324-72.0703 74.1211-71.6309 73.3398-70.3125L67.4805-60.1562C66.8457-59.082 67.1387-57.8125 68.1641-57.0312Z"/>
|
||||
<path class="monochrome-11 multicolor-11:tintColor hierarchical-11:primary SFSymbolsPreviewWireframe" d="M54.834-62.793C57.0312-62.4512 59.082-61.9141 61.0352-61.1816C62.207-60.6934 63.4277-61.084 64.0625-62.1582L69.9219-72.3145C70.7031-73.6328 70.2637-75.1953 68.8477-75.8301C64.6973-77.7344 60.2051-78.9551 55.4688-79.4434C53.9062-79.5898 52.7344-78.4668 52.7344-76.9043L52.7344-65.1855C52.7344-63.916 53.6133-62.9883 54.834-62.793Z"/>
|
||||
</g>
|
||||
<g id="Regular-S" transform="matrix(1 0 0 1 1399.87 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:primary SFSymbolsPreviewWireframe" d="M38.623-61.4746C41.1621-62.5977 43.8477-63.3301 46.7285-63.6719C47.5098-63.7207 48.0469-64.3066 48.0469-65.0879L48.0469-76.709C48.0469-77.6367 47.3633-78.3691 46.4355-78.2715C41.1621-77.832 36.1816-76.3672 31.5918-74.2188C30.7617-73.8281 30.5176-72.8516 30.957-72.0703L36.6699-62.1094C37.1094-61.4258 37.8906-61.1816 38.623-61.4746Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M27.0508-52.2949C28.7109-54.541 30.6641-56.543 32.959-58.2031C33.5938-58.6914 33.7402-59.4727 33.3496-60.1562L27.5879-70.1172C27.1484-70.8984 26.1719-71.1426 25.3906-70.6055C21.1914-67.627 17.5781-64.0137 14.6484-59.8145C14.1113-59.0332 14.3066-58.0566 15.1367-57.5684L25.1465-51.8555C25.8301-51.4648 26.5625-51.6113 27.0508-52.2949Z"/>
|
||||
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M21.582-38.5254C21.9238-41.3574 22.6562-44.0918 23.7305-46.582C24.0723-47.3145 23.8281-48.0957 23.1445-48.4863L13.1348-54.248C12.3047-54.7363 11.377-54.4434 10.9863-53.6133C8.83789-49.0234 7.42188-44.0918 6.98242-38.8184C6.93359-37.8906 7.56836-37.207 8.54492-37.207L20.166-37.207C20.9473-37.207 21.5332-37.7441 21.582-38.5254Z"/>
|
||||
<path class="monochrome-3 multicolor-3:tintColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M23.7305-23.877C22.6562-26.3672 21.9238-29.1016 21.582-31.9824C21.5332-32.7637 20.9473-33.3008 20.166-33.3008L8.49609-33.3008C7.56836-33.3008 6.88477-32.6172 6.98242-31.6895C7.42188-26.3672 8.83789-21.3867 11.0352-16.7969C11.4258-15.9668 12.3047-15.6738 13.1348-16.1621L23.1445-21.9727C23.8281-22.3633 24.0723-23.1445 23.7305-23.877Z"/>
|
||||
<path class="monochrome-4 multicolor-4:tintColor hierarchical-4:secondary SFSymbolsPreviewWireframe" d="M32.959-12.2559C30.7129-13.916 28.7109-15.8691 27.0508-18.1641C26.5625-18.7988 25.7812-18.9941 25.0977-18.5547L15.1367-12.8906C14.3066-12.4023 14.1113-11.4258 14.5996-10.6445C17.5781-6.44531 21.1914-2.83203 25.4395 0.146484C26.2207 0.683594 27.1973 0.439453 27.6367-0.341797L33.3496-10.3516C33.7402-11.0352 33.5938-11.7676 32.959-12.2559Z"/>
|
||||
<path class="monochrome-5 multicolor-5:tintColor hierarchical-5:tertiary SFSymbolsPreviewWireframe" d="M46.7285-6.78711C43.8965-7.12891 41.1621-7.86133 38.6719-8.93555C37.9395-9.27734 37.1582-9.0332 36.7188-8.34961L31.0059 1.61133C30.5664 2.39258 30.8105 3.36914 31.6406 3.75977C36.1816 5.9082 41.1621 7.37305 46.4355 7.8125C47.3633 7.86133 48.0469 7.17773 48.0469 6.25L48.0469-5.37109C48.0469-6.15234 47.5098-6.73828 46.7285-6.78711Z"/>
|
||||
<path class="monochrome-6 multicolor-6:tintColor hierarchical-6:primary SFSymbolsPreviewWireframe" d="M61.377-8.93555C58.8867-7.86133 56.1523-7.12891 53.2715-6.78711C52.4414-6.73828 51.9043-6.15234 51.9043-5.37109L51.9043 6.25C51.9043 7.22656 52.5879 7.91016 53.5645 7.8125C58.7891 7.37305 63.8672 5.9082 68.4082 3.75977C69.2383 3.36914 69.4824 2.39258 69.043 1.61133L63.2812-8.34961C62.8906-9.0332 62.1094-9.27734 61.377-8.93555Z"/>
|
||||
<path class="monochrome-7 multicolor-7:tintColor hierarchical-7:primary SFSymbolsPreviewWireframe" d="M72.998-18.1641C71.3379-15.918 69.2871-13.9648 67.041-12.2559C66.4062-11.7676 66.2109-10.9863 66.6016-10.3516L72.3633-0.390625C72.8027 0.390625 73.7793 0.683594 74.5605 0.146484C78.8086-2.83203 82.4219-6.49414 85.3516-10.6445C85.8887-11.4258 85.6445-12.4023 84.8633-12.8906L74.9023-18.6035C74.2676-19.043 73.4863-18.7988 72.998-18.1641Z"/>
|
||||
<path class="monochrome-8 multicolor-8:tintColor hierarchical-8:primary SFSymbolsPreviewWireframe" d="M78.418-31.9824C78.0762-29.1016 77.3438-26.3672 76.2207-23.877C75.9277-23.1445 76.1719-22.4121 76.8555-22.0215L86.8164-16.2598C87.6465-15.7715 88.623-16.0156 89.0137-16.8945C91.1621-21.4844 92.5293-26.416 92.9688-31.6895C93.0176-32.6172 92.3828-33.3008 91.4551-33.3008L79.8828-33.3008C79.1016-33.3008 78.4668-32.7637 78.418-31.9824Z"/>
|
||||
<path class="monochrome-9 multicolor-9:tintColor hierarchical-9:primary SFSymbolsPreviewWireframe" d="M76.2207-46.6309C77.2949-44.0918 78.0762-41.3574 78.418-38.5254C78.4668-37.7441 79.1016-37.207 79.8828-37.207L91.4551-37.207C92.3828-37.207 93.0176-37.8906 92.9688-38.8184C92.5293-44.043 91.1621-49.0723 89.0137-53.6133C88.623-54.4434 87.5977-54.7363 86.8164-54.248L76.8066-48.4375C76.1719-48.0469 75.9277-47.3145 76.2207-46.6309Z"/>
|
||||
<path class="monochrome-10 multicolor-10:tintColor hierarchical-10:primary SFSymbolsPreviewWireframe" d="M67.041-58.2031C69.2871-56.543 71.2891-54.5898 72.998-52.2949C73.4863-51.6602 74.2676-51.416 74.9023-51.8555L84.8633-57.5684C85.6934-58.0566 85.8887-59.0332 85.4004-59.8145C82.4219-64.0137 78.7598-67.6758 74.5605-70.6055C73.7793-71.1426 72.8027-70.8984 72.3633-70.1172L66.6016-60.1562C66.2109-59.4727 66.3574-58.6914 67.041-58.2031Z"/>
|
||||
<path class="monochrome-11 multicolor-11:tintColor hierarchical-11:primary SFSymbolsPreviewWireframe" d="M53.2715-63.6719C56.1523-63.3789 58.8379-62.5977 61.377-61.4746C62.1094-61.1816 62.8906-61.4258 63.2812-62.1094L69.043-72.0703C69.4824-72.8516 69.1895-73.8281 68.3594-74.2188C63.7695-76.3672 58.8379-77.832 53.5645-78.2715C52.6367-78.3203 51.9043-77.6367 51.9043-76.709L51.9043-65.0879C51.9043-64.3066 52.4414-63.7207 53.2715-63.6719Z"/>
|
||||
</g>
|
||||
<g id="Ultralight-S" transform="matrix(1 0 0 1 511.779 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:primary SFSymbolsPreviewWireframe" d="M36.5342-61.1113C39.1187-62.2344 41.895-63.0122 44.7759-63.354C45.6934-63.4482 46.3213-64.1704 46.3213-65.0425L46.3213-74.4839C46.3213-75.5479 45.5469-76.3257 44.4829-76.228C39.4365-75.834 34.6831-74.5054 30.3657-72.4024C29.3994-71.9663 29.1099-70.8989 29.6401-69.9815L34.354-61.7915C34.7935-61.0171 35.6655-60.773 36.5342-61.1113Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M25.0982-51.9771C26.8037-54.2686 28.8477-56.3159 31.1426-58.0669C31.9136-58.6006 32.1055-59.5181 31.6694-60.2925L26.9522-68.437C26.4219-69.3545 25.3545-69.5986 24.4824-69.0161C20.4194-66.2192 16.9424-62.6514 14.1489-58.6338C13.5664-57.7617 13.8071-56.6943 14.728-56.1607L22.876-51.4468C23.6504-50.9653 24.5645-51.2026 25.0982-51.9771Z"/>
|
||||
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M19.7656-38.3438C20.062-41.2666 20.8399-44.001 22.0049-46.582C22.3467-47.4053 22.1026-48.2773 21.2827-48.7588L13.1348-53.4761C12.2139-54.0098 11.1499-53.6714 10.7139-52.7051C8.70166-48.3877 7.37647-43.6831 6.93701-38.6821C6.88818-37.6182 7.65918-36.7983 8.72656-36.7983L18.1226-36.7983C19.0401-36.7983 19.6714-37.4263 19.7656-38.3438Z"/>
|
||||
<path class="monochrome-3 multicolor-3:tintColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M22.0049-23.8315C20.8853-26.4126 20.1528-29.1924 19.811-32.1187C19.7168-33.0361 18.9946-33.6641 18.1226-33.6641L8.67773-33.6641C7.61377-33.6641 6.83936-32.8896 6.93701-31.8257C7.33106-26.7759 8.65625-22.0225 10.7173-17.6597C11.1533-16.6934 12.2593-16.355 13.1802-16.8887L21.3281-21.6548C22.148-22.1362 22.3921-23.0083 22.0049-23.8315Z"/>
|
||||
<path class="monochrome-4 multicolor-4:tintColor hierarchical-4:secondary SFSymbolsPreviewWireframe" d="M31.188-12.3921C28.8965-14.0977 26.8491-16.1416 25.1436-18.4365C24.6099-19.1621 23.7378-19.4028 22.918-18.918L14.7734-14.2529C13.8525-13.7192 13.5664-12.6518 14.1455-11.7798C16.9424-7.7622 20.4648-4.28514 24.4858-1.44286C25.3579-0.81493 26.4253-1.05907 26.9556-2.02196L31.6694-10.1699C32.1055-10.9443 31.9136-11.8584 31.188-12.3921Z"/>
|
||||
<path class="monochrome-5 multicolor-5:tintColor hierarchical-5:tertiary SFSymbolsPreviewWireframe" d="M44.7759-7.10498C41.8985-7.40137 39.1641-8.1792 36.583-9.29883C35.7598-9.68603 34.8423-9.39648 34.4029-8.62207L29.689-0.432113C29.1587 0.485367 29.4483 1.55275 30.4146 1.98878C34.6831 4.0464 39.4365 5.37502 44.4375 5.81447C45.5015 5.8633 46.3213 5.08888 46.3213 4.02492L46.3213-5.4165C46.3213-6.28857 45.6934-7.01074 44.7759-7.10498Z"/>
|
||||
<path class="monochrome-6 multicolor-6:tintColor hierarchical-6:primary SFSymbolsPreviewWireframe" d="M59.2881-9.34424C56.7525-8.22461 53.9727-7.44677 51.0464-7.10498C50.1255-7.01074 49.4976-6.28857 49.4976-5.4165L49.4976 4.07033C49.4976 5.13771 50.272 5.91213 51.3394 5.81447C56.3823 5.42043 61.1426 4.09181 65.502 1.98878C66.4683 1.55275 66.7578 0.485367 66.2276-0.432113L61.4649-8.62207C61.0288-9.39648 60.1113-9.68603 59.2881-9.34424Z"/>
|
||||
<path class="monochrome-7 multicolor-7:tintColor hierarchical-7:primary SFSymbolsPreviewWireframe" d="M70.7276-18.4819C69.022-16.1904 66.9712-14.1465 64.6343-12.3921C63.9087-11.8584 63.7134-10.9409 64.1494-10.1699L68.9121-2.02538C69.4424-1.06249 70.5098-0.81493 71.3819-1.44286C75.4483-4.23974 78.9253-7.76561 81.7188-11.7798C82.3013-12.6518 82.0572-13.7192 81.1397-14.2529L72.9497-18.9668C72.1333-19.4517 71.2612-19.2075 70.7276-18.4819Z"/>
|
||||
<path class="monochrome-8 multicolor-8:tintColor hierarchical-8:primary SFSymbolsPreviewWireframe" d="M76.0567-32.1187C75.7603-29.1924 74.9824-26.4126 73.814-23.8315C73.4756-23.0083 73.7197-22.1397 74.5396-21.6582L82.6841-16.9409C83.605-16.4072 84.6724-16.6968 85.1084-17.6665C87.166-21.9839 88.4878-26.7339 88.9273-31.7803C88.9761-32.8442 88.2051-33.6641 87.1411-33.6641L77.7031-33.6641C76.7857-33.6641 76.1509-33.0361 76.0567-32.1187Z"/>
|
||||
<path class="monochrome-9 multicolor-9:tintColor hierarchical-9:primary SFSymbolsPreviewWireframe" d="M73.814-46.5854C74.9336-44.001 75.7149-41.2212 76.0112-38.3438C76.1509-37.4263 76.7857-36.7983 77.7031-36.7983L87.1866-36.7983C88.2505-36.7983 89.0215-37.5728 88.9273-38.6367C88.5332-43.6797 87.2115-48.4365 85.1084-52.7959C84.6724-53.7622 83.6016-54.0552 82.6841-53.5215L74.4907-48.7554C73.6743-48.2739 73.4756-47.4507 73.814-46.5854Z"/>
|
||||
<path class="monochrome-10 multicolor-10:tintColor hierarchical-10:primary SFSymbolsPreviewWireframe" d="M64.6797-58.0215C66.9712-56.3159 69.0186-54.272 70.7276-51.9771C71.2612-51.2515 72.1333-51.0073 72.9497-51.4922L81.0943-56.1607C82.0152-56.6943 82.3013-57.7617 81.7222-58.6338C78.9707-62.6514 75.4449-66.1318 71.4273-68.9707C70.5552-69.5986 69.4424-69.3545 68.9121-68.3916L64.1948-60.2471C63.7588-59.4727 63.9507-58.5552 64.6797-58.0215Z"/>
|
||||
<path class="monochrome-11 multicolor-11:tintColor hierarchical-11:primary SFSymbolsPreviewWireframe" d="M51.0464-63.354C53.9727-63.061 56.7036-62.2798 59.2881-61.1113C60.1113-60.773 61.0288-61.0625 61.4649-61.8369L66.2276-69.9815C66.7578-70.8989 66.4195-71.9663 65.4531-72.4024C61.1358-74.46 56.3858-75.7886 51.3848-76.228C50.3208-76.2769 49.4976-75.5025 49.4976-74.4385L49.4976-65.0425C49.4976-64.1704 50.1255-63.4482 51.0464-63.354Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 33 KiB |
47
Meshtastic/Enums/KeyBackupStatus.swift
Normal file
47
Meshtastic/Enums/KeyBackupStatus.swift
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// iCloudStats.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 6/18/25.
|
||||
//
|
||||
|
||||
enum KeyBackupStatus: String, CaseIterable, Equatable, Decodable {
|
||||
case saved
|
||||
case restored
|
||||
case deleted
|
||||
case saveFailed
|
||||
case restoreFailed
|
||||
case deleteFailed
|
||||
var description: String {
|
||||
switch self {
|
||||
case .saved:
|
||||
return "Private Key saved successfully to iCloud keychain.".localized
|
||||
case .restored:
|
||||
return "Private Key restored successfully from iCloud keychain.".localized
|
||||
case .deleted:
|
||||
return "Private Key deleted successfully from iCloud keychain.".localized
|
||||
case .saveFailed:
|
||||
return "Private Key failed to save to iCloud keychain.".localized
|
||||
case .restoreFailed:
|
||||
return "Private Key value not found in iCloud keychain.".localized
|
||||
case .deleteFailed:
|
||||
return "Private Key failed to delete from iCloud keychain.".localized
|
||||
}
|
||||
}
|
||||
var success: Bool {
|
||||
switch self {
|
||||
case .saved:
|
||||
return true
|
||||
case .restored:
|
||||
return true
|
||||
case .deleted:
|
||||
return true
|
||||
case .saveFailed:
|
||||
return false
|
||||
case .restoreFailed:
|
||||
return false
|
||||
case .deleteFailed:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,17 +37,17 @@ enum ActivityType: Int, CaseIterable, Identifiable {
|
|||
var fileNameString: String {
|
||||
switch self {
|
||||
case .walking:
|
||||
return "walk".localized
|
||||
return "Walking".localized.lowercased()
|
||||
case .hiking:
|
||||
return "hiking".localized
|
||||
return "Hiking".localized.lowercased()
|
||||
case .biking:
|
||||
return "biking".localized
|
||||
return "Biking".localized.lowercased()
|
||||
case .driving:
|
||||
return "driving".localized
|
||||
return "Driving".localized.lowercased()
|
||||
case .overlanding:
|
||||
return "overlanding".localized
|
||||
return "Overlanding".localized.lowercased()
|
||||
case .skiing:
|
||||
return "skiing".localized
|
||||
return "Skiing".localized.lowercased()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ enum SerialTimeoutIntervals: Int, CaseIterable, Identifiable {
|
|||
case .tenSeconds:
|
||||
return "Ten Seconds".localized
|
||||
case .fifteenSeconds:
|
||||
return "Thirty Seconds".localized
|
||||
return "Fifteen Seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "Thirty Seconds".localized
|
||||
case .oneMinute:
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ extension ChannelEntity {
|
|||
channel.settings.psk = self.psk ?? Data()
|
||||
channel.role = Channel.Role(rawValue: Int(self.role)) ?? Channel.Role.secondary
|
||||
channel.settings.moduleSettings.positionPrecision = UInt32(self.positionPrecision)
|
||||
channel.settings.moduleSettings.isClientMuted = self.mute
|
||||
return channel
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,4 +31,11 @@ extension MessageEntity {
|
|||
|
||||
return (try? context.fetch(fetchRequest)) ?? [MessageEntity]()
|
||||
}
|
||||
|
||||
func displayTimestamp(aboveMessage: MessageEntity?) -> Bool {
|
||||
if let aboveMessage = aboveMessage {
|
||||
return aboveMessage.timestamp.addingTimeInterval(3600) < timestamp // 60 minutes
|
||||
}
|
||||
return false // First message will have no timestamp
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
// NodeInfoEntityToNodeInfo.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Utility to convert NodeInfoEntity (Core Data) to NodeInfo (protobuf)
|
||||
|
||||
import Foundation
|
||||
import MeshtasticProtobufs
|
||||
|
||||
extension NodeInfoEntity {
|
||||
func toProto() -> NodeInfo {
|
||||
var userProto = User()
|
||||
if let user = self.user {
|
||||
userProto.id = user.userId ?? ""
|
||||
userProto.longName = user.longName ?? ""
|
||||
userProto.shortName = user.shortName ?? ""
|
||||
userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unset
|
||||
userProto.isLicensed = user.isLicensed
|
||||
if userProto.hasIsUnmessagable == true {
|
||||
userProto.isUnmessagable = user.unmessagable
|
||||
}
|
||||
userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .client
|
||||
userProto.publicKey = user.publicKey?.subdata(in: 0..<user.publicKey!.count) ?? Data()
|
||||
}
|
||||
var node = NodeInfo()
|
||||
node.num = UInt32(self.num)
|
||||
node.user = userProto
|
||||
// Add more fields as needed
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
|
@ -106,14 +106,42 @@ extension UserEntity {
|
|||
}
|
||||
}
|
||||
}
|
||||
public func createUser(num: Int64, context: NSManagedObjectContext) -> UserEntity {
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.num = Int64(num)
|
||||
let userId = String(format: "%2X", num)
|
||||
newUser.userId = "!\(userId)"
|
||||
let last4 = String(userId.suffix(4))
|
||||
newUser.longName = "Meshtastic \(last4)"
|
||||
newUser.shortName = last4
|
||||
newUser.hwModel = "UNSET"
|
||||
|
||||
public func createUser(num: Int64, context: NSManagedObjectContext) throws -> UserEntity {
|
||||
// Validate Input
|
||||
guard num >= 0 else {
|
||||
throw CoreDataError.invalidInput(message: "User number cannot be negative.")
|
||||
}
|
||||
|
||||
var newUser: UserEntity! // Use an implicitly unwrapped optional, but ensure it's assigned
|
||||
|
||||
context.performAndWait {
|
||||
newUser = UserEntity(context: context)
|
||||
newUser.num = num
|
||||
let userId = num.toHex()
|
||||
newUser.userId = userId
|
||||
let last4 = String(userId.suffix(4))
|
||||
newUser.longName = "Meshtastic \(last4)"
|
||||
newUser.shortName = last4
|
||||
newUser.hwModel = "UNSET"
|
||||
}
|
||||
|
||||
return newUser
|
||||
}
|
||||
|
||||
enum CoreDataError: Error, LocalizedError {
|
||||
case invalidInput(message: String)
|
||||
case saveFailed(message: String)
|
||||
case entityCreationFailed(message: String) // In case UserEntity(context:) fails for some reason
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidInput(let message):
|
||||
return "Core Data Input Error: \(message)"
|
||||
case .saveFailed(let message):
|
||||
return "Core Data Save Error: \(message)"
|
||||
case .entityCreationFailed(let message):
|
||||
return "Core Data Entity Creation Error: \(message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ extension WaypointEntity {
|
|||
static func allWaypointssFetchRequest() -> NSFetchRequest<WaypointEntity> {
|
||||
let request: NSFetchRequest<WaypointEntity> = WaypointEntity.fetchRequest()
|
||||
request.fetchLimit = 50
|
||||
// request.fetchBatchSize = 1
|
||||
// request.returnsObjectsAsFaults = false
|
||||
// request.includesSubentities = true
|
||||
request.returnsDistinctResults = true
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)]
|
||||
request.predicate = NSPredicate(format: "expire == nil || expire >= %@", Date() as NSDate)
|
||||
|
|
@ -24,7 +21,6 @@ extension WaypointEntity {
|
|||
}
|
||||
|
||||
var latitude: Double? {
|
||||
|
||||
let d = Double(latitudeI)
|
||||
if d == 0 {
|
||||
return 0
|
||||
|
|
@ -33,7 +29,6 @@ extension WaypointEntity {
|
|||
}
|
||||
|
||||
var longitude: Double? {
|
||||
|
||||
let d = Double(longitudeI)
|
||||
if d == 0 {
|
||||
return 0
|
||||
|
|
@ -46,7 +41,7 @@ extension WaypointEntity {
|
|||
let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
|
||||
return coord
|
||||
} else {
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,16 +55,29 @@ extension WaypointEntity {
|
|||
}
|
||||
|
||||
extension WaypointEntity: MKAnnotation {
|
||||
public var coordinate: CLLocationCoordinate2D { waypointCoordinate ?? LocationsHandler.DefaultLocation }
|
||||
public var title: String? { name ?? "Dropped Pin" }
|
||||
@MainActor
|
||||
public var coordinate: CLLocationCoordinate2D {
|
||||
get {
|
||||
waypointCoordinate ?? LocationsHandler.DefaultLocation
|
||||
}
|
||||
set {
|
||||
latitudeI = Int32(newValue.latitude * 1e7)
|
||||
longitudeI = Int32(newValue.longitude * 1e7)
|
||||
}
|
||||
}
|
||||
|
||||
public var title: String? {
|
||||
name ?? "Dropped Pin"
|
||||
}
|
||||
|
||||
public var subtitle: String? {
|
||||
(longDescription ?? "") +
|
||||
String(expire != nil ? "\n⌛ Expires \(String(describing: expire?.formatted()))" : "") +
|
||||
String(locked > 0 ? "\n🔒 Locked" : "") }
|
||||
String(locked > 0 ? "\n🔒 Locked" : "")
|
||||
}
|
||||
}
|
||||
|
||||
struct WaypointCoordinate: Identifiable {
|
||||
|
||||
let id: UUID
|
||||
let coordinate: CLLocationCoordinate2D?
|
||||
let waypointId: Int64
|
||||
|
|
|
|||
|
|
@ -115,6 +115,17 @@ extension String {
|
|||
.joined()
|
||||
}
|
||||
|
||||
/// Formats a short name like "P130" to read as "Node P 130" for VoiceOver
|
||||
/// This ensures proper pronunciation of alphanumeric node IDs
|
||||
func formatNodeNameForVoiceOver() -> String {
|
||||
let spaced = self.replacingOccurrences(
|
||||
of: #"([A-Za-z])([0-9]+)"#,
|
||||
with: "$1 $2",
|
||||
options: .regularExpression
|
||||
)
|
||||
return "Node".localized + " " + spaced
|
||||
}
|
||||
|
||||
// Adds variation selectors to prefer the graphical form of emoji.
|
||||
// Looks ahead to make sure that the variation selector is not already applied.
|
||||
var addingVariationSelectors: String {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ extension UserDefaults {
|
|||
case enableMapTraffic
|
||||
case enableMapPointsOfInterest
|
||||
case enableOfflineMaps
|
||||
case enableMapShowFavorites
|
||||
case mapTileServer
|
||||
case enableOverlayServer
|
||||
case mapOverlayServer
|
||||
|
|
@ -75,6 +76,7 @@ extension UserDefaults {
|
|||
case mapReportingOptIn
|
||||
case firstLaunch
|
||||
case showDeviceOnboarding
|
||||
case usageDataAndCrashReporting
|
||||
case testIntEnum
|
||||
}
|
||||
|
||||
|
|
@ -121,6 +123,9 @@ extension UserDefaults {
|
|||
@UserDefault(.enableMapPointsOfInterest, defaultValue: false)
|
||||
static var enableMapPointsOfInterest: Bool
|
||||
|
||||
@UserDefault(.enableMapShowFavorites, defaultValue: false)
|
||||
static var enableMapShowFavorites: Bool
|
||||
|
||||
@UserDefault(.enableDetectionNotifications, defaultValue: false)
|
||||
static var enableDetectionNotifications: Bool
|
||||
|
||||
|
|
@ -154,6 +159,8 @@ extension UserDefaults {
|
|||
@UserDefault(.mapReportingOptIn, defaultValue: false)
|
||||
static var mapReportingOptIn: Bool
|
||||
|
||||
@UserDefault(.usageDataAndCrashReporting, defaultValue: true)
|
||||
static var usageDataAndCrashReporting: Bool
|
||||
@UserDefault(.firstLaunch, defaultValue: true)
|
||||
static var firstLaunch: Bool
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Meshtastic/Helpers/KeychainHelper.swift
Normal file
66
Meshtastic/Helpers/KeychainHelper.swift
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// KeychainHelper.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 6/17/25.
|
||||
//
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
class KeychainHelper {
|
||||
|
||||
static let standard = KeychainHelper()
|
||||
|
||||
private init() {}
|
||||
|
||||
func save(key: String, value: String, service: String = Bundle.main.bundleIdentifier!) -> OSStatus {
|
||||
let data = value.data(using: .utf8)!
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecValueData as String: data,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue!,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
|
||||
]
|
||||
|
||||
SecItemDelete(query as CFDictionary) // Delete existing item if any
|
||||
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
return status
|
||||
}
|
||||
|
||||
func read(key: String, service: String = Bundle.main.bundleIdentifier!) -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecReturnData as String: kCFBooleanTrue,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue!
|
||||
]
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
|
||||
if status == errSecSuccess {
|
||||
if let data = item as? Data {
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func delete(key: String, service: String = Bundle.main.bundleIdentifier!) -> OSStatus {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecAttrSynchronizable as String: kCFBooleanTrue!
|
||||
]
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
|
@ -70,8 +70,8 @@ class LocalNotificationManager {
|
|||
if notification.critical {
|
||||
content.sound = UNNotificationSound.defaultCritical
|
||||
}
|
||||
|
||||
let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: nil)
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger)
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error {
|
||||
|
|
|
|||
|
|
@ -123,15 +123,42 @@ import OSLog
|
|||
} else {
|
||||
locationsArray = [location]
|
||||
}
|
||||
UserDefaults.standard.set(location.coordinate.latitude, forKey: "lastKnownLatitude")
|
||||
UserDefaults.standard.set(location.coordinate.longitude, forKey: "lastKnownLongitude")
|
||||
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastKnownLocationTimestamp")
|
||||
return true
|
||||
}
|
||||
|
||||
static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090)
|
||||
static var currentLocation: CLLocationCoordinate2D {
|
||||
guard let location = shared.manager.location else {
|
||||
if let location = shared.manager.location {
|
||||
return location.coordinate
|
||||
} else {
|
||||
// Check authorization status
|
||||
let status = shared.manager.authorizationStatus
|
||||
switch status {
|
||||
case .notDetermined:
|
||||
Logger.services.info("📍 [App] Location permission not determined, requesting authorization")
|
||||
shared.manager.requestWhenInUseAuthorization()
|
||||
case .denied, .restricted:
|
||||
Logger.services.warning("📍 [App] Location access denied or restricted. Please enable location services in Settings to get accurate positioning!")
|
||||
shared.manager.requestWhenInUseAuthorization()
|
||||
default:
|
||||
break
|
||||
}
|
||||
// Fallback 1: Last known location from UserDefaults (if within 4 hours)
|
||||
if let lat = UserDefaults.standard.object(forKey: "lastKnownLatitude") as? Double,
|
||||
let lon = UserDefaults.standard.object(forKey: "lastKnownLongitude") as? Double,
|
||||
let timestamp = UserDefaults.standard.object(forKey: "lastKnownLocationTimestamp") as? Double,
|
||||
lat >= -90 && lat <= 90,
|
||||
lon >= -180 && lon <= 180,
|
||||
Date().timeIntervalSince1970 - timestamp <= 14_400 { // 4 hours in seconds
|
||||
Logger.services.info("📍 [App] Falling back to last known location (age: \(Int(Date().timeIntervalSince1970 - timestamp)) seconds)")
|
||||
return CLLocationCoordinate2D(latitude: lat, longitude: lon)
|
||||
}
|
||||
// Fallback 2: Default location
|
||||
Logger.services.warning("📍 [App] No Location and no last known location, something is really wrong. Teleporting user to Apple Park")
|
||||
return DefaultLocation
|
||||
}
|
||||
return location.coordinate
|
||||
}
|
||||
|
||||
static var satsInView: Int {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import OSLog
|
|||
import ActivityKit
|
||||
#endif
|
||||
|
||||
// Simple extension to consicely pass values through a has_XXX boolean check
|
||||
// Simple extension to concisely pass values through a has_XXX boolean check
|
||||
fileprivate extension Bool {
|
||||
func then<T>(_ value: T) -> T? {
|
||||
self ? value : nil
|
||||
|
|
@ -185,9 +185,6 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
|
|||
mutableChannels.add(newChannel)
|
||||
}
|
||||
fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet
|
||||
if newChannel.name?.lowercased() == "admin" {
|
||||
fetchedMyInfo[0].adminIndex = newChannel.index
|
||||
}
|
||||
context.refresh(newChannel, mergeChanges: true)
|
||||
do {
|
||||
try context.save()
|
||||
|
|
@ -292,14 +289,18 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
newTelemetries.append(telemetry)
|
||||
newNode.telemetries? = NSOrderedSet(array: newTelemetries)
|
||||
}
|
||||
|
||||
newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
|
||||
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
|
||||
if nodeInfo.lastHeard > 0 {
|
||||
newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
|
||||
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
|
||||
} else {
|
||||
newNode.firstHeard = Date()
|
||||
newNode.lastHeard = Date()
|
||||
}
|
||||
newNode.snr = nodeInfo.snr
|
||||
if nodeInfo.hasUser {
|
||||
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.userId = nodeInfo.user.id
|
||||
newUser.userId = nodeInfo.num.toHex()
|
||||
newUser.num = Int64(nodeInfo.num)
|
||||
newUser.longName = nodeInfo.user.longName
|
||||
newUser.shortName = nodeInfo.user.shortName
|
||||
|
|
@ -317,10 +318,27 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
newUser.pkiEncrypted = true
|
||||
newUser.publicKey = nodeInfo.user.publicKey
|
||||
}
|
||||
/// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default
|
||||
if nodeInfo.user.hasIsUnmessagable {
|
||||
newUser.unmessagable = nodeInfo.user.isUnmessagable
|
||||
} else {
|
||||
let roles = [2, 4, 5, 6, 7, 10, 11]
|
||||
let containsRole = roles.contains(Int(newUser.role))
|
||||
if containsRole {
|
||||
newUser.unmessagable = true
|
||||
} else {
|
||||
newUser.unmessagable = false
|
||||
}}
|
||||
newNode.user = newUser
|
||||
} else if nodeInfo.num > Constants.minimumNodeNum {
|
||||
let newUser = createUser(num: Int64(nodeInfo.num), context: context)
|
||||
newNode.user = newUser
|
||||
do {
|
||||
let newUser = try createUser(num: Int64(nodeInfo.num), context: context)
|
||||
newNode.user = newUser
|
||||
} catch CoreDataError.invalidInput(let message) {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)")
|
||||
} catch {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) {
|
||||
|
|
@ -380,26 +398,43 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
fetchedNode[0].user?.pkiEncrypted = true
|
||||
fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey
|
||||
}
|
||||
fetchedNode[0].user!.userId = nodeInfo.user.id
|
||||
fetchedNode[0].user!.num = Int64(nodeInfo.num)
|
||||
fetchedNode[0].user!.numString = String(nodeInfo.num)
|
||||
fetchedNode[0].user!.longName = nodeInfo.user.longName
|
||||
fetchedNode[0].user!.shortName = nodeInfo.user.shortName
|
||||
fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed
|
||||
fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue)
|
||||
fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
|
||||
fetchedNode[0].user!.hwModelId = Int32(nodeInfo.user.hwModel.rawValue)
|
||||
fetchedNode[0].user?.userId = nodeInfo.num.toHex()
|
||||
fetchedNode[0].user?.num = Int64(nodeInfo.num)
|
||||
fetchedNode[0].user?.numString = String(nodeInfo.num)
|
||||
fetchedNode[0].user?.longName = nodeInfo.user.longName
|
||||
fetchedNode[0].user?.shortName = nodeInfo.user.shortName
|
||||
fetchedNode[0].user?.isLicensed = nodeInfo.user.isLicensed
|
||||
fetchedNode[0].user?.role = Int32(nodeInfo.user.role.rawValue)
|
||||
fetchedNode[0].user?.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
|
||||
fetchedNode[0].user?.hwModelId = Int32(nodeInfo.user.hwModel.rawValue)
|
||||
/// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default
|
||||
if nodeInfo.user.hasIsUnmessagable {
|
||||
fetchedNode[0].user?.unmessagable = nodeInfo.user.isUnmessagable
|
||||
} else {
|
||||
let roles = [-1, 2, 4, 5, 6, 7, 10, 11]
|
||||
let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1))
|
||||
if containsRole {
|
||||
fetchedNode[0].user?.unmessagable = true
|
||||
} else {
|
||||
fetchedNode[0].user?.unmessagable = false
|
||||
}
|
||||
}
|
||||
Task {
|
||||
Api().loadDeviceHardwareData { (hw) in
|
||||
let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId })
|
||||
fetchedNode[0].user!.hwDisplayName = dh?.displayName
|
||||
fetchedNode[0].user?.hwDisplayName = dh?.displayName
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum {
|
||||
|
||||
let newUser = createUser(num: Int64(nodeInfo.num), context: context)
|
||||
fetchedNode[0].user = newUser
|
||||
do {
|
||||
let newUser = try createUser(num: Int64(nodeInfo.num), context: context)
|
||||
fetchedNode[0].user = newUser
|
||||
} catch CoreDataError.invalidInput(let message) {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)")
|
||||
} catch {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -469,9 +504,6 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) {
|
||||
|
||||
if let cmmc = try? CannedMessageModuleConfig(serializedBytes: packet.decoded.payload) {
|
||||
|
||||
if !cmmc.messages.isEmpty {
|
||||
|
||||
let logString = String.localizedStringWithFormat("Canned Messages Messages Received For: %@".localized, packet.from.toHex())
|
||||
Logger.mesh.info("🥫 \(logString, privacy: .public)")
|
||||
|
||||
|
|
@ -485,10 +517,11 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
.replacingOccurrences(of: "11: ", with: "")
|
||||
.replacingOccurrences(of: "\"", with: "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.components(separatedBy: "\n").first ?? ""
|
||||
fetchedNode[0].cannedMessageConfig?.messages = messages
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num.toHex(), privacy: .public)")
|
||||
Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex() ?? "Unknown".localized, privacy: .public)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
|
|
@ -498,7 +531,6 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
} catch {
|
||||
Logger.data.error("💥 Error Deserializing ADMIN_APP packet.")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) {
|
||||
channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context)
|
||||
|
|
@ -688,7 +720,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) {
|
||||
let logString = String.localizedStringWithFormat("Telemetry received for: %@".localized, String(packet.from))
|
||||
Logger.mesh.info("📈 \(logString, privacy: .public)")
|
||||
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
|
||||
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
|
||||
/// Other unhandled telemetry packets
|
||||
return
|
||||
}
|
||||
|
|
@ -909,7 +941,14 @@ func textMessageAppPacket(
|
|||
// For S&F broadcast messages, treat as a channel message (not a DM)
|
||||
newMessage.toUser = nil
|
||||
} else {
|
||||
newMessage.toUser = createUser(num: Int64(truncatingIfNeeded: packet.to), context: context)
|
||||
do {
|
||||
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.to), context: context)
|
||||
newMessage.toUser = newUser
|
||||
} catch CoreDataError.invalidInput(let message) {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.to, privacy: .public) Error: \(message, privacy: .public)")
|
||||
} catch {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.to, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
if fetchedUsers.first(where: { $0.num == packet.from }) != nil {
|
||||
|
|
@ -939,7 +978,14 @@ func textMessageAppPacket(
|
|||
}
|
||||
} else {
|
||||
/// Make a new from user if they are unknown
|
||||
newMessage.fromUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
|
||||
do {
|
||||
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
|
||||
newMessage.fromUser = newUser
|
||||
} catch CoreDataError.invalidInput(let message) {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
|
||||
} catch {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
if packet.rxTime > 0 {
|
||||
newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
|
||||
|
|
@ -956,79 +1002,79 @@ func textMessageAppPacket(
|
|||
try context.save()
|
||||
Logger.data.info("💾 Saved a new message for \(newMessage.messageId, privacy: .public)")
|
||||
messageSaved = true
|
||||
|
||||
if messageSaved {
|
||||
if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications {
|
||||
return
|
||||
}
|
||||
if newMessage.fromUser != nil && newMessage.toUser != nil {
|
||||
// Set Unread Message Indicators
|
||||
if packet.to == connectedNode {
|
||||
appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0
|
||||
}
|
||||
if !(newMessage.fromUser?.mute ?? false) {
|
||||
// Create an iOS Notification for the received DM message
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)",
|
||||
messageId: newMessage.messageId,
|
||||
channel: newMessage.channel,
|
||||
userNum: Int64(packet.from),
|
||||
critical: critical
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
|
||||
}
|
||||
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
|
||||
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode))
|
||||
do {
|
||||
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
|
||||
if !fetchedMyInfo.isEmpty {
|
||||
appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages
|
||||
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
|
||||
if channel.index == newMessage.channel {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
}
|
||||
if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications {
|
||||
// Create an iOS Notification for the received channel message
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)",
|
||||
messageId: newMessage.messageId,
|
||||
channel: newMessage.channel,
|
||||
userNum: Int64(newMessage.fromUser?.userId ?? "0"),
|
||||
critical: critical
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("Failed to save new MessageEntity \(nsError, privacy: .public)")
|
||||
}
|
||||
// Send notifications if the message saved properly to core data
|
||||
if messageSaved {
|
||||
if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications {
|
||||
return
|
||||
}
|
||||
if newMessage.fromUser != nil && newMessage.toUser != nil {
|
||||
// Set Unread Message Indicators
|
||||
if packet.to == connectedNode {
|
||||
appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0
|
||||
}
|
||||
if !(newMessage.fromUser?.mute ?? false) {
|
||||
// Create an iOS Notification for the received DM message
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)",
|
||||
messageId: newMessage.messageId,
|
||||
channel: newMessage.channel,
|
||||
userNum: Int64(packet.from),
|
||||
critical: critical
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
|
||||
}
|
||||
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
|
||||
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode))
|
||||
do {
|
||||
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
|
||||
if !fetchedMyInfo.isEmpty {
|
||||
appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages
|
||||
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
|
||||
if channel.index == newMessage.channel {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
}
|
||||
if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications {
|
||||
// Create an iOS Notification for the received channel message
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)",
|
||||
messageId: newMessage.messageId,
|
||||
channel: newMessage.channel,
|
||||
userNum: Int64(newMessage.fromUser?.userId ?? "0"),
|
||||
critical: critical
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.data.error("Fetch Message To and From Users Error")
|
||||
}
|
||||
|
|
@ -1040,17 +1086,29 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
let logString = String.localizedStringWithFormat("Waypoint Packet received from node: %@".localized, String(packet.from))
|
||||
Logger.mesh.info("📍 \(logString, privacy: .public)")
|
||||
|
||||
let fetchWaypointRequest = WaypointEntity.fetchRequest()
|
||||
fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(packet.id))
|
||||
|
||||
do {
|
||||
|
||||
if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) {
|
||||
let fetchedWaypoint = try context.fetch(fetchWaypointRequest)
|
||||
if fetchedWaypoint.isEmpty {
|
||||
let waypoint = WaypointEntity(context: context)
|
||||
// Fetch waypoint by waypointMessage.id, not packet.id
|
||||
let fetchWaypointRequest = WaypointEntity.fetchRequest()
|
||||
fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(waypointMessage.id))
|
||||
|
||||
waypoint.id = Int64(packet.id)
|
||||
let fetchedWaypoint = try context.fetch(fetchWaypointRequest)
|
||||
// Fetch the node info to get the short name
|
||||
var nodeShortName: String = "?"
|
||||
let fetchNodeRequest = NodeInfoEntity.fetchRequest()
|
||||
fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
|
||||
do {
|
||||
let fetchedNode = try context.fetch(fetchNodeRequest)
|
||||
if let node = fetchedNode.first, let user = node.user {
|
||||
nodeShortName = user.shortName ?? node.user?.userId ?? String(packet.from.toHex())
|
||||
}
|
||||
} catch {
|
||||
Logger.data.error("Failed to fetch NodeInfoEntity for node \(packet.from.toHex(), privacy: .public): \(error)")
|
||||
}
|
||||
if fetchedWaypoint.isEmpty {
|
||||
// Create a new waypoint
|
||||
let waypoint = WaypointEntity(context: context)
|
||||
waypoint.id = Int64(waypointMessage.id) // Use waypointMessage.id
|
||||
waypoint.name = waypointMessage.name
|
||||
waypoint.longDescription = waypointMessage.description_p
|
||||
waypoint.latitudeI = waypointMessage.latitudeI
|
||||
|
|
@ -1073,7 +1131,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(waypoint.id)"),
|
||||
title: "New Waypoint Received",
|
||||
title: "New Waypoint From \(nodeShortName)",
|
||||
subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")",
|
||||
content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")",
|
||||
target: "map",
|
||||
|
|
@ -1088,26 +1146,42 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)")
|
||||
}
|
||||
} else {
|
||||
fetchedWaypoint[0].id = Int64(packet.id)
|
||||
fetchedWaypoint[0].name = waypointMessage.name
|
||||
fetchedWaypoint[0].longDescription = waypointMessage.description_p
|
||||
fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI
|
||||
fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI
|
||||
fetchedWaypoint[0].icon = Int64(waypointMessage.icon)
|
||||
fetchedWaypoint[0].locked = Int64(waypointMessage.lockedTo)
|
||||
if waypointMessage.expire >= 1 {
|
||||
fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
|
||||
} else {
|
||||
fetchedWaypoint[0].expire = nil
|
||||
}
|
||||
fetchedWaypoint[0].lastUpdated = Date()
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("💾 Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id, privacy: .public)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)")
|
||||
// Update existing waypoint
|
||||
let existingWaypoint = fetchedWaypoint[0]
|
||||
if existingWaypoint.locked == 0 || existingWaypoint.locked == packet.from {
|
||||
let currentTime = Int64(Date().timeIntervalSince1970)
|
||||
if waypointMessage.expire > 0 && waypointMessage.expire <= currentTime {
|
||||
context.delete(existingWaypoint)
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("💾 Deleted a waypoint")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)")
|
||||
}
|
||||
} else {
|
||||
existingWaypoint.name = waypointMessage.name
|
||||
existingWaypoint.longDescription = waypointMessage.description_p
|
||||
existingWaypoint.latitudeI = waypointMessage.latitudeI
|
||||
existingWaypoint.longitudeI = waypointMessage.longitudeI
|
||||
existingWaypoint.icon = Int64(waypointMessage.icon)
|
||||
existingWaypoint.locked = Int64(waypointMessage.lockedTo)
|
||||
if waypointMessage.expire >= 1 {
|
||||
existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
|
||||
} else {
|
||||
existingWaypoint.expire = nil
|
||||
}
|
||||
existingWaypoint.lastUpdated = Date()
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("💾 Updated Node Waypoint App Packet For: \(existingWaypoint.id, privacy: .public)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,31 +41,29 @@ class MqttClientProxyManager {
|
|||
|
||||
if let host = host {
|
||||
let port = defaultServerPort
|
||||
let username = node.mqttConfig?.username
|
||||
let password = node.mqttConfig?.password
|
||||
let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh"
|
||||
let prefix = root!
|
||||
topic = prefix + "/2/e" + "/#"
|
||||
// Require opt in to map report terms to connect
|
||||
if node.mqttConfig?.mapReportingEnabled ?? false && UserDefaults.mapReportingOptIn || !(node.mqttConfig?.mapReportingEnabled ?? false) {
|
||||
connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic)
|
||||
connect(host: host, port: port, useSsl: useSsl, topic: topic, node: node)
|
||||
} else {
|
||||
delegate?.onMqttError(message: "MQTT Map Reporting Terms need to be accepted.")
|
||||
}
|
||||
}
|
||||
}
|
||||
func connect(host: String, port: Int, useSsl: Bool, username: String?, password: String?, topic: String?) {
|
||||
func connect(host: String, port: Int, useSsl: Bool, topic: String?, node: NodeInfoEntity) {
|
||||
guard !host.isEmpty else {
|
||||
delegate?.onMqttDisconnected()
|
||||
return
|
||||
}
|
||||
let clientId = "MeshtasticAppleMqttProxy-" + String(ProcessInfo().processIdentifier)
|
||||
let clientId = "MeshtasticAppleMqttProxy-" + (node.user?.userId ?? String(ProcessInfo().processIdentifier))
|
||||
mqttClientProxy = CocoaMQTT(clientID: clientId, host: host, port: UInt16(port))
|
||||
if let mqttClient = mqttClientProxy {
|
||||
mqttClient.enableSSL = useSsl
|
||||
mqttClient.allowUntrustCACertificate = true
|
||||
mqttClient.username = username
|
||||
mqttClient.password = password
|
||||
mqttClient.username = node.mqttConfig?.username
|
||||
mqttClient.password = node.mqttConfig?.password
|
||||
mqttClient.keepAlive = 60
|
||||
mqttClient.cleanSession = true
|
||||
if debugLog {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.usernotifications.critical-alerts</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:meshtastic.org/e/*</string>
|
||||
<string>applinks:meshtastic.org/v/*</string>
|
||||
</array>
|
||||
<key>com.apple.developer.carplay-communication</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.usernotifications.critical-alerts</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.weatherkit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
|
|
@ -20,7 +23,9 @@
|
|||
<true/>
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.carplay-communication</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)gvh.MeshtasticClient</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV 50.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV 53.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24D81" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788" systemVersion="24D81" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,505 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788" systemVersion="24D81" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="green" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ledState" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="red" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="ambientLightingConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceLoggingEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
|
||||
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="downlinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
|
||||
<attribute name="psk" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="triggerType" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="disableTripleClick" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="ledHeartbeatEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="nodeInfoBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tripleClickAsAdHocPing" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="tzdef" optional="YES" attributeType="String"/>
|
||||
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="excludedModules" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hwModel" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="units" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="useI2SAsBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
|
||||
</entity>
|
||||
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ignoreMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="okToMqtt" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
|
||||
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="read" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="messageId"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="mapPositionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="13" usesScalarValueType="YES"/>
|
||||
<attribute name="mapPublishIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mapReportingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
|
||||
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="deviceId" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="registered" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
|
||||
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="myNodeNum"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabledProtocols" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ntpServer" optional="YES" attributeType="String"/>
|
||||
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
|
||||
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
|
||||
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="favorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="firstHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hopsAway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ignored" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sessionExpiration" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="sessionPasskey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="viaMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
|
||||
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
|
||||
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
|
||||
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
|
||||
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
|
||||
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
|
||||
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
|
||||
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
|
||||
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
|
||||
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
|
||||
<relationship name="pax" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PaxCounterEntity" inverseName="paxNode" inverseEntity="PaxCounterEntity"/>
|
||||
<relationship name="paxCounterConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PaxCounterConfigEntity" inverseName="paxCounterConfigNode" inverseEntity="PaxCounterConfigEntity"/>
|
||||
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
|
||||
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
|
||||
<relationship name="powerConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PowerConfigEntity" inverseName="powerConfigNode" inverseEntity="PowerConfigEntity"/>
|
||||
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
|
||||
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
|
||||
<relationship name="securityConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SecurityConfigEntity" inverseName="securityConfigNode" inverseEntity="SecurityConfigEntity"/>
|
||||
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
|
||||
<relationship name="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
|
||||
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
|
||||
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
|
||||
<relationship name="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
|
||||
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="num"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PaxCounterConfigEntity" representedClassName="PaxCounterConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleThreshold" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="updateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiThreshold" optional="YES" attributeType="Integer 32" defaultValueString="-80" usesScalarValueType="YES"/>
|
||||
<relationship name="paxCounterConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="paxCounterConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PaxCounterEntity" representedClassName="PaxCounterEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ble" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="uptime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="paxNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="pax" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsEnGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="precisionBits" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PowerConfigEntity" representedClassName="PowerConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adcMultiplierOverride" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceBatteryInaAddress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isPowerSaving" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="lsSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="minWakeSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="onBatteryShutdownAfterSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="waitBluetoothSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="powerConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="powerConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="color" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="distance" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="elevationGain" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="notes" optional="YES" attributeType="String"/>
|
||||
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
|
||||
</entity>
|
||||
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
|
||||
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SecurityConfigEntity" representedClassName="SecurityConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adminChannelEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="adminKey2" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="adminKey3" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="bluetoothLoggingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="debugLogApiEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isManaged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="privateKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="serialEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="securityConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="securityConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideConsoleSerialPort" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isRouter" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeartbeat" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="lastRequest" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="powerMeasurementEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="powerScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="powerUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES">
|
||||
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="iaq" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="irLux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="lux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numOnlineNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsRx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsRxBad" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsTx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numRxDupe" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numTotalNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numTxRelay" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numTxRelayCanceled" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh1Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh1Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh2Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh2Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh3Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh3Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="radiation" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rainfall1H" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rainfall24H" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="soilMoisture" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="soilTemperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="uptimeSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uvLux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="weight" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="whiteLux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windDirection" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="windGust" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windLull" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windSpeed" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="hopsBack" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopsTowards" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="routeBackText" optional="YES" attributeType="String"/>
|
||||
<attribute name="routeText" optional="YES" attributeType="String"/>
|
||||
<attribute name="sent" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
|
||||
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="back" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
|
||||
</entity>
|
||||
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hwDisplayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="hwModel" attributeType="String"/>
|
||||
<attribute name="hwModelId" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="keyMatch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="longName" attributeType="String"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="newPublicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numString" optional="YES" attributeType="String"/>
|
||||
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="shortName" attributeType="String"/>
|
||||
<attribute name="unmessagable" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="userId" attributeType="String"/>
|
||||
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -0,0 +1,506 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788" systemVersion="24D81" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="green" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ledState" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="red" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="ambientLightingConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceLoggingEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
|
||||
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="downlinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
|
||||
<attribute name="psk" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="triggerType" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="disableTripleClick" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="ledHeartbeatEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="nodeInfoBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tripleClickAsAdHocPing" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="tzdef" optional="YES" attributeType="String"/>
|
||||
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="excludedModules" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hwModel" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="units" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="useI2SAsBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
|
||||
</entity>
|
||||
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ignoreMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="okToMqtt" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
|
||||
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="read" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="messageId"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="mapPositionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="13" usesScalarValueType="YES"/>
|
||||
<attribute name="mapPublishIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mapReportingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="mapReportingShouldReportLocation" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
|
||||
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="deviceId" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="registered" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
|
||||
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="myNodeNum"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabledProtocols" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ntpServer" optional="YES" attributeType="String"/>
|
||||
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
|
||||
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
|
||||
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="favorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="firstHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hopsAway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ignored" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sessionExpiration" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="sessionPasskey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="viaMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
|
||||
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
|
||||
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
|
||||
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
|
||||
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
|
||||
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
|
||||
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
|
||||
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
|
||||
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
|
||||
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
|
||||
<relationship name="pax" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PaxCounterEntity" inverseName="paxNode" inverseEntity="PaxCounterEntity"/>
|
||||
<relationship name="paxCounterConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PaxCounterConfigEntity" inverseName="paxCounterConfigNode" inverseEntity="PaxCounterConfigEntity"/>
|
||||
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
|
||||
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
|
||||
<relationship name="powerConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PowerConfigEntity" inverseName="powerConfigNode" inverseEntity="PowerConfigEntity"/>
|
||||
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
|
||||
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
|
||||
<relationship name="securityConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SecurityConfigEntity" inverseName="securityConfigNode" inverseEntity="SecurityConfigEntity"/>
|
||||
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
|
||||
<relationship name="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
|
||||
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
|
||||
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
|
||||
<relationship name="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
|
||||
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="num"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PaxCounterConfigEntity" representedClassName="PaxCounterConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleThreshold" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="updateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiThreshold" optional="YES" attributeType="Integer 32" defaultValueString="-80" usesScalarValueType="YES"/>
|
||||
<relationship name="paxCounterConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="paxCounterConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PaxCounterEntity" representedClassName="PaxCounterEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ble" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="uptime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="paxNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="pax" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsEnGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="precisionBits" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PowerConfigEntity" representedClassName="PowerConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adcMultiplierOverride" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceBatteryInaAddress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isPowerSaving" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="lsSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="minWakeSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="onBatteryShutdownAfterSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="waitBluetoothSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="powerConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="powerConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="color" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="distance" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="elevationGain" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="notes" optional="YES" attributeType="String"/>
|
||||
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
|
||||
</entity>
|
||||
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
|
||||
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SecurityConfigEntity" representedClassName="SecurityConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adminChannelEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="adminKey2" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="adminKey3" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="bluetoothLoggingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="debugLogApiEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isManaged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="privateKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="serialEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="securityConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="securityConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideConsoleSerialPort" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isRouter" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeartbeat" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="lastRequest" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="powerMeasurementEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="powerScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="powerUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES">
|
||||
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="iaq" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="irLux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="lux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numOnlineNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsRx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsRxBad" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsTx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numRxDupe" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numTotalNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numTxRelay" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numTxRelayCanceled" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh1Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh1Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh2Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh2Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh3Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh3Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="radiation" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rainfall1H" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rainfall24H" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="soilMoisture" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="soilTemperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="uptimeSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uvLux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="weight" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="whiteLux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windDirection" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="windGust" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windLull" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windSpeed" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="hopsBack" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopsTowards" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="routeBackText" optional="YES" attributeType="String"/>
|
||||
<attribute name="routeText" optional="YES" attributeType="String"/>
|
||||
<attribute name="sent" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
|
||||
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="back" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
|
||||
</entity>
|
||||
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hwDisplayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="hwModel" attributeType="String"/>
|
||||
<attribute name="hwModelId" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="keyMatch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="longName" attributeType="String"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="newPublicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numString" optional="YES" attributeType="String"/>
|
||||
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="shortName" attributeType="String"/>
|
||||
<attribute name="unmessagable" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="userId" attributeType="String"/>
|
||||
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -0,0 +1,506 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="24D81" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="green" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ledState" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="red" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="ambientLightingConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceLoggingEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
|
||||
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="downlinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
|
||||
<attribute name="psk" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="triggerType" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="disableTripleClick" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="ledHeartbeatEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="nodeInfoBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tripleClickAsAdHocPing" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="tzdef" optional="YES" attributeType="String"/>
|
||||
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="excludedModules" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hwModel" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="units" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="use12HClock" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="useI2SAsBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
|
||||
</entity>
|
||||
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ignoreMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="okToMqtt" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
|
||||
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="read" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="messageId"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="mapPositionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="13" usesScalarValueType="YES"/>
|
||||
<attribute name="mapPublishIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mapReportingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="mapReportingShouldReportLocation" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
|
||||
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="deviceId" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="registered" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
|
||||
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="myNodeNum"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabledProtocols" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ntpServer" optional="YES" attributeType="String"/>
|
||||
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
|
||||
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
|
||||
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="favorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="firstHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hopsAway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ignored" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sessionExpiration" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="sessionPasskey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="viaMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
|
||||
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
|
||||
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
|
||||
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
|
||||
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
|
||||
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
|
||||
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
|
||||
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
|
||||
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
|
||||
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
|
||||
<relationship name="pax" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PaxCounterEntity" inverseName="paxNode" inverseEntity="PaxCounterEntity"/>
|
||||
<relationship name="paxCounterConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PaxCounterConfigEntity" inverseName="paxCounterConfigNode" inverseEntity="PaxCounterConfigEntity"/>
|
||||
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
|
||||
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
|
||||
<relationship name="powerConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PowerConfigEntity" inverseName="powerConfigNode" inverseEntity="PowerConfigEntity"/>
|
||||
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
|
||||
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
|
||||
<relationship name="securityConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SecurityConfigEntity" inverseName="securityConfigNode" inverseEntity="SecurityConfigEntity"/>
|
||||
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
|
||||
<relationship name="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
|
||||
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
|
||||
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
|
||||
<relationship name="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
|
||||
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="num"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PaxCounterConfigEntity" representedClassName="PaxCounterConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleThreshold" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="updateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiThreshold" optional="YES" attributeType="Integer 32" defaultValueString="-80" usesScalarValueType="YES"/>
|
||||
<relationship name="paxCounterConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="paxCounterConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PaxCounterEntity" representedClassName="PaxCounterEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ble" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="uptime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="paxNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="pax" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsEnGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="precisionBits" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PowerConfigEntity" representedClassName="PowerConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adcMultiplierOverride" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceBatteryInaAddress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isPowerSaving" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="lsSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="minWakeSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="onBatteryShutdownAfterSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="waitBluetoothSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="powerConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="powerConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="color" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="distance" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="elevationGain" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="notes" optional="YES" attributeType="String"/>
|
||||
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
|
||||
</entity>
|
||||
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
|
||||
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SecurityConfigEntity" representedClassName="SecurityConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adminChannelEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="adminKey2" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="adminKey3" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="bluetoothLoggingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="debugLogApiEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isManaged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="privateKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="serialEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="securityConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="securityConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideConsoleSerialPort" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isRouter" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeartbeat" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="lastRequest" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="powerMeasurementEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="powerScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="powerUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES">
|
||||
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="iaq" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="irLux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="lux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numOnlineNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsRx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsRxBad" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsTx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numRxDupe" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numTotalNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numTxRelay" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numTxRelayCanceled" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh1Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh1Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh2Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh2Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh3Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="powerCh3Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="radiation" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rainfall1H" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rainfall24H" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="soilMoisture" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="soilTemperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="uptimeSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uvLux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="weight" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="whiteLux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windDirection" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="windGust" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windLull" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windSpeed" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="hopsBack" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopsTowards" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="routeBackText" optional="YES" attributeType="String"/>
|
||||
<attribute name="routeText" optional="YES" attributeType="String"/>
|
||||
<attribute name="sent" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
|
||||
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="back" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
|
||||
</entity>
|
||||
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hwDisplayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="hwModel" attributeType="String"/>
|
||||
<attribute name="hwModelId" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="keyMatch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="longName" attributeType="String"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="newPublicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numString" optional="YES" attributeType="String"/>
|
||||
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="shortName" attributeType="String"/>
|
||||
<attribute name="unmessagable" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="userId" attributeType="String"/>
|
||||
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -4,14 +4,17 @@ import SwiftUI
|
|||
import CoreData
|
||||
import OSLog
|
||||
import TipKit
|
||||
import MeshtasticProtobufs
|
||||
import DatadogCore
|
||||
import DatadogCrashReporting
|
||||
import DatadogRUM
|
||||
|
||||
@main
|
||||
struct MeshtasticAppleApp: App {
|
||||
|
||||
@UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self)
|
||||
private var appDelegate
|
||||
@UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) private var appDelegate
|
||||
|
||||
@ObservedObject var appState: AppState
|
||||
@ObservedObject var appState: AppState
|
||||
|
||||
private let persistenceController: PersistenceController
|
||||
|
||||
|
|
@ -26,37 +29,71 @@ struct MeshtasticAppleApp: App {
|
|||
let appState = AppState(
|
||||
router: Router()
|
||||
)
|
||||
self._appState = ObservedObject(wrappedValue: appState)
|
||||
// Initialize Datadog
|
||||
// RUM Client Tokens are NOT secret
|
||||
let appID = "79fe92a9-74c9-4c8f-ba63-6308384ecfa9"
|
||||
let clientToken = "pub4427bea20dbdb08a6af68034de22cd3b"
|
||||
let environment = "testflight"
|
||||
|
||||
Datadog.initialize(
|
||||
with: Datadog.Configuration(
|
||||
clientToken: clientToken,
|
||||
env: environment,
|
||||
site: .us5
|
||||
),
|
||||
trackingConsent: UserDefaults.usageDataAndCrashReporting ? .granted : .notGranted
|
||||
)
|
||||
|
||||
RUM.enable(
|
||||
with: RUM.Configuration(
|
||||
applicationID: appID,
|
||||
uiKitViewsPredicate: DefaultUIKitRUMViewsPredicate(),
|
||||
uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate()
|
||||
)
|
||||
)
|
||||
self._appState = ObservedObject(wrappedValue: appState)
|
||||
// Initialize the BLEManager singleton with the necessary dependencies
|
||||
BLEManager.setup(appState: appState, context: persistenceController.container.viewContext)
|
||||
self.persistenceController = persistenceController
|
||||
|
||||
// Wire up router
|
||||
self.appDelegate.router = appState.router
|
||||
// Show Tips
|
||||
#if DEBUG
|
||||
// Show tips in development
|
||||
try? Tips.resetDatastore()
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(
|
||||
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
|
||||
|
||||
if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#")) != nil {
|
||||
self.saveChannels = false
|
||||
if self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/v/#") == true {
|
||||
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
|
||||
if (self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil {
|
||||
|
|
@ -72,7 +109,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 {
|
||||
|
|
@ -80,10 +117,11 @@ struct MeshtasticAppleApp: App {
|
|||
}
|
||||
}
|
||||
.onOpenURL(perform: { (url) in
|
||||
|
||||
Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)")
|
||||
self.incomingUrl = url
|
||||
if url.absoluteString.lowercased().contains("meshtastic.org/e/#") {
|
||||
if url.absoluteString.lowercased().contains("meshtastic.org/v/#") {
|
||||
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
|
||||
if self.incomingUrl?.absoluteString.lowercased().contains("?") != nil {
|
||||
|
|
@ -99,7 +137,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)
|
||||
|
|
@ -115,7 +153,7 @@ struct MeshtasticAppleApp: App {
|
|||
.displayFrequency(.immediate)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { (_, newScenePhase) in
|
||||
switch newScenePhase {
|
||||
|
|
@ -138,5 +176,9 @@ struct MeshtasticAppleApp: App {
|
|||
Logger.services.error("🍎 [App] Apple must have changed something")
|
||||
}
|
||||
}
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(BLEManager.shared)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,22 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
if let targetValue = userInfo["target"] as? String,
|
||||
let deepLink = userInfo["path"] as? String,
|
||||
let url = URL(string: deepLink) {
|
||||
Logger.services.info("userNotificationCenter didReceiveResponse \(targetValue, privacy: .public) \(deepLink, privacy: .public)")
|
||||
Logger.services.info("userNotificationCenter didReceiveResponse handling deeplink: \(targetValue, privacy: .public) \(deepLink, privacy: .public)")
|
||||
// Handle TraceRoute notifications specially to ensure they navigate correctly
|
||||
if deepLink.contains("meshtastic:///nodes") && deepLink.contains("nodenum=") {
|
||||
// First extract the node number from the URL
|
||||
if let nodeNumString = deepLink.components(separatedBy: "nodenum=").last,
|
||||
let nodeNum = Int64(nodeNumString) {
|
||||
Logger.services.info("Navigation to specific node via notification: \(nodeNum, privacy: .public)")
|
||||
self.router?.navigationState.selectedTab = .nodes
|
||||
// Post a notification to trigger app-wide refresh
|
||||
NotificationCenter.default.post(name: NSNotification.Name("ForceNavigationRefresh"),
|
||||
object: nil,
|
||||
userInfo: ["nodeNum": nodeNum])
|
||||
self.router?.navigationState.nodeListSelectedNodeNum = nodeNum
|
||||
}
|
||||
}
|
||||
// Still call the regular router in all cases
|
||||
router?.route(url: url)
|
||||
} else {
|
||||
Logger.services.error("Failed to handle notification response: \(userInfo, privacy: .public)")
|
||||
|
|
|
|||
|
|
@ -8,6 +8,57 @@ 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)
|
||||
}
|
||||
var nodePKIExpireTime: TimeInterval {
|
||||
return TimeInterval((nodeExpireDays < 7 ? -7 : -nodeExpireDays) * 86400)
|
||||
}
|
||||
|
||||
if nodeExpireDays == 0 {
|
||||
// Purge Disabled
|
||||
Logger.data.info("💾 [NodeInfoEntity] Skip clearing stale nodes")
|
||||
return false
|
||||
}
|
||||
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "NodeInfoEntity")
|
||||
fetchRequest.predicate = NSPredicate(format: "favorite == false AND ignored == false AND ((user.pkiEncrypted == NO AND lastHeard < %@) OR (user.pkiEncrypted == YES AND lastHeard < %@))",
|
||||
NSDate(timeIntervalSinceNow: nodeExpireTime), NSDate(timeIntervalSinceNow: nodePKIExpireTime))
|
||||
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
batchDeleteRequest.resultType = .resultTypeCount
|
||||
|
||||
do {
|
||||
Logger.data.info("💾 [NodeInfoEntity] Clearing nodes older than \(nodeExpireDays) days")
|
||||
if let batchDeleteResult = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult {
|
||||
try context.save()
|
||||
let deletedNodes = batchDeleteResult.result as? Int ?? 0
|
||||
Logger.data.info("💾 [NodeInfoEntity] Cleared \(deletedNodes) stale nodes")
|
||||
if deletedNodes > 0 {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
Logger.data.error("💥 [NodeInfoEntity] bad delete results")
|
||||
}
|
||||
} catch {
|
||||
context.rollback()
|
||||
Logger.data.error("💥 [NodeInfoEntity] Error deleting stale nodes")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool {
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -160,7 +211,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
newNode.channel = Int32(packet.channel)
|
||||
}
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) {
|
||||
newNode.hopsAway = Int32(nodeInfoMessage.hopsAway)
|
||||
if nodeInfoMessage.hasHopsAway {
|
||||
newNode.hopsAway = Int32(nodeInfoMessage.hopsAway)
|
||||
}
|
||||
newNode.favorite = nodeInfoMessage.isFavorite
|
||||
}
|
||||
|
||||
|
|
@ -168,19 +221,37 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
|
||||
if newUserMessage.id.isEmpty {
|
||||
if packet.from > Constants.minimumNodeNum {
|
||||
let newUser = createUser(num: Int64(packet.from), context: context)
|
||||
newNode.user = newUser
|
||||
do {
|
||||
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
|
||||
newNode.user = newUser
|
||||
} catch CoreDataError.invalidInput(let message) {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
|
||||
} catch {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.userId = newUserMessage.id
|
||||
newUser.userId = newNode.num.toHex()
|
||||
newUser.num = Int64(packet.from)
|
||||
newUser.longName = newUserMessage.longName
|
||||
newUser.shortName = newUserMessage.shortName
|
||||
newUser.role = Int32(newUserMessage.role.rawValue)
|
||||
newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased()
|
||||
newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue)
|
||||
/// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default
|
||||
if newUserMessage.hasIsUnmessagable {
|
||||
newUser.unmessagable = newUserMessage.isUnmessagable
|
||||
} else {
|
||||
let roles = [2, 4, 5, 6, 7, 10, 11]
|
||||
let containsRole = roles.contains(Int(newUser.role))
|
||||
if containsRole {
|
||||
newUser.unmessagable = true
|
||||
} else {
|
||||
newUser.unmessagable = false
|
||||
}
|
||||
}
|
||||
if !newUserMessage.publicKey.isEmpty {
|
||||
newUser.pkiEncrypted = true
|
||||
newUser.publicKey = newUserMessage.publicKey
|
||||
|
|
@ -211,17 +282,34 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
}
|
||||
} else {
|
||||
if packet.from > Constants.minimumNodeNum {
|
||||
let newUser = createUser(num: Int64(packet.from), context: context)
|
||||
if !packet.publicKey.isEmpty {
|
||||
newNode.user?.pkiEncrypted = true
|
||||
newNode.user?.publicKey = packet.publicKey
|
||||
do {
|
||||
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
|
||||
if !packet.publicKey.isEmpty {
|
||||
newNode.user?.pkiEncrypted = true
|
||||
newNode.user?.publicKey = packet.publicKey
|
||||
}
|
||||
newNode.user = newUser
|
||||
} catch CoreDataError.invalidInput(let message) {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
|
||||
} catch {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
newNode.user = newUser
|
||||
}
|
||||
}
|
||||
|
||||
// User is messed up and has failed to create at least once, if this fails bail out
|
||||
if newNode.user == nil && packet.from > Constants.minimumNodeNum {
|
||||
newNode.user = createUser(num: Int64(packet.from), context: context)
|
||||
do {
|
||||
let newUser = try createUser(num: Int64(packet.from), context: context)
|
||||
newNode.user = newUser
|
||||
} catch CoreDataError.invalidInput(let message) {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
|
||||
context.rollback()
|
||||
return
|
||||
} catch {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
|
||||
context.rollback()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let myInfoEntity = MyInfoEntity(context: context)
|
||||
|
|
@ -269,22 +357,33 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries)
|
||||
}
|
||||
if nodeInfoMessage.hasUser {
|
||||
/// Seeing Some crashes here ?
|
||||
fetchedNode[0].user!.userId = nodeInfoMessage.user.id
|
||||
fetchedNode[0].user!.num = Int64(nodeInfoMessage.num)
|
||||
fetchedNode[0].user!.longName = nodeInfoMessage.user.longName
|
||||
fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName
|
||||
fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue)
|
||||
fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased()
|
||||
fetchedNode[0].user!.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue)
|
||||
fetchedNode[0].user?.userId = nodeInfoMessage.num.toHex()
|
||||
fetchedNode[0].user?.num = Int64(nodeInfoMessage.num)
|
||||
fetchedNode[0].user?.longName = nodeInfoMessage.user.longName
|
||||
fetchedNode[0].user?.shortName = nodeInfoMessage.user.shortName
|
||||
fetchedNode[0].user?.role = Int32(nodeInfoMessage.user.role.rawValue)
|
||||
fetchedNode[0].user?.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased()
|
||||
fetchedNode[0].user?.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue)
|
||||
/// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default
|
||||
if nodeInfoMessage.user.hasIsUnmessagable {
|
||||
fetchedNode[0].user?.unmessagable = nodeInfoMessage.user.isUnmessagable
|
||||
} else {
|
||||
let roles = [-1, 2, 4, 5, 6, 7, 10, 11]
|
||||
let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1))
|
||||
if containsRole {
|
||||
fetchedNode[0].user?.unmessagable = true
|
||||
} else {
|
||||
fetchedNode[0].user?.unmessagable = false
|
||||
}
|
||||
}
|
||||
if !nodeInfoMessage.user.publicKey.isEmpty {
|
||||
fetchedNode[0].user!.pkiEncrypted = true
|
||||
fetchedNode[0].user!.publicKey = nodeInfoMessage.user.publicKey
|
||||
fetchedNode[0].user?.pkiEncrypted = true
|
||||
fetchedNode[0].user?.publicKey = nodeInfoMessage.user.publicKey
|
||||
}
|
||||
Task {
|
||||
Api().loadDeviceHardwareData { (hw) in
|
||||
let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 })
|
||||
fetchedNode[0].user!.hwDisplayName = dh?.displayName
|
||||
fetchedNode[0].user?.hwDisplayName = dh?.displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -292,9 +391,14 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit)
|
||||
}
|
||||
if fetchedNode[0].user == nil {
|
||||
let newUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
|
||||
fetchedNode[0].user? = newUser
|
||||
|
||||
do {
|
||||
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
|
||||
fetchedNode[0].user = newUser
|
||||
} catch CoreDataError.invalidInput(let message) {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
|
||||
} catch {
|
||||
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
|
|
@ -358,12 +462,12 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
} else {
|
||||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
|
||||
}
|
||||
guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else {
|
||||
guard let mutablePositions = fetchedNode[0].positions?.mutableCopy() as? NSMutableOrderedSet else {
|
||||
return
|
||||
}
|
||||
/// 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 {
|
||||
|
|
@ -528,6 +632,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses
|
|||
newDisplayConfig.displayMode = Int32(config.displaymode.rawValue)
|
||||
newDisplayConfig.units = Int32(config.units.rawValue)
|
||||
newDisplayConfig.headingBold = config.headingBold
|
||||
newDisplayConfig.use12HClock = config.use12HClock
|
||||
fetchedNode[0].displayConfig = newDisplayConfig
|
||||
} else {
|
||||
|
||||
|
|
@ -539,6 +644,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses
|
|||
fetchedNode[0].displayConfig?.oledType = Int32(config.oled.rawValue)
|
||||
fetchedNode[0].displayConfig?.displayMode = Int32(config.displaymode.rawValue)
|
||||
fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue)
|
||||
fetchedNode[0].displayConfig?.use12HClock = config.use12HClock
|
||||
fetchedNode[0].displayConfig?.headingBold = config.headingBold
|
||||
}
|
||||
if sessionPasskey != nil {
|
||||
|
|
@ -687,7 +793,7 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, ses
|
|||
|
||||
func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("Positon config received: %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Position config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🗺️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -1205,6 +1311,7 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6
|
|||
newMQTTConfig.jsonEnabled = config.jsonEnabled
|
||||
newMQTTConfig.tlsEnabled = config.tlsEnabled
|
||||
newMQTTConfig.mapReportingEnabled = config.mapReportingEnabled
|
||||
newMQTTConfig.mapReportingShouldReportLocation = config.mapReportSettings.shouldReportLocation
|
||||
newMQTTConfig.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision)
|
||||
newMQTTConfig.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs)
|
||||
fetchedNode[0].mqttConfig = newMQTTConfig
|
||||
|
|
@ -1273,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
|
||||
|
|
@ -1404,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 {
|
||||
|
|
|
|||
|
|
@ -229,7 +229,8 @@
|
|||
"images": [
|
||||
"tlora-t3s3-epaper.svg"
|
||||
],
|
||||
"requiresDfu": true
|
||||
"requiresDfu": true,
|
||||
"hasInkHud": true
|
||||
},
|
||||
{
|
||||
"hwModel": 17,
|
||||
|
|
@ -604,7 +605,7 @@
|
|||
"hwModelSlug": "HELTEC_WIRELESS_TRACKER",
|
||||
"platformioTarget": "tracksenger",
|
||||
"architecture": "esp32-s3",
|
||||
"activelySupported": false,
|
||||
"activelySupported": true,
|
||||
"supportLevel": 3,
|
||||
"displayName": "TrackSenger (small TFT)",
|
||||
"requiresDfu": true,
|
||||
|
|
@ -626,7 +627,7 @@
|
|||
"hwModelSlug": "HELTEC_WIRELESS_TRACKER",
|
||||
"platformioTarget": "tracksenger-oled",
|
||||
"architecture": "esp32-s3",
|
||||
"activelySupported": false,
|
||||
"activelySupported": true,
|
||||
"supportLevel": 3,
|
||||
"displayName": "TrackSenger (big OLED)",
|
||||
"partitionScheme": "8MB"
|
||||
|
|
@ -872,5 +873,110 @@
|
|||
"images": [
|
||||
"thinknode_m2.svg"
|
||||
]
|
||||
},
|
||||
{
|
||||
"hwModel": 94,
|
||||
"hwModelSlug": "HELTEC_MESH_POCKET",
|
||||
"platformioTarget": "heltec-mesh-pocket-10000",
|
||||
"architecture": "nrf52840",
|
||||
"activelySupported": true,
|
||||
"supportLevel": 1,
|
||||
"displayName": "Heltec MeshPocket",
|
||||
"tags": [
|
||||
"Heltec"
|
||||
],
|
||||
"images": [
|
||||
"heltec_mesh_pocket.svg"
|
||||
],
|
||||
"requiresDfu": true,
|
||||
"hasInkHud": true
|
||||
},
|
||||
{
|
||||
"hwModel": 95,
|
||||
"hwModelSlug": "SEEED_SOLAR_NODE",
|
||||
"platformioTarget": "seeed_solar_node",
|
||||
"architecture": "nrf52840",
|
||||
"activelySupported": true,
|
||||
"supportLevel": 1,
|
||||
"displayName": "Seeed SenseCAP Solar Node",
|
||||
"tags": [
|
||||
"Seeed"
|
||||
],
|
||||
"images": [
|
||||
"seeed_solar.svg"
|
||||
],
|
||||
"requiresDfu": true
|
||||
},
|
||||
{
|
||||
"hwModel": 99,
|
||||
"hwModelSlug": "SEEED_WIO_TRACKER_L1",
|
||||
"platformioTarget": "seeed_wio_tracker_L1",
|
||||
"architecture": "nrf52840",
|
||||
"activelySupported": true,
|
||||
"supportLevel": 1,
|
||||
"displayName": "Seeed Wio Tracker L1",
|
||||
"tags": [
|
||||
"Seeed"
|
||||
],
|
||||
"images": [
|
||||
"wio_tracker_l1_case.svg"
|
||||
],
|
||||
"requiresDfu": true
|
||||
},
|
||||
{
|
||||
"hwModel": 97,
|
||||
"hwModelSlug": "CROWPANEL",
|
||||
"platformioTarget": "elecrow-adv1-43-50-70-tft",
|
||||
"architecture": "esp32-s3",
|
||||
"activelySupported": true,
|
||||
"supportLevel": 1,
|
||||
"displayName": "Crowpanel Adv 4.3/5.0/7.0 TFT",
|
||||
"tags": [
|
||||
"Elecrow"
|
||||
],
|
||||
"requiresDfu": true,
|
||||
"images": [
|
||||
"crowpanel_5_0.svg",
|
||||
"crowpanel_7_0.svg"
|
||||
],
|
||||
"partitionScheme": "16MB",
|
||||
"hasMui": true
|
||||
},
|
||||
{
|
||||
"hwModel": 97,
|
||||
"hwModelSlug": "CROWPANEL",
|
||||
"platformioTarget": "elecrow-adv-24-28-tft",
|
||||
"architecture": "esp32-s3",
|
||||
"activelySupported": true,
|
||||
"supportLevel": 1,
|
||||
"displayName": "Crowpanel Adv 2.4/2.8 TFT",
|
||||
"tags": [
|
||||
"Elecrow"
|
||||
],
|
||||
"requiresDfu": true,
|
||||
"images": [
|
||||
"crowpanel_2_4.svg",
|
||||
"crowpanel_2_8.svg"
|
||||
],
|
||||
"partitionScheme": "16MB",
|
||||
"hasMui": true
|
||||
},
|
||||
{
|
||||
"hwModel": 97,
|
||||
"hwModelSlug": "CROWPANEL",
|
||||
"platformioTarget": "elecrow-adv-35-tft",
|
||||
"architecture": "esp32-s3",
|
||||
"activelySupported": true,
|
||||
"supportLevel": 1,
|
||||
"displayName": "Crowpanel Adv 3.5 TFT",
|
||||
"tags": [
|
||||
"Elecrow"
|
||||
],
|
||||
"requiresDfu": true,
|
||||
"images": [
|
||||
"crowpanel_3_5.svg"
|
||||
],
|
||||
"partitionScheme": "16MB",
|
||||
"hasMui": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ struct BluetoothConnectionTip: Tip {
|
|||
Text("Connected Radio")
|
||||
}
|
||||
var message: Text? {
|
||||
Text("Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity.")
|
||||
Text("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.")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "flipphone")
|
||||
|
|
|
|||
37
Meshtastic/Tips/PersistantTips.swift
Normal file
37
Meshtastic/Tips/PersistantTips.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Untitled.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 6/19/25.
|
||||
//
|
||||
import TipKit
|
||||
|
||||
struct PersistentTip: TipViewStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
VStack {
|
||||
HStack(alignment: .top) {
|
||||
if let image = configuration.image {
|
||||
image
|
||||
.font(.system(size: 42))
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(.trailing, 5)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
if let title = configuration.title {
|
||||
title
|
||||
.bold()
|
||||
.font(.headline)
|
||||
}
|
||||
if let message = configuration.message {
|
||||
message
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.backgroundStyle(.thinMaterial)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
|
@ -32,9 +32,10 @@ struct Connect: View {
|
|||
VStack {
|
||||
List {
|
||||
if bleManager.isSwitchedOn {
|
||||
Section(header: Text("Connected Radio").font(.title)) {
|
||||
Section {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected {
|
||||
TipView(BluetoothConnectionTip(), arrowEdge: .bottom)
|
||||
.tipViewStyle(PersistentTip())
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
VStack(alignment: .center) {
|
||||
|
|
@ -76,47 +77,58 @@ struct Connect: View {
|
|||
.foregroundColor(Color.gray)
|
||||
.padding([.top])
|
||||
.swipeActions {
|
||||
Button(role: .destructive) {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral,
|
||||
connectedPeripheral.peripheral.state == .connected {
|
||||
bleManager.disconnectPeripheral(reconnect: false)
|
||||
if bleManager.allowDisconnect {
|
||||
Button(role: .destructive) {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral,
|
||||
connectedPeripheral.peripheral.state == .connected {
|
||||
bleManager.disconnectPeripheral(reconnect: false)
|
||||
}
|
||||
} label: {
|
||||
Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
|
||||
}
|
||||
} label: {
|
||||
Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
|
||||
if node != nil {
|
||||
Label("\(String(node!.num))", systemImage: "number")
|
||||
Label("BLE RSSI \(connectedPeripheral.rssi)", systemImage: "cellularbars")
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
Button {
|
||||
if !liveActivityStarted {
|
||||
#if canImport(ActivityKit)
|
||||
Logger.services.info("Start live activity.")
|
||||
startNodeActivity()
|
||||
#endif
|
||||
} else {
|
||||
if bleManager.isSubscribed {
|
||||
Button {
|
||||
if !liveActivityStarted {
|
||||
#if canImport(ActivityKit)
|
||||
Logger.services.info("Stop live activity.")
|
||||
endActivity()
|
||||
#endif
|
||||
Logger.services.info("Start live activity.")
|
||||
startNodeActivity()
|
||||
#endif
|
||||
} else {
|
||||
#if canImport(ActivityKit)
|
||||
Logger.services.info("Stop live activity.")
|
||||
endActivity()
|
||||
#endif
|
||||
}
|
||||
} label: {
|
||||
Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play")
|
||||
}
|
||||
} label: {
|
||||
Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play")
|
||||
}
|
||||
#endif
|
||||
Text("Num: \(String(node!.num))")
|
||||
Text("Short Name: \(node?.user?.shortName ?? "?")")
|
||||
Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)")
|
||||
Text("BLE RSSI: \(connectedPeripheral.rssi)")
|
||||
|
||||
Button {
|
||||
if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!, adminIndex: node!.myInfo!.adminIndex) {
|
||||
Logger.mesh.error("Shutdown Failed")
|
||||
if bleManager.allowDisconnect {
|
||||
Button(role: .destructive) {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral,
|
||||
connectedPeripheral.peripheral.state == .connected {
|
||||
bleManager.disconnectPeripheral(reconnect: false)
|
||||
}
|
||||
} label: {
|
||||
Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!) {
|
||||
Logger.mesh.error("Shutdown Failed")
|
||||
}
|
||||
|
||||
} label: {
|
||||
Label("Power Off", systemImage: "power")
|
||||
} label: {
|
||||
Label("Power Off", systemImage: "power")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -132,7 +144,6 @@ struct Connect: View {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
if bleManager.isConnecting {
|
||||
HStack {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
|
|
|
|||
|
|
@ -32,47 +32,64 @@ import Foundation
|
|||
import SwiftUI
|
||||
|
||||
struct SignalStrengthIndicator: View {
|
||||
let signalStrength: BLESignalStrength
|
||||
// Accessibility: VoiceOver description
|
||||
private var accessibilityDescription: String {
|
||||
switch signalStrength {
|
||||
case .weak:
|
||||
return "Signal strength weak".localized
|
||||
case .normal:
|
||||
return "Signal strength normal".localized
|
||||
case .strong:
|
||||
return "Signal strength strong".localized
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
ForEach(0..<3) { bar in
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.divided(amount: (CGFloat(bar) + 1) / CGFloat(3))
|
||||
.fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3))
|
||||
.frame(width: 8, height: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
let signalStrength: BLESignalStrength
|
||||
|
||||
private func getColor() -> Color {
|
||||
switch signalStrength {
|
||||
case .weak:
|
||||
return Color.red
|
||||
case .normal:
|
||||
return Color.yellow
|
||||
case .strong:
|
||||
return Color.green
|
||||
}
|
||||
}
|
||||
var body: some View {
|
||||
Group {
|
||||
HStack {
|
||||
ForEach(0..<3) { bar in
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.divided(amount: (CGFloat(bar) + 1) / CGFloat(3))
|
||||
.fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3))
|
||||
.frame(width: 8, height: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("Signal strength".localized)
|
||||
.accessibilityValue(accessibilityDescription)
|
||||
}
|
||||
|
||||
private func getColor() -> Color {
|
||||
switch signalStrength {
|
||||
case .weak:
|
||||
return Color.red
|
||||
case .normal:
|
||||
return Color.yellow
|
||||
case .strong:
|
||||
return Color.green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Divided<S: Shape>: Shape {
|
||||
var amount: CGFloat // Should be in range 0...1
|
||||
var shape: S
|
||||
func path(in rect: CGRect) -> Path {
|
||||
shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice)
|
||||
}
|
||||
var amount: CGFloat // Should be in range 0...1
|
||||
var shape: S
|
||||
func path(in rect: CGRect) -> Path {
|
||||
shape.path(in: rect.divided(atDistance: amount * rect.height, from: .maxYEdge).slice)
|
||||
}
|
||||
}
|
||||
|
||||
extension Shape {
|
||||
func divided(amount: CGFloat) -> Divided<Self> {
|
||||
return Divided(amount: amount, shape: self)
|
||||
}
|
||||
func divided(amount: CGFloat) -> Divided<Self> {
|
||||
return Divided(amount: amount, shape: self)
|
||||
}
|
||||
}
|
||||
|
||||
enum BLESignalStrength: Int {
|
||||
case weak = 0
|
||||
case normal = 1
|
||||
case strong = 2
|
||||
case weak = 0
|
||||
case normal = 1
|
||||
case strong = 2
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,69 +13,101 @@ struct BatteryCompact: View {
|
|||
var color: Color
|
||||
|
||||
var body: some View {
|
||||
// Group the battery icon and label in a single accessible container
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
if let batteryLevel {
|
||||
if batteryLevel == 100 {
|
||||
Image(systemName: "battery.100.bolt")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
} else if batteryLevel < 100 && batteryLevel > 74 {
|
||||
Image(systemName: "battery.75")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
} else if batteryLevel < 75 && batteryLevel > 49 {
|
||||
Image(systemName: "battery.50")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
} else if batteryLevel < 50 && batteryLevel > 14 {
|
||||
Image(systemName: "battery.25")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
} else if batteryLevel < 15 && batteryLevel > 0 {
|
||||
Image(systemName: "battery.0")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
} else if batteryLevel == 0 {
|
||||
Image(systemName: "battery.0")
|
||||
.font(iconFont)
|
||||
.foregroundColor(.red)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
} else if batteryLevel > 100 {
|
||||
// Check for plugged in state
|
||||
let isPluggedIn = batteryLevel > 100
|
||||
let isCharging = batteryLevel == 100
|
||||
// Battery icon selection based on level
|
||||
if isPluggedIn {
|
||||
Image(systemName: "powerplug")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.accessibilityHidden(true) // Hide from VoiceOver since container will handle it
|
||||
} else if isCharging {
|
||||
Image(systemName: "battery.100.bolt")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.accessibilityHidden(true)
|
||||
} else if batteryLevel > 74 {
|
||||
Image(systemName: "battery.75")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.accessibilityHidden(true)
|
||||
} else if batteryLevel > 49 {
|
||||
Image(systemName: "battery.50")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.accessibilityHidden(true)
|
||||
} else if batteryLevel > 14 {
|
||||
Image(systemName: "battery.25")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.accessibilityHidden(true)
|
||||
} else if batteryLevel > 0 {
|
||||
Image(systemName: "battery.0")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
Image(systemName: "battery.0")
|
||||
.font(iconFont)
|
||||
.foregroundColor(.red)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "battery.0")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
if let batteryLevel {
|
||||
if batteryLevel > 100 {
|
||||
// Battery text label
|
||||
if isPluggedIn {
|
||||
Text("PWD")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(font)
|
||||
} else if batteryLevel == 100 {
|
||||
.accessibilityHidden(true)
|
||||
} else if isCharging {
|
||||
Text("CHG")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(font)
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
Text(verbatim: "\(batteryLevel.formatted(.number.precision(.fractionLength(0))))%")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(font)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
} else {
|
||||
// Unknown battery state
|
||||
Image(systemName: "battery.0")
|
||||
.font(iconFont)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.accessibilityHidden(true)
|
||||
Text(verbatim: "?")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(font)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
// Setup container-level accessibility for VoiceOver
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge"))
|
||||
// Set appropriate value based on the battery state using a computed property
|
||||
.accessibilityValue(batteryLevel.map { level in
|
||||
if level > 100 {
|
||||
// Plugged in - same as PWD visual indicator
|
||||
return "Plugged in".localized
|
||||
} else if level == 100 {
|
||||
// Charging - same as CHG visual indicator
|
||||
return "Charging".localized
|
||||
} else {
|
||||
// Normal battery level
|
||||
return String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(level))
|
||||
}
|
||||
} ?? "Unknown")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,18 +18,20 @@ struct BatteryGauge: View {
|
|||
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
let batteryLevel = Double(mostRecent?.batteryLevel ?? 0)
|
||||
// For VoiceOver purposes, detect when device is plugged in (battery > 100%)
|
||||
let isPluggedIn = (mostRecent?.batteryLevel ?? 0) > 100
|
||||
// Use a capped battery level for UI display
|
||||
let batteryLevel = Double(min(100, mostRecent?.batteryLevel ?? 0))
|
||||
|
||||
VStack {
|
||||
if batteryLevel > 100.0 {
|
||||
// Plugged in
|
||||
Image(systemName: "powerplug")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
if isPluggedIn {
|
||||
// Use a completely standalone view for the plugged in state
|
||||
// to avoid any VoiceOver confusion
|
||||
PluggedInIndicator()
|
||||
} else {
|
||||
let gradient = Gradient(colors: [.red, .orange, .green])
|
||||
Gauge(value: batteryLevel, in: minValue...maxValue) {
|
||||
// Accessibility for battery gauge
|
||||
if batteryLevel >= 0.0 && batteryLevel < 10 {
|
||||
Label("Battery Level %", systemImage: "battery.0")
|
||||
} else if batteryLevel >= 10.0 && batteryLevel < 25.00 {
|
||||
|
|
@ -50,6 +52,8 @@ struct BatteryGauge: View {
|
|||
Text(Int(batteryLevel), format: .percent)
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Battery Level", comment: "VoiceOver label for battery gauge"))
|
||||
.accessibilityValue(String(format: NSLocalizedString("Battery Level %", comment: "VoiceOver value for battery level"), Int(batteryLevel)))
|
||||
.tint(gradient)
|
||||
.gaugeStyle(.accessoryCircular)
|
||||
}
|
||||
|
|
@ -63,6 +67,23 @@ struct BatteryGauge: View {
|
|||
}
|
||||
}
|
||||
|
||||
/// A dedicated view for showing a device is plugged in
|
||||
/// With proper VoiceOver support that matches the visual indication
|
||||
struct PluggedInIndicator: View {
|
||||
var body: some View {
|
||||
// This view is isolated from any battery measurement
|
||||
// to ensure VoiceOver doesn't pick up any percentages
|
||||
Image(systemName: "powerplug")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
// Override the accessibility to ensure correct VoiceOver announcement
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("Battery Level".localized)
|
||||
.accessibilityValue("Plugged in".localized)
|
||||
}
|
||||
}
|
||||
|
||||
struct BatteryGauge_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
|
|
|
|||
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,22 +21,46 @@ struct ConnectedDevice: View {
|
|||
if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly {
|
||||
if bluetoothOn {
|
||||
if deviceConnected {
|
||||
if mqttUplinkEnabled || mqttDownlinkEnabled {
|
||||
MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic)
|
||||
}
|
||||
Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
|
||||
.imageScale(.large)
|
||||
.foregroundColor(.green)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text(name.addingVariationSelectors).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray)
|
||||
// Create an HStack for connected state with proper accessibility
|
||||
HStack {
|
||||
if mqttUplinkEnabled || mqttDownlinkEnabled {
|
||||
MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
|
||||
.imageScale(.large)
|
||||
.foregroundColor(.green)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.accessibilityHidden(true)
|
||||
Text(name.addingVariationSelectors)
|
||||
.font(name.isEmoji() ? .title : .callout)
|
||||
.foregroundColor(.gray)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("Connected to Bluetooth device".localized + ", " + name.formatNodeNameForVoiceOver())
|
||||
} else {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right.slash")
|
||||
.imageScale(.medium)
|
||||
.foregroundColor(.red)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
// Create a container for disconnected state
|
||||
HStack {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right.slash")
|
||||
.imageScale(.medium)
|
||||
.foregroundColor(.red)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("No Bluetooth device connected".localized)
|
||||
}
|
||||
} else {
|
||||
Text("Bluetooth is off").font(.subheadline).foregroundColor(.red)
|
||||
// Create a container for Bluetooth off state
|
||||
HStack {
|
||||
Text("bluetooth.off".localized)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.red)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("bluetooth.off".localized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
97
Meshtastic/Views/Helpers/Help/ChannelsHelp.swift
Normal file
97
Meshtastic/Views/Helpers/Help/ChannelsHelp.swift
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
//
|
||||
// ChannelHelp.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 6/18/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ChannelsHelp: View {
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
Label("Channels Help", systemImage: "questionmark.circle")
|
||||
.font(.title)
|
||||
.padding(.vertical)
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
CircleText(text: String(0), color: .accentColor)
|
||||
.brightness(0.2)
|
||||
.offset(y: -10)
|
||||
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(.leading)
|
||||
.padding(.trailing, 7)
|
||||
.foregroundColor(Color.green)
|
||||
.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.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(.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)
|
||||
}
|
||||
}
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
#endif
|
||||
}
|
||||
.frame(minHeight: 0, maxHeight: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.presentationDetents([.large])
|
||||
.presentationContentInteraction(.scrolls)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .large))
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelHelpPreviews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
ChannelsHelp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,6 +51,20 @@ struct LoRaSignalStrengthMeter_Previews: PreviewProvider {
|
|||
static var previews: some View {
|
||||
ScrollView {
|
||||
VStack {
|
||||
VStack {
|
||||
// Good
|
||||
LoRaSignalStrengthMeter(snr: -10, rssi: -100, preset: ModemPresets.longFast, compact: true)
|
||||
.padding(.bottom)
|
||||
// Fair
|
||||
LoRaSignalStrengthMeter(snr: -9.5, rssi: -119, preset: ModemPresets.longFast, compact: true)
|
||||
.padding(.bottom)
|
||||
// Bad
|
||||
LoRaSignalStrengthMeter(snr: -12.75, rssi: -139, preset: ModemPresets.longFast, compact: true)
|
||||
.padding(.bottom)
|
||||
// None
|
||||
LoRaSignalStrengthMeter(snr: -26.0, rssi: -128, preset: ModemPresets.longFast, compact: true)
|
||||
.padding(.bottom)
|
||||
}.padding()
|
||||
HStack {
|
||||
// Good
|
||||
LoRaSignalStrengthMeter(snr: -1, rssi: -114, preset: ModemPresets.longFast, compact: false)
|
||||
|
|
@ -85,16 +99,5 @@ struct LoRaSignalStrengthMeter_Previews: PreviewProvider {
|
|||
}
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
VStack {
|
||||
// Good
|
||||
LoRaSignalStrengthMeter(snr: -10, rssi: -100, preset: ModemPresets.longFast, compact: true)
|
||||
// Fair
|
||||
LoRaSignalStrengthMeter(snr: -9.5, rssi: -119, preset: ModemPresets.longFast, compact: true)
|
||||
// Bad
|
||||
LoRaSignalStrengthMeter(snr: -12.75, rssi: -139, preset: ModemPresets.longFast, compact: true)
|
||||
// None
|
||||
LoRaSignalStrengthMeter(snr: -26.0, rssi: -128, preset: ModemPresets.longFast, compact: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,27 +11,22 @@ struct MeshtasticLogo: View {
|
|||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
VStack {
|
||||
Image("logo-white")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(.accentColor)
|
||||
.scaledToFit()
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.padding(.top, 5)
|
||||
.offset(x: -15)
|
||||
#else
|
||||
VStack {
|
||||
Image(colorScheme == .dark ? "logo-white" : "logo-black")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.scaledToFit()
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.offset(x: -15)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
113
Meshtastic/Views/Helpers/RateLimitedButton.swift
Normal file
113
Meshtastic/Views/Helpers/RateLimitedButton.swift
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
//
|
||||
// RateLimitCountdownView.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Jake Bordens on 5/5/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// This class provides a rate limited button.
|
||||
// Provide a key to differentiate which action is rate-limited
|
||||
// This allows you to keep different rate limits for different action
|
||||
// Rate limits are stored in a RateLimitStorage singleton, but do not persist
|
||||
public struct RateLimitedButton<Content: View>: View {
|
||||
typealias Builder = ((percentComplete: Double, secondsRemaining: TimeInterval)?) -> Content
|
||||
|
||||
let key: String
|
||||
|
||||
@StateObject var storage = RateLimitStorage.shared
|
||||
|
||||
let rateLimit: TimeInterval
|
||||
let content: Builder
|
||||
let action: () -> Void
|
||||
|
||||
init(key: String, rateLimit: TimeInterval, action: @escaping () -> Void, @ViewBuilder label: @escaping Builder) {
|
||||
self.key = key
|
||||
self.rateLimit = rateLimit
|
||||
self.content = label
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
let percentRemaining = storage.rateLimitRemainingPercentage(forKey: key)
|
||||
let secondsRemaining = storage.rateLimitSecondsRemaining(forKey: key)
|
||||
if percentRemaining > 0.0 {
|
||||
content((percentRemaining, secondsRemaining))
|
||||
} else {
|
||||
Button {
|
||||
storage.actionOccured(forKey: key, rateLimit: rateLimit)
|
||||
action()
|
||||
} label: {
|
||||
content(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To store the time an action occured (name by a key) and the time limit
|
||||
// Does not persist across app launches
|
||||
class RateLimitStorage: ObservableObject {
|
||||
private struct RateLimiter {
|
||||
var actionOccuredTimestamp: Date
|
||||
var rateLimitSeconds: TimeInterval
|
||||
|
||||
var rateLimitExpires: Date {
|
||||
return actionOccuredTimestamp.addingTimeInterval(rateLimitSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
static var shared: RateLimitStorage = RateLimitStorage() // Singleton instance
|
||||
|
||||
private var rateLimits = [String: RateLimiter]()
|
||||
private var timer: Timer?
|
||||
|
||||
func actionOccured(forKey key: String, rateLimit: TimeInterval) {
|
||||
let now = Date()
|
||||
if let existingRateLimit = rateLimits[key] {
|
||||
if existingRateLimit.rateLimitExpires > now.addingTimeInterval(rateLimit) {
|
||||
// We have an existing rate limit that is larger than the one being requested
|
||||
// Ignore
|
||||
return
|
||||
}
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
rateLimits[key] = RateLimiter(actionOccuredTimestamp: now, rateLimitSeconds: rateLimit)
|
||||
startTimerIfNecessary()
|
||||
}
|
||||
|
||||
func rateLimitRemainingPercentage(forKey: String) -> Double {
|
||||
guard let rateLimit = rateLimits[forKey] else {
|
||||
return 0.0
|
||||
}
|
||||
let percent = (rateLimit.rateLimitExpires.timeIntervalSinceNow) / rateLimit.rateLimitSeconds
|
||||
return min(1.0, max(percent, 0.0))
|
||||
}
|
||||
|
||||
func rateLimitSecondsRemaining(forKey: String) -> TimeInterval {
|
||||
guard let rateLimit = rateLimits[forKey] else {
|
||||
return 0.0
|
||||
}
|
||||
return rateLimit.rateLimitExpires.timeIntervalSinceNow
|
||||
}
|
||||
|
||||
func startTimerIfNecessary() {
|
||||
// Timer exists, don't create one
|
||||
guard timer == nil else { return }
|
||||
|
||||
// Create the timer
|
||||
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.objectWillChange.send()
|
||||
|
||||
// Determine if we can clean up the dictionary and stop the timer.
|
||||
let maxExpiration = self.rateLimits.values.map { $0.rateLimitExpires }.max() ?? .distantPast
|
||||
if maxExpiration.timeIntervalSinceNow < 0 {
|
||||
// All rateLimits are in the past. Stop and clean up
|
||||
self.timer?.invalidate()
|
||||
self.timer = nil
|
||||
self.rateLimits.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,19 +12,28 @@ struct SecureInput: View {
|
|||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@Binding private var text: String
|
||||
@Binding private var isValid: Bool
|
||||
@State var isSecure: Bool = true
|
||||
private var title: String
|
||||
|
||||
init(_ title: String, text: Binding<String>, isValid: Binding<Bool>) {
|
||||
// Local state to store the value of iSSecure, or optionally a binding
|
||||
private var isSecureBinding: Binding<Bool>?
|
||||
@State private var isSecureLocal: Bool = true
|
||||
|
||||
private var isSecure: Binding<Bool> {
|
||||
// Use the binding if we have one, otherwise fallback to the local state variable
|
||||
isSecureBinding ?? $isSecureLocal
|
||||
}
|
||||
|
||||
init(_ title: String, text: Binding<String>, isValid: Binding<Bool>, isSecure: Binding<Bool>? = nil) {
|
||||
self.title = title
|
||||
self._text = text
|
||||
self._isValid = isValid
|
||||
self.isSecureBinding = isSecure
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .trailing) {
|
||||
Group {
|
||||
if isSecure {
|
||||
if isSecure.wrappedValue {
|
||||
SecureField(title, text: $text)
|
||||
.font(idiom == .phone ? .caption : .callout)
|
||||
.allowsTightening(true)
|
||||
|
|
@ -51,9 +60,9 @@ struct SecureInput: View {
|
|||
|
||||
if !text.isEmpty {
|
||||
Button(action: {
|
||||
isSecure.toggle()
|
||||
isSecure.wrappedValue.toggle()
|
||||
}) {
|
||||
Image(systemName: self.isSecure ? "eye.slash" : "eye")
|
||||
Image(systemName: self.isSecure.wrappedValue ? "eye.slash" : "eye")
|
||||
.accentColor(.secondary)
|
||||
}.buttonStyle(BorderlessButtonStyle())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,17 @@ struct ChannelList: View {
|
|||
var channelSelection: ChannelEntity?
|
||||
|
||||
@State private var isPresentingDeleteChannelMessagesConfirm: Bool = false
|
||||
|
||||
@State private var isPresentingTraceRouteSentAlert = false
|
||||
@State private var showingHelp = false
|
||||
|
||||
var restrictedChannels = ["gpio", "mqtt", "serial", "admin"]
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)],
|
||||
predicate: nil,
|
||||
animation: .default
|
||||
) private var channels: FetchedResults<ChannelEntity>
|
||||
|
||||
@ViewBuilder
|
||||
private func makeChannelRow(
|
||||
myInfo: MyInfoEntity,
|
||||
|
|
@ -52,6 +58,7 @@ struct ChannelList: View {
|
|||
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
ChannelLock(channel: channel)
|
||||
if channel.name?.isEmpty ?? false {
|
||||
if channel.role == 1 {
|
||||
Text(String("PrimaryChannel").camelCaseToWords())
|
||||
|
|
@ -87,6 +94,9 @@ struct ChannelList: View {
|
|||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
if channel.mute {
|
||||
Image(systemName: "bell.slash")
|
||||
}
|
||||
}
|
||||
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
|
|
@ -103,7 +113,7 @@ struct ChannelList: View {
|
|||
var body: some View {
|
||||
VStack {
|
||||
// Display Contacts for the rest of the non admin channels
|
||||
if let node, let myInfo = node.myInfo, let channels = myInfo.channels?.array as? [ChannelEntity] {
|
||||
if let node, let myInfo = node.myInfo {
|
||||
List(selection: $channelSelection) {
|
||||
ForEach(channels) { (channel: ChannelEntity) in
|
||||
if !restrictedChannels.contains(channel.name?.lowercased() ?? "") {
|
||||
|
|
@ -119,7 +129,7 @@ struct ChannelList: View {
|
|||
}
|
||||
}
|
||||
Button {
|
||||
channel.mute = !channel.mute
|
||||
channel.mute.toggle()
|
||||
do {
|
||||
let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!)
|
||||
if adminMessageId > 0 {
|
||||
|
|
@ -152,8 +162,31 @@ struct ChannelList: View {
|
|||
}
|
||||
.padding([.top, .bottom])
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Channels")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Channels")
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,15 @@ struct ChannelMessageList: View {
|
|||
ZStack(alignment: .bottomTrailing) {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(channel.allPrivateMessages) { (message: MessageEntity) in
|
||||
ForEach(Array(channel.allPrivateMessages.enumerated()), id: \.element.id) { index, message in
|
||||
// Get the previous message, if it exists
|
||||
let previousMessage = index > 0 ? channel.allPrivateMessages[index - 1] : nil
|
||||
let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false)
|
||||
if message.displayTimestamp(aboveMessage: previousMessage) {
|
||||
Text(message.timestamp.formatted(date: .abbreviated, time: .shortened))
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
if message.replyID > 0 {
|
||||
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
|
|
@ -44,7 +51,6 @@ struct ChannelMessageList: View {
|
|||
messageToHighlight = messageNum
|
||||
}
|
||||
scrollView.scrollTo(messageNum, anchor: .center)
|
||||
|
||||
// Reset highlight after delay
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||
|
|
@ -130,12 +136,11 @@ struct ChannelMessageList: View {
|
|||
Spacer(minLength: 50)
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(.blue, lineWidth: 2)
|
||||
.opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
|
||||
}
|
||||
// .overlay {
|
||||
// RoundedRectangle(cornerRadius: 18)
|
||||
// .stroke(.blue, lineWidth: 2)
|
||||
// .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
|
||||
// }
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
|
|
@ -175,24 +180,24 @@ struct ChannelMessageList: View {
|
|||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.onFirstAppear {
|
||||
// Find first unread message
|
||||
if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId {
|
||||
if channel.unreadMessages == 0 {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
|
||||
showScrollToBottomButton = true
|
||||
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
}
|
||||
} else {
|
||||
// If no unread messages, scroll to bottom
|
||||
withAnimation {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
|
||||
showScrollToBottomButton = true
|
||||
}
|
||||
}
|
||||
}
|
||||
gotFirstUnreadMessage = true
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
|
||||
withAnimation {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
showScrollToBottomButton = false
|
||||
}
|
||||
|
|
@ -200,7 +205,7 @@ struct ChannelMessageList: View {
|
|||
.onChange(of: channel.allPrivateMessages) {
|
||||
if hasReachedBottom {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
|
||||
}
|
||||
} else {
|
||||
showScrollToBottomButton = true
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ struct Messages: View {
|
|||
}
|
||||
|
||||
TipView(MessagesTip(), arrowEdge: .top)
|
||||
.tipViewStyle(PersistentTip())
|
||||
}
|
||||
.navigationTitle("Messages")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ struct RequestPositionButton: View {
|
|||
var body: some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.accessibilityLabel("Position Exchange Requested".localized)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large)
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ struct TextMessageField: View {
|
|||
} label: {
|
||||
Image(systemName: "x.circle.fill")
|
||||
}
|
||||
Text("Replying to a message")
|
||||
Text("Reply")
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
ZStack {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ struct TextMessageSize: View {
|
|||
|
||||
var body: some View {
|
||||
ProgressView("\("Bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
|
||||
.accessibilityLabel(NSLocalizedString("Message Size", comment: "VoiceOver label for message size"))
|
||||
.accessibilityValue(String(format: NSLocalizedString("Bytes Used", comment: "VoiceOver value for bytes used"), totalBytes, maxbytes))
|
||||
.frame(width: 130)
|
||||
.padding(5)
|
||||
.font(.subheadline)
|
||||
|
|
|
|||
|
|
@ -39,18 +39,6 @@ struct UserList: View {
|
|||
roleFilter
|
||||
]}
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false),
|
||||
NSSortDescriptor(key: "userNode.favorite", ascending: false),
|
||||
NSSortDescriptor(key: "pkiEncrypted", ascending: false),
|
||||
NSSortDescriptor(key: "userNode.lastHeard", ascending: false),
|
||||
NSSortDescriptor(key: "longName", ascending: true)],
|
||||
predicate: NSPredicate(
|
||||
format: "userNode.ignored == false && longName != '' AND NOT (userNode.viaMqtt == YES AND userNode.hopsAway > 0)"
|
||||
), animation: .default
|
||||
)
|
||||
var users: FetchedResults<UserEntity>
|
||||
|
||||
@Binding var node: NodeInfoEntity?
|
||||
@Binding var userSelection: UserEntity?
|
||||
|
||||
|
|
@ -60,8 +48,23 @@ struct UserList: View {
|
|||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY")
|
||||
VStack {
|
||||
List(selection: $userSelection) {
|
||||
ForEach(users) { (user: UserEntity) in
|
||||
FilteredUserList(
|
||||
searchText: searchText,
|
||||
viaLora: viaLora,
|
||||
viaMqtt: viaMqtt,
|
||||
isOnline: isOnline,
|
||||
isPkiEncrypted: isPkiEncrypted,
|
||||
isFavorite: isFavorite,
|
||||
isIgnored: isIgnored,
|
||||
isEnvironment: isEnvironment,
|
||||
distanceFilter: distanceFilter,
|
||||
maxDistance: maxDistance,
|
||||
hopsAway: hopsAway,
|
||||
roleFilter: roleFilter,
|
||||
deviceRoles: deviceRoles,
|
||||
userSelection: $userSelection
|
||||
) { users in
|
||||
List(users, selection: $userSelection) { (user: UserEntity) in
|
||||
let mostRecent = user.messageList.last
|
||||
let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 ))))
|
||||
let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0
|
||||
|
|
@ -134,18 +137,17 @@ struct UserList: View {
|
|||
.frame(height: 62)
|
||||
.contextMenu {
|
||||
Button {
|
||||
|
||||
if node != nil && !(user.userNode?.favorite ?? false) {
|
||||
let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num))
|
||||
if success {
|
||||
user.userNode?.favorite = !(user.userNode?.favorite ?? true)
|
||||
user.userNode?.favorite = !(user.userNode?.favorite ?? false)
|
||||
Logger.data.info("Favorited a node")
|
||||
}
|
||||
} else {
|
||||
let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num))
|
||||
if success {
|
||||
user.userNode?.favorite = !(user.userNode?.favorite ?? true)
|
||||
Logger.data.info("Un Favorited a node")
|
||||
user.userNode?.favorite = !(user.userNode?.favorite ?? false)
|
||||
Logger.data.info("Unfavorited a node")
|
||||
}
|
||||
}
|
||||
context.refresh(user, mergeChanges: true)
|
||||
|
|
@ -156,7 +158,7 @@ struct UserList: View {
|
|||
Logger.data.error("Save Node Favorite Error")
|
||||
}
|
||||
} label: {
|
||||
Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill")
|
||||
Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill")
|
||||
}
|
||||
Button {
|
||||
user.mute = !user.mute
|
||||
|
|
@ -169,7 +171,7 @@ struct UserList: View {
|
|||
} label: {
|
||||
Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
if user.messageList.count > 0 {
|
||||
if user.messageList.count > 0 {
|
||||
Button(role: .destructive) {
|
||||
isPresentingDeleteUserMessagesConfirm = true
|
||||
userSelection = user
|
||||
|
|
@ -192,66 +194,15 @@ struct UserList: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle(String.localizedStringWithFormat("Contacts (%@)", String(users.count)))
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle(String.localizedStringWithFormat("Contacts (%@)".localized, String(users.count == 0 ? 0 : users.count)))
|
||||
.sheet(isPresented: $editingFilters) {
|
||||
NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles)
|
||||
}
|
||||
.sheet(isPresented: $showingHelp) {
|
||||
DirectMessagesHelp()
|
||||
}
|
||||
.onChange(of: searchText) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: viaLora) {
|
||||
if !viaLora && !viaMqtt {
|
||||
viaMqtt = true
|
||||
}
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: viaMqtt) {
|
||||
if !viaLora && !viaMqtt {
|
||||
viaLora = true
|
||||
}
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: [deviceRoles]) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: hopsAway) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: [boolFilters]) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: maxDistance) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: isPkiEncrypted) {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .leading) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
|
|
@ -282,36 +233,60 @@ struct UserList: View {
|
|||
.padding(5)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.padding(.bottom, 5)
|
||||
.searchable(text: $searchText, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact")
|
||||
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Find a contact")
|
||||
.disableAutocorrection(true)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
}
|
||||
}
|
||||
private func searchUserList() async {
|
||||
}
|
||||
|
||||
/// Case Insensitive Search Text Predicates
|
||||
let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in
|
||||
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
|
||||
}
|
||||
/// Create a compound predicate using each text search preicate as an OR
|
||||
let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates)
|
||||
/// Create an array of predicates to hold our AND predicates
|
||||
struct FilteredUserList<Content: View>: View {
|
||||
@FetchRequest var fetchRequest: FetchedResults<UserEntity>
|
||||
let content: (FetchedResults<UserEntity>) -> Content
|
||||
|
||||
var body: some View {
|
||||
content(fetchRequest)
|
||||
}
|
||||
|
||||
init(
|
||||
searchText: String,
|
||||
viaLora: Bool,
|
||||
viaMqtt: Bool,
|
||||
isOnline: Bool,
|
||||
isPkiEncrypted: Bool,
|
||||
isFavorite: Bool,
|
||||
isIgnored: Bool,
|
||||
isEnvironment: Bool,
|
||||
distanceFilter: Bool,
|
||||
maxDistance: Double,
|
||||
hopsAway: Double,
|
||||
roleFilter: Bool,
|
||||
deviceRoles: Set<Int>,
|
||||
userSelection: Binding<UserEntity?>,
|
||||
@ViewBuilder content: @escaping (FetchedResults<UserEntity>) -> Content
|
||||
) {
|
||||
self.content = content
|
||||
// Build predicates based on filter variables
|
||||
var predicates: [NSPredicate] = []
|
||||
/// Mqtt and lora
|
||||
// Search text predicates
|
||||
if !searchText.isEmpty {
|
||||
let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in
|
||||
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
|
||||
}
|
||||
let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates)
|
||||
predicates.append(textSearchPredicate)
|
||||
}
|
||||
// Mqtt and lora
|
||||
if !(viaLora && viaMqtt) {
|
||||
if viaLora {
|
||||
let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO")
|
||||
predicates.append(loraPredicate)
|
||||
} else {
|
||||
let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES AND userNode.hopsAway == 0")
|
||||
let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES")
|
||||
predicates.append(mqttPredicate)
|
||||
}
|
||||
} else {
|
||||
let mqttPredicate = NSPredicate(format: "NOT (userNode.viaMqtt == YES AND userNode.hopsAway > 0)")
|
||||
predicates.append(mqttPredicate)
|
||||
}
|
||||
/// Roles
|
||||
// Roles
|
||||
if roleFilter && deviceRoles.count > 0 {
|
||||
var rolesArray: [NSPredicate] = []
|
||||
for dr in deviceRoles {
|
||||
|
|
@ -321,33 +296,32 @@ struct UserList: View {
|
|||
let compoundPredicate = NSCompoundPredicate(type: .or, subpredicates: rolesArray)
|
||||
predicates.append(compoundPredicate)
|
||||
}
|
||||
/// Hops Away
|
||||
if hopsAway == 0.0 {
|
||||
// Hops Away
|
||||
if hopsAway == 0 {
|
||||
let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway))
|
||||
predicates.append(hopsAwayPredicate)
|
||||
} else if hopsAway > -1.0 {
|
||||
let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway > 0 AND userNode.hopsAway <= %i", Int32(hopsAway))
|
||||
predicates.append(hopsAwayPredicate)
|
||||
}
|
||||
/// Online
|
||||
// Online
|
||||
if isOnline {
|
||||
let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate)
|
||||
predicates.append(isOnlinePredicate)
|
||||
}
|
||||
/// Encrypted
|
||||
// Encrypted
|
||||
if isPkiEncrypted {
|
||||
let isPkiEncryptedPredicate = NSPredicate(format: "pkiEncrypted == YES")
|
||||
predicates.append(isPkiEncryptedPredicate)
|
||||
}
|
||||
/// Favorites
|
||||
// Favorites
|
||||
if isFavorite {
|
||||
let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES")
|
||||
predicates.append(isFavoritePredicate)
|
||||
}
|
||||
/// Distance
|
||||
// Distance
|
||||
if distanceFilter {
|
||||
let pointOfInterest = LocationsHandler.currentLocation
|
||||
|
||||
if pointOfInterest.latitude != LocationsHandler.DefaultLocation.latitude && pointOfInterest.longitude != LocationsHandler.DefaultLocation.longitude {
|
||||
let d: Double = maxDistance * 1.1
|
||||
let r: Double = 6371009
|
||||
|
|
@ -362,16 +336,29 @@ struct UserList: View {
|
|||
predicates.append(distancePredicate)
|
||||
}
|
||||
}
|
||||
|
||||
if predicates.count > 0 || !searchText.isEmpty {
|
||||
if !searchText.isEmpty {
|
||||
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates])
|
||||
} else {
|
||||
users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
}
|
||||
} else {
|
||||
users.nsPredicate = nil
|
||||
}
|
||||
// Always apply unmessagable and connected node filters
|
||||
// Show unmessagable nodes only if they have messages, otherwise hide them
|
||||
let unmessagablePredicate = NSPredicate(format: "unmessagable == NO")
|
||||
let hasMessagesPredicate = NSPredicate(format: "receivedMessages.@count > 0 OR sentMessages.@count > 0")
|
||||
let isUnmessagablePredicate = NSCompoundPredicate(type: .or, subpredicates: [unmessagablePredicate, hasMessagesPredicate])
|
||||
predicates.append(isUnmessagablePredicate)
|
||||
let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO")
|
||||
predicates.append(isIgnoredPredicate)
|
||||
let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS %@)", String(UserDefaults.preferredPeripheralNum))
|
||||
predicates.append(isConnectedNodePredicate)
|
||||
// Combine all predicates
|
||||
let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
// Initialize the fetch request with the combined predicate
|
||||
_fetchRequest = FetchRequest<UserEntity>(
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(key: "lastMessage", ascending: false),
|
||||
NSSortDescriptor(key: "userNode.favorite", ascending: false),
|
||||
NSSortDescriptor(key: "pkiEncrypted", ascending: false),
|
||||
NSSortDescriptor(key: "userNode.lastHeard", ascending: false),
|
||||
NSSortDescriptor(key: "longName", ascending: true)
|
||||
],
|
||||
predicate: finalPredicate,
|
||||
animation: .spring
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ struct UserMessageList: View {
|
|||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
||||
// Keyboard State
|
||||
@FocusState var messageFieldFocused: Bool
|
||||
// View State Items
|
||||
|
|
@ -24,16 +23,22 @@ struct UserMessageList: View {
|
|||
@State private var showScrollToBottomButton = false
|
||||
@State private var hasReachedBottom = false
|
||||
@State private var gotFirstUnreadMessage: Bool = false
|
||||
|
||||
@State private var messageToHighlight: Int64 = 0
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollViewReader { scrollView in
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach( user.messageList ) { (message: MessageEntity) in
|
||||
ForEach( Array(user.messageList.enumerated()), id: \.element.id) { index, message in
|
||||
// Get the previous message, if it exists
|
||||
let previousMessage = index > 0 ? user.messageList[index - 1] : nil
|
||||
if message.displayTimestamp(aboveMessage: previousMessage) {
|
||||
Text(message.timestamp.formatted(date: .abbreviated, time: .shortened))
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
if user.num != bleManager.connectedPeripheral?.num ?? -1 {
|
||||
let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false)
|
||||
|
||||
|
|
@ -46,7 +51,6 @@ struct UserMessageList: View {
|
|||
messageToHighlight = messageNum
|
||||
}
|
||||
scrollView.scrollTo(messageNum, anchor: .center)
|
||||
|
||||
// Reset highlight after delay
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||
|
|
@ -119,11 +123,11 @@ struct UserMessageList: View {
|
|||
Spacer(minLength: 50)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(.blue, lineWidth: 2)
|
||||
.opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
|
||||
}
|
||||
// .overlay {
|
||||
// RoundedRectangle(cornerRadius: 10)
|
||||
// .stroke(.blue, lineWidth: 2)
|
||||
// .opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
|
||||
// }
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
|
|
@ -163,24 +167,24 @@ struct UserMessageList: View {
|
|||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.onFirstAppear {
|
||||
// Find first unread message
|
||||
if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId {
|
||||
if user.unreadMessages == 0 {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
|
||||
showScrollToBottomButton = true
|
||||
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
}
|
||||
} else {
|
||||
// If no unread messages, scroll to bottom
|
||||
withAnimation {
|
||||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
|
||||
showScrollToBottomButton = true
|
||||
}
|
||||
}
|
||||
}
|
||||
gotFirstUnreadMessage = true
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
|
||||
withAnimation {
|
||||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
showScrollToBottomButton = false
|
||||
}
|
||||
|
|
@ -188,7 +192,7 @@ struct UserMessageList: View {
|
|||
.onChange(of: user.messageList) {
|
||||
if hasReachedBottom {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
|
||||
}
|
||||
} else {
|
||||
showScrollToBottomButton = true
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ struct IgnoreNodeButton: View {
|
|||
Image(systemName: node.ignored ? "minus.circle.fill" : "minus.circle")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
// Accessibility: Label for VoiceOver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,25 +9,28 @@ struct TraceRouteButton: View {
|
|||
private var isPresentingTraceRouteSentAlert: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
RateLimitedButton(key: "traceroute", rateLimit: 30.0) {
|
||||
isPresentingTraceRouteSentAlert = bleManager.sendTraceRouteRequest(
|
||||
destNum: node.user?.num ?? 0,
|
||||
wantResponse: true
|
||||
)
|
||||
} label: {
|
||||
Label {
|
||||
Text("Trace Route")
|
||||
} icon: {
|
||||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
}.alert(
|
||||
"Trace Route Sent",
|
||||
isPresented: $isPresentingTraceRouteSentAlert
|
||||
) {
|
||||
Button("OK") { }.keyboardShortcut(.defaultAction)
|
||||
} message: {
|
||||
Text("This could take a while. The response will appear in the trace route log for the node it was sent to.")
|
||||
} label: { completion in
|
||||
if let completion, completion.percentComplete > 0.0 {
|
||||
Label {
|
||||
Text("Trace Route (in \(completion.secondsRemaining.formatted(.number.precision(.fractionLength(0))))s)")
|
||||
.foregroundStyle(.secondary)
|
||||
} icon: {
|
||||
Image("progress.ring.dashed", variableValue: completion.percentComplete)
|
||||
.foregroundStyle(.secondary)
|
||||
}.disabled(true)
|
||||
} else {
|
||||
Label {
|
||||
Text("Trace Route")
|
||||
} icon: {
|
||||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ struct MeshMapContent: MapContent {
|
|||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
|
||||
@AppStorage("enableMapConvexHull") private var showConvexHull = false
|
||||
@AppStorage("enableMapShowFavorites") private var showFavorites = false
|
||||
@Binding var showTraffic: Bool
|
||||
@Binding var showPointsOfInterest: Bool
|
||||
@Binding var selectedMapLayer: MapLayer
|
||||
|
|
@ -39,11 +40,12 @@ struct MeshMapContent: MapContent {
|
|||
@MapContentBuilder
|
||||
var positionAnnotations: some MapContent {
|
||||
ForEach(positions, id: \.id) { position in
|
||||
/// Node color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
let positionName = position.nodePosition?.user?.longName ?? "?"
|
||||
/// Latest Position Anotations
|
||||
Annotation(positionName, coordinate: position.coordinate) {
|
||||
if !showFavorites || (position.nodePosition?.favorite == true) {
|
||||
/// Node color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
let positionName = position.nodePosition?.user?.longName ?? "?"
|
||||
/// Latest Position Anotations
|
||||
Annotation(positionName, coordinate: position.coordinate) {
|
||||
LazyVStack {
|
||||
ZStack {
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
|
|
@ -59,6 +61,13 @@ struct MeshMapContent: MapContent {
|
|||
.onAppear {
|
||||
self.scale = 1
|
||||
}
|
||||
.onChange(of: showFavorites) {
|
||||
|
||||
scale = 0.5 // Reset to initial state
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
scale = 1
|
||||
}
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
if position.nodePosition?.hasDetectionSensorMetrics ?? false {
|
||||
|
|
@ -130,7 +139,7 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
/// Reduced Precision Map Circles
|
||||
if 10...19 ~= position.precisionBits {
|
||||
if 12...15 ~= position.precisionBits {
|
||||
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
|
||||
let radius: CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
if radius > 0.0 {
|
||||
|
|
@ -141,6 +150,8 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@MapContentBuilder
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ struct NodeMapContent: MapContent {
|
|||
let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771))
|
||||
let headingDegrees = Angle.degrees(Double(position.heading))
|
||||
/// Reduced Precision Map Circle
|
||||
if position.latest && 10...19 ~= position.precisionBits {
|
||||
if position.latest && 12...15 ~= position.precisionBits {
|
||||
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
|
||||
let radius: CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
if radius > 0.0 {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ struct MapSettingsForm: View {
|
|||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var currentDetent = PresentationDetent.medium
|
||||
@AppStorage("meshMapShowNodeHistory") private var nodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var routeLines = false
|
||||
@AppStorage("meshMapShowRouteLines") private var enableMapRouteLines = false
|
||||
@AppStorage("enableMapConvexHull") private var convexHull = false
|
||||
@AppStorage("enableMapWaypoints") private var waypoints = true
|
||||
@AppStorage("enableMapWaypoints") private var enableMapWaypoints = true
|
||||
@AppStorage("enableMapShowFavorites") private var enableMapShowFavorites = false
|
||||
@Binding var traffic: Bool
|
||||
@Binding var pointsOfInterest: Bool
|
||||
@Binding var mapLayer: MapLayer
|
||||
|
|
@ -29,7 +30,7 @@ struct MapSettingsForm: View {
|
|||
Picker(selection: $mapLayer, label: Text("")) {
|
||||
ForEach(MapLayer.allCases, id: \.self) { layer in
|
||||
if layer != MapLayer.offline {
|
||||
Text(layer.localized)
|
||||
Text(layer.localized.capitalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,15 +54,25 @@ struct MapSettingsForm: View {
|
|||
.onChange(of: meshMapDistance) { _, newMeshMapDistance in
|
||||
UserDefaults.meshMapDistance = newMeshMapDistance
|
||||
}
|
||||
Toggle(isOn: $waypoints) {
|
||||
Label("Show Waypoints ", systemImage: "signpost.right.and.left")
|
||||
Toggle(isOn: $enableMapWaypoints) {
|
||||
Label {
|
||||
Text("Show Waypoints")
|
||||
} icon: {
|
||||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
UserDefaults.enableMapWaypoints = !waypoints
|
||||
.tint(.accentColor)
|
||||
}
|
||||
Toggle(isOn: $enableMapShowFavorites) {
|
||||
Label {
|
||||
Text("Favorites")
|
||||
} icon: {
|
||||
Image(systemName: "star.fill")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
}
|
||||
|
||||
.tint(.accentColor)
|
||||
Toggle(isOn: $nodeHistory) {
|
||||
Label("Node History", systemImage: "building.columns.fill")
|
||||
}
|
||||
|
|
@ -70,15 +81,10 @@ struct MapSettingsForm: View {
|
|||
self.nodeHistory.toggle()
|
||||
UserDefaults.enableMapNodeHistoryPins = self.nodeHistory
|
||||
}
|
||||
Toggle(isOn: $routeLines) {
|
||||
Toggle(isOn: $enableMapRouteLines) {
|
||||
Label("Route Lines", systemImage: "road.lanes")
|
||||
}
|
||||
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.routeLines.toggle()
|
||||
UserDefaults.enableMapRouteLines = self.routeLines
|
||||
}
|
||||
.tint(.accentColor)
|
||||
Toggle(isOn: $convexHull) {
|
||||
Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right")
|
||||
}
|
||||
|
|
@ -96,9 +102,14 @@ struct MapSettingsForm: View {
|
|||
UserDefaults.enableMapTraffic = self.traffic
|
||||
}
|
||||
Toggle(isOn: $pointsOfInterest) {
|
||||
Label("Points of Interest", systemImage: "mappin.and.ellipse")
|
||||
Label {
|
||||
Text("Points of Interest")
|
||||
} icon: {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.tint(.accentColor)
|
||||
.onTapGesture {
|
||||
self.pointsOfInterest.toggle()
|
||||
UserDefaults.enableMapPointsOfInterest = self.pointsOfInterest
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ struct PositionPopover: View {
|
|||
if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
|
||||
let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude))
|
||||
Label {
|
||||
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
.font(idiom == .phone ? .callout : .body)
|
||||
} icon: {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import SwiftUI
|
|||
struct WaypointForm: View {
|
||||
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State var waypoint: WaypointEntity
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
|
|
@ -30,6 +31,7 @@ struct WaypointForm: View {
|
|||
@State private var lockedTo: Int64 = 0
|
||||
@State private var detents: Set<PresentationDetent> = [.medium, .fraction(0.85)]
|
||||
@State private var selectedDetent: PresentationDetent = .medium
|
||||
@State private var waypointFailedAlert: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
|
@ -47,7 +49,19 @@ struct WaypointForm: View {
|
|||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
|
||||
}
|
||||
Button {
|
||||
let currentLoc = LocationsHandler.currentLocation
|
||||
waypoint.coordinate.longitude = currentLoc.longitude
|
||||
waypoint.coordinate.latitude = currentLoc.latitude
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Use my Location")
|
||||
Image(systemName: "location")
|
||||
}
|
||||
}
|
||||
.accessibilityLabel("Set to current location")
|
||||
HStack {
|
||||
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
|
||||
DistanceText(meters: distance)
|
||||
|
|
@ -72,6 +86,7 @@ struct WaypointForm: View {
|
|||
name = String(name.dropLast())
|
||||
totalBytes = name.utf8.count
|
||||
}
|
||||
waypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
|
|
@ -167,8 +182,8 @@ struct WaypointForm: View {
|
|||
if bleManager.sendWaypoint(waypoint: newWaypoint) {
|
||||
dismiss()
|
||||
} else {
|
||||
dismiss()
|
||||
Logger.mesh.warning("Send waypoint failed")
|
||||
waypointFailedAlert = true
|
||||
}
|
||||
} else {
|
||||
Logger.mesh.warning("Send waypoint failed, node not connected")
|
||||
|
|
@ -196,11 +211,11 @@ struct WaypointForm: View {
|
|||
|
||||
Menu {
|
||||
Button("For me", action: {
|
||||
bleManager.context.delete(waypoint)
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try bleManager.context.save()
|
||||
try context.save()
|
||||
} catch {
|
||||
bleManager.context.rollback()
|
||||
context.rollback()
|
||||
}
|
||||
dismiss() })
|
||||
Button("For everyone", action: {
|
||||
|
|
@ -225,16 +240,16 @@ struct WaypointForm: View {
|
|||
newWaypoint.expire = UInt32(1)
|
||||
if bleManager.sendWaypoint(waypoint: newWaypoint) {
|
||||
|
||||
bleManager.context.delete(waypoint)
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try bleManager.context.save()
|
||||
try context.save()
|
||||
} catch {
|
||||
bleManager.context.rollback()
|
||||
context.rollback()
|
||||
}
|
||||
dismiss()
|
||||
} else {
|
||||
dismiss()
|
||||
Logger.mesh.warning("Send waypoint failed")
|
||||
waypointFailedAlert = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -256,8 +271,8 @@ struct WaypointForm: View {
|
|||
Text(waypoint.name ?? "?")
|
||||
.font(.largeTitle)
|
||||
Spacer()
|
||||
if waypoint.locked > 0 {
|
||||
Image(systemName: "lock.fill" )
|
||||
if waypoint.locked > 0 && waypoint.locked != UInt32(BLEManager.shared.connectedPeripheral?.num ?? 0) {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.largeTitle)
|
||||
} else {
|
||||
Button {
|
||||
|
|
@ -368,21 +383,32 @@ struct WaypointForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.alert("Waypoint Failed to Send", isPresented: $waypointFailedAlert) {
|
||||
Button("OK", role: .cancel) {
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if waypoint.id == 0 {
|
||||
// New, unsent waypoint created by the user: delete it
|
||||
bleManager.context.delete(waypoint)
|
||||
context.delete(waypoint)
|
||||
do {
|
||||
try bleManager.context.save()
|
||||
try context.save()
|
||||
} catch {
|
||||
bleManager.context.rollback()
|
||||
context.rollback()
|
||||
Logger.mesh.error("Failed to save context on waypoint deletion: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if waypoint.id > 0 {
|
||||
let waypoint = getWaypoint(id: Int64(waypoint.id), context: bleManager.context)
|
||||
let waypoint = getWaypoint(id: Int64(waypoint.id), context: context)
|
||||
name = waypoint.name ?? "Dropped Pin"
|
||||
description = waypoint.longDescription ?? ""
|
||||
icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍")
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ struct NodeDetail: View {
|
|||
Section("Hardware") {
|
||||
NodeInfoItem(node: node)
|
||||
}
|
||||
Section("Node") {
|
||||
.accessibilityElement(children: .combine)
|
||||
Section("Node") { // Node
|
||||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
CircleText(
|
||||
|
|
@ -67,6 +68,7 @@ struct NodeDetail: View {
|
|||
.foregroundColor(getRssiColor(rssi: node.rssi))
|
||||
.font(.caption)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
if node.telemetries?.count ?? 0 > 0 {
|
||||
Spacer()
|
||||
|
|
@ -74,6 +76,7 @@ struct NodeDetail: View {
|
|||
}
|
||||
Spacer()
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.listRowSeparator(.hidden)
|
||||
if let user = node.user {
|
||||
if !user.keyMatch {
|
||||
|
|
@ -86,6 +89,7 @@ struct NodeDetail: View {
|
|||
.foregroundStyle(.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
} icon: {
|
||||
Image(systemName: "key.slash.fill")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
|
|
@ -104,6 +108,7 @@ struct NodeDetail: View {
|
|||
Text(String(node.num))
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
|
||||
HStack {
|
||||
Label {
|
||||
|
|
@ -116,6 +121,32 @@ struct NodeDetail: View {
|
|||
Text(node.num.toHex())
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
|
||||
if node.user?.keyMatch ?? false {
|
||||
if let publicKey = node.user?.publicKey {
|
||||
HStack {
|
||||
Label {
|
||||
Text("Public Key")
|
||||
} icon: {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
Spacer()
|
||||
Button(action: {
|
||||
context.perform{
|
||||
UIPasteboard.general.string = publicKey.base64EncodedString()
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "key.horizontal.fill")
|
||||
Text("Copy")
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
if let metadata = node.metadata {
|
||||
HStack {
|
||||
|
|
@ -129,6 +160,7 @@ struct NodeDetail: View {
|
|||
|
||||
Text(metadata.firmwareVersion ?? "Unknown".localized)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) {
|
||||
|
|
@ -142,6 +174,20 @@ struct NodeDetail: View {
|
|||
Spacer()
|
||||
Text(deviceRole.name)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
if node.user?.unmessagable ?? false {
|
||||
HStack {
|
||||
Label {
|
||||
Text("Messaging")
|
||||
} icon: {
|
||||
Image(systemName: "iphone.slash")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
Spacer()
|
||||
Text("Unmonitored")
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds {
|
||||
|
|
@ -161,6 +207,7 @@ struct NodeDetail: View {
|
|||
Text(uptime)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
|
||||
|
|
@ -179,7 +226,9 @@ struct NodeDetail: View {
|
|||
Text(firstHeard.formatted())
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}.onTapGesture {
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.onTapGesture {
|
||||
dateFormatRelative.toggle()
|
||||
}
|
||||
}
|
||||
|
|
@ -203,7 +252,9 @@ struct NodeDetail: View {
|
|||
Text(lastHeard.formatted())
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}.onTapGesture {
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.onTapGesture {
|
||||
dateFormatRelative.toggle()
|
||||
}
|
||||
}
|
||||
|
|
@ -216,79 +267,84 @@ struct NodeDetail: View {
|
|||
if node.hasPositions && UserDefaults.environmentEnableWeatherKit
|
||||
|| node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "Distance", "soilTemperature", "soilMoisture"]) {
|
||||
Section("Environment") {
|
||||
if !node.hasEnvironmentMetrics {
|
||||
LocalWeatherConditions(location: node.latestPosition?.nodeLocation)
|
||||
} else {
|
||||
VStack {
|
||||
if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 {
|
||||
IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient)
|
||||
.padding(.vertical)
|
||||
}
|
||||
LazyVGrid(columns: gridItemLayout) {
|
||||
if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() {
|
||||
WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP")
|
||||
// Group weather/environment data for better VoiceOver experience
|
||||
VStack {
|
||||
if !node.hasEnvironmentMetrics {
|
||||
LocalWeatherConditions(location: node.latestPosition?.nodeLocation)
|
||||
} else {
|
||||
VStack {
|
||||
if node.latestEnvironmentMetrics?.iaq ?? -1 > 0 {
|
||||
IndoorAirQuality(iaq: Int(node.latestEnvironmentMetrics?.iaq ?? 0), displayMode: .gradient)
|
||||
.padding(.vertical)
|
||||
}
|
||||
if let humidity = node.latestEnvironmentMetrics?.relativeHumidity {
|
||||
if let temperature = node.latestEnvironmentMetrics?.temperature {
|
||||
let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity)
|
||||
.formatted(.number.precision(.fractionLength(0))) + "°"
|
||||
HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint)
|
||||
} else {
|
||||
HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil)
|
||||
LazyVGrid(columns: gridItemLayout) {
|
||||
if let temperature = node.latestEnvironmentMetrics?.temperature?.shortFormattedTemperature() {
|
||||
WeatherConditionsCompactWidget(temperature: String(temperature), symbolName: "cloud.sun", description: "TEMP")
|
||||
}
|
||||
if let humidity = node.latestEnvironmentMetrics?.relativeHumidity {
|
||||
if let temperature = node.latestEnvironmentMetrics?.temperature {
|
||||
let dewPoint = calculateDewPoint(temp: temperature, relativeHumidity: humidity)
|
||||
.formatted(.number.precision(.fractionLength(0))) + "°"
|
||||
HumidityCompactWidget(humidity: Int(humidity), dewPoint: dewPoint)
|
||||
} else {
|
||||
HumidityCompactWidget(humidity: Int(humidity), dewPoint: nil)
|
||||
}
|
||||
}
|
||||
if let pressure = node.latestEnvironmentMetrics?.barometricPressure {
|
||||
PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144)
|
||||
}
|
||||
if let windSpeed = node.latestEnvironmentMetrics?.windSpeed {
|
||||
let windSpeedMeasurement = Measurement(value: Double(windSpeed), unit: UnitSpeed.metersPerSecond)
|
||||
let windGust = node.latestEnvironmentMetrics?.windGust.map { Measurement(value: Double($0), unit: UnitSpeed.metersPerSecond) }
|
||||
let direction = cardinalValue(from: Double(node.latestEnvironmentMetrics?.windDirection ?? 0))
|
||||
WindCompactWidget(speed: windSpeedMeasurement.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))),
|
||||
gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction)
|
||||
}
|
||||
if let rainfall1h = node.latestEnvironmentMetrics?.rainfall1H {
|
||||
let locale = NSLocale.current as NSLocale
|
||||
let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches)
|
||||
let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches
|
||||
let unitLabel = usesMetricSystem ? "mm" : "in"
|
||||
let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters)
|
||||
let decimals = usesMetricSystem ? 0 : 1
|
||||
let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals)))
|
||||
RainfallCompactWidget(timespan: .rainfall1H, rainfall: formattedRain, unit: unitLabel)
|
||||
}
|
||||
if let rainfall24h = node.latestEnvironmentMetrics?.rainfall24H {
|
||||
let locale = NSLocale.current as NSLocale
|
||||
let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches)
|
||||
let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches
|
||||
let unitLabel = usesMetricSystem ? "mm" : "in"
|
||||
let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters)
|
||||
let decimals = usesMetricSystem ? 0 : 1
|
||||
let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals)))
|
||||
RainfallCompactWidget(timespan: .rainfall24H, rainfall: formattedRain, unit: unitLabel)
|
||||
}
|
||||
if let radiation = node.latestEnvironmentMetrics?.radiation {
|
||||
RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr")
|
||||
}
|
||||
if let weight = node.latestEnvironmentMetrics?.weight {
|
||||
WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg")
|
||||
}
|
||||
if let distance = node.latestEnvironmentMetrics?.distance {
|
||||
DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm")
|
||||
}
|
||||
if let soilTemperature = node.latestEnvironmentMetrics?.soilTemperature {
|
||||
let locale = NSLocale.current as NSLocale
|
||||
let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey"))
|
||||
let unit = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? "°F" : "°C"
|
||||
SoilTemperatureCompactWidget(temperature: soilTemperature.localeTemperature().formatted(.number.precision(.fractionLength(0))), unit: unit)
|
||||
}
|
||||
if let soilMoisture = node.latestEnvironmentMetrics?.soilMoisture {
|
||||
SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%")
|
||||
}
|
||||
}
|
||||
if let pressure = node.latestEnvironmentMetrics?.barometricPressure {
|
||||
PressureCompactWidget(pressure: pressure.formatted(.number.precision(.fractionLength(2))), unit: "hPA", low: pressure <= 1009.144)
|
||||
}
|
||||
if let windSpeed = node.latestEnvironmentMetrics?.windSpeed {
|
||||
let windSpeedMeasurement = Measurement(value: Double(windSpeed), unit: UnitSpeed.metersPerSecond)
|
||||
let windGust = node.latestEnvironmentMetrics?.windGust.map { Measurement(value: Double($0), unit: UnitSpeed.metersPerSecond) }
|
||||
let direction = cardinalValue(from: Double(node.latestEnvironmentMetrics?.windDirection ?? 0))
|
||||
WindCompactWidget(speed: windSpeedMeasurement.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))),
|
||||
gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction)
|
||||
}
|
||||
if let rainfall1h = node.latestEnvironmentMetrics?.rainfall1H {
|
||||
let locale = NSLocale.current as NSLocale
|
||||
let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches)
|
||||
let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches
|
||||
let unitLabel = usesMetricSystem ? "mm" : "in"
|
||||
let measurement = Measurement(value: Double(rainfall1h), unit: UnitLength.millimeters)
|
||||
let decimals = usesMetricSystem ? 0 : 1
|
||||
let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals)))
|
||||
RainfallCompactWidget(timespan: .rainfall1H, rainfall: formattedRain, unit: unitLabel)
|
||||
}
|
||||
if let rainfall24h = node.latestEnvironmentMetrics?.rainfall24H {
|
||||
let locale = NSLocale.current as NSLocale
|
||||
let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches)
|
||||
let unit = usesMetricSystem ? UnitLength.millimeters : UnitLength.inches
|
||||
let unitLabel = usesMetricSystem ? "mm" : "in"
|
||||
let measurement = Measurement(value: Double(rainfall24h), unit: UnitLength.millimeters)
|
||||
let decimals = usesMetricSystem ? 0 : 1
|
||||
let formattedRain = measurement.converted(to: unit).value.formatted(.number.precision(.fractionLength(decimals)))
|
||||
RainfallCompactWidget(timespan: .rainfall24H, rainfall: formattedRain, unit: unitLabel)
|
||||
}
|
||||
if let radiation = node.latestEnvironmentMetrics?.radiation {
|
||||
RadiationCompactWidget(radiation: radiation.formatted(.number.precision(.fractionLength(1))), unit: "µR/hr")
|
||||
}
|
||||
if let weight = node.latestEnvironmentMetrics?.weight {
|
||||
WeightCompactWidget(weight: weight.formatted(.number.precision(.fractionLength(1))), unit: "kg")
|
||||
}
|
||||
if let distance = node.latestEnvironmentMetrics?.distance {
|
||||
DistanceCompactWidget(distance: distance.formatted(.number.precision(.fractionLength(0))), unit: "mm")
|
||||
}
|
||||
if let soilTemperature = node.latestEnvironmentMetrics?.soilTemperature {
|
||||
let locale = NSLocale.current as NSLocale
|
||||
let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey"))
|
||||
let unit = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? "°F" : "°C"
|
||||
SoilTemperatureCompactWidget(temperature: soilTemperature.localeTemperature().formatted(.number.precision(.fractionLength(0))), unit: unit)
|
||||
}
|
||||
if let soilMoisture = node.latestEnvironmentMetrics?.soilMoisture {
|
||||
SoilMoistureCompactWidget(moisture: soilMoisture.formatted(.number.precision(.fractionLength(0))), unit: "%")
|
||||
}
|
||||
.padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical)
|
||||
}
|
||||
.padding(node.latestEnvironmentMetrics?.iaq ?? -1 > 0 ? .bottom : .vertical)
|
||||
}
|
||||
}
|
||||
// Apply accessibility properties to the environment section
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
if node.hasPowerMetrics && node.latestPowerMetrics != nil {
|
||||
|
|
@ -298,6 +354,7 @@ struct NodeDetail: View {
|
|||
PowerMetrics(metric: metric)
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
Section("Logs") {
|
||||
|
|
@ -454,12 +511,11 @@ struct NodeDetail: View {
|
|||
let connectedNode,
|
||||
self.bleManager.connectedPeripheral != nil {
|
||||
Section("Administration") {
|
||||
if connectedNode.myInfo?.hasAdmin ?? false {
|
||||
if UserDefaults.enableAdministration {
|
||||
Button {
|
||||
let adminMessageId = bleManager.requestDeviceMetadata(
|
||||
fromUser: connectedNode.user!,
|
||||
toUser: node.user!,
|
||||
adminIndex: connectedNode.myInfo!.adminIndex,
|
||||
context: context
|
||||
)
|
||||
if adminMessageId > 0 {
|
||||
|
|
@ -486,8 +542,7 @@ struct NodeDetail: View {
|
|||
Button("Shutdown Node?", role: .destructive) {
|
||||
if !bleManager.sendShutdown(
|
||||
fromUser: connectedNode.user!,
|
||||
toUser: node.user!,
|
||||
adminIndex: connectedNode.myInfo!.adminIndex
|
||||
toUser: node.user!
|
||||
) {
|
||||
Logger.mesh.warning("Shutdown Failed")
|
||||
}
|
||||
|
|
@ -509,8 +564,7 @@ struct NodeDetail: View {
|
|||
Button("Reboot node?", role: .destructive) {
|
||||
if !bleManager.sendReboot(
|
||||
fromUser: connectedNode.user!,
|
||||
toUser: node.user!,
|
||||
adminIndex: connectedNode.myInfo!.adminIndex
|
||||
toUser: node.user!
|
||||
) {
|
||||
Logger.mesh.warning("Reboot Failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ struct NodeInfoItem: View {
|
|||
.foregroundStyle(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
Spacer()
|
||||
}
|
||||
VStack(alignment: .center) {
|
||||
|
|
@ -49,9 +50,11 @@ struct NodeInfoItem: View {
|
|||
.cornerRadius(5)
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.onAppear {
|
||||
Api().loadDeviceHardwareData { (hw) in
|
||||
for device in hw {
|
||||
|
|
@ -76,9 +79,10 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,20 +91,21 @@ struct NodeListFilter: View {
|
|||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
Toggle(isOn: $isIgnored) {
|
||||
|
||||
Label {
|
||||
Text("Ignored")
|
||||
} icon: {
|
||||
|
||||
Image(systemName: "minus.circle.fill")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
if filterTitle == "Node Filters" {
|
||||
Toggle(isOn: $isIgnored) {
|
||||
|
||||
Label {
|
||||
Text("Ignored")
|
||||
} icon: {
|
||||
|
||||
Image(systemName: "minus.circle.fill")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
Toggle(isOn: $isEnvironment) {
|
||||
Label {
|
||||
Text("Environment")
|
||||
|
|
|
|||
|
|
@ -7,9 +7,96 @@
|
|||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
struct NodeListItem: View {
|
||||
|
||||
// Accessibility: Synthesized description for VoiceOver
|
||||
private var accessibilityDescription: String {
|
||||
var desc = ""
|
||||
if let shortName = node.user?.shortName {
|
||||
// Format the shortName using the String extension method
|
||||
desc = shortName.formatNodeNameForVoiceOver()
|
||||
} else if let longName = node.user?.longName {
|
||||
desc = longName
|
||||
} else {
|
||||
desc = "Unknown".localized + " " + "Node".localized
|
||||
}
|
||||
if connected {
|
||||
desc += ", currently connected"
|
||||
}
|
||||
if node.favorite {
|
||||
desc += ", favorite"
|
||||
}
|
||||
if node.lastHeard != nil {
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .full
|
||||
let relative = formatter.localizedString(for: node.lastHeard!, relativeTo: Date())
|
||||
desc += ", last heard " + relative
|
||||
}
|
||||
if node.isOnline {
|
||||
desc += ", online"
|
||||
} else {
|
||||
desc += ", offline"
|
||||
}
|
||||
let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0))
|
||||
if let roleName = role?.name {
|
||||
desc += ", role: \(roleName)"
|
||||
}
|
||||
if node.hopsAway > 0 {
|
||||
desc += ", \(node.hopsAway) hops away"
|
||||
}
|
||||
if let battery = node.latestDeviceMetrics?.batteryLevel {
|
||||
// Check for plugged in and charging states, same logic as in BatteryCompact and BatteryGauge
|
||||
if battery > 100 {
|
||||
desc += ", " + "Plugged in".localized
|
||||
} else if battery == 100 {
|
||||
desc += ", " + "Charging".localized
|
||||
} else {
|
||||
desc += ", battery \(battery)%"
|
||||
}
|
||||
}
|
||||
// Add distance and heading/bearing if available, but only for non-connected nodes
|
||||
if !connected, let (lastPosition, myCoord) = locationData {
|
||||
let nodeCoord = CLLocation(latitude: lastPosition.nodeCoordinate!.latitude, longitude: lastPosition.nodeCoordinate!.longitude)
|
||||
let metersAway = nodeCoord.distance(from: myCoord)
|
||||
// Distance information
|
||||
let distanceFormatter = LengthFormatter()
|
||||
distanceFormatter.unitStyle = .medium
|
||||
let formattedDistance = distanceFormatter.string(fromMeters: metersAway)
|
||||
// For VoiceOver, prepend 'Distance' (localized)
|
||||
desc += ", " + String(format: "%@: %@", "Distance".localized, formattedDistance)
|
||||
// Add bearing/heading information for VoiceOver
|
||||
let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord)
|
||||
let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees)
|
||||
let formattedHeading = heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0))))
|
||||
// Using a direct format without requiring a new localization key
|
||||
desc += ", " + "Heading".localized + " " + formattedHeading
|
||||
}
|
||||
// Add signal strength if available
|
||||
if node.snr != 0 && !node.viaMqtt {
|
||||
let signalStrength: BLESignalStrength
|
||||
if node.snr < -10 {
|
||||
signalStrength = .weak
|
||||
} else if node.snr < 5 {
|
||||
signalStrength = .normal
|
||||
} else {
|
||||
signalStrength = .strong
|
||||
}
|
||||
let signalString: String
|
||||
switch signalStrength {
|
||||
case .weak:
|
||||
signalString = "Signal strength weak".localized
|
||||
case .normal:
|
||||
signalString = "Signal strength normal".localized
|
||||
case .strong:
|
||||
signalString = "Signal strength strong".localized
|
||||
}
|
||||
desc += ", " + signalString
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
var connected: Bool
|
||||
var connectedNode: Int64
|
||||
|
|
@ -85,6 +172,11 @@ struct NodeListItem: View {
|
|||
let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0))
|
||||
IconAndText(systemName: role?.systemName ?? "figure",
|
||||
text: "Role: \(role?.name ?? "Unknown".localized)")
|
||||
if node.user?.unmessagable ?? false {
|
||||
IconAndText(systemName: "iphone.slash",
|
||||
renderingMode: .multicolor,
|
||||
text: "Unmonitored")
|
||||
}
|
||||
if node.isStoreForwardRouter {
|
||||
IconAndText(systemName: "envelope.arrow.triangle.branch",
|
||||
renderingMode: .multicolor,
|
||||
|
|
@ -167,7 +259,10 @@ struct NodeListItem: View {
|
|||
}
|
||||
.padding(.top, 4)
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
// Accessibility: Make the whole row a single element for VoiceOver
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(accessibilityDescription)
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultIcon: View {
|
||||
|
|
|
|||
92
Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift
Normal file
92
Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// ShareContactQRDialog.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by GitHub Copilot on 5/13/25.
|
||||
|
||||
import SwiftUI
|
||||
import CoreImage.CIFilterBuiltins
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
import CoreData
|
||||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
|
||||
struct ShareContactQRDialog: View {
|
||||
let node: NodeInfo
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
var qrString: String {
|
||||
var contact = SharedContact()
|
||||
contact.nodeNum = node.num
|
||||
contact.user = node.user
|
||||
do {
|
||||
let contactString = try contact.serializedData().base64EncodedString()
|
||||
return ("https://meshtastic.org/v/#" + contactString.base64ToBase64url())
|
||||
} catch {
|
||||
Logger.services.error("Error serializing contact: \(error)")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
var qrImage: UIImage {
|
||||
let context = CIContext()
|
||||
let filter = CIFilter.qrCodeGenerator()
|
||||
filter.setValue(Data(qrString.utf8), forKey: "inputMessage")
|
||||
let transform = CGAffineTransform(scaleX: 10, y: 10)
|
||||
if let outputImage = filter.outputImage?.transformed(by: transform),
|
||||
let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
|
||||
return UIImage(cgImage: cgimg)
|
||||
}
|
||||
return UIImage(systemName: "xmark.circle") ?? UIImage()
|
||||
}
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Text("Share Contact QR")
|
||||
.font(.title2)
|
||||
.padding(.top)
|
||||
Text(node.user.longName)
|
||||
.font(.headline)
|
||||
Image(uiImage: qrImage)
|
||||
.interpolation(.none)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(16)
|
||||
.shadow(radius: 4)
|
||||
Text("Scan this QR code to add \(node.user.longName) to another device.")
|
||||
.font(.subheadline)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondary)
|
||||
ShareLink("Share QR Code & Link",
|
||||
item: Image(uiImage: qrImage),
|
||||
subject: Text("Add Meshtastic Node \(node.user.shortName) as a contact"),
|
||||
message: Text(qrString),
|
||||
preview: SharePreview("Add Meshtastic Node \(node.user.shortName) as a contact",
|
||||
image: Image(uiImage: qrImage))
|
||||
)
|
||||
Button("Done") { dismiss() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: 350)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct ShareContactQRDialog_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
var node = NodeInfo()
|
||||
node.num = 123456
|
||||
var userProto = User()
|
||||
userProto.id = "!1234"
|
||||
userProto.longName = "Bud"
|
||||
userProto.shortName = "Bud"
|
||||
userProto.hwModel = HardwareModel.tbeam
|
||||
userProto.role = Config.DeviceConfig.Role.client
|
||||
userProto.publicKey = Data()
|
||||
node.user = userProto
|
||||
|
||||
return ShareContactQRDialog(node: node)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -28,6 +28,8 @@ struct NodeList: View {
|
|||
@State private var isFavorite = false
|
||||
@State private var isIgnored = false
|
||||
@State private var isEnvironment = false
|
||||
// Force refresh ID to make SwiftUI rebuild the view hierarchy
|
||||
@State private var forceRefreshID = UUID()
|
||||
@State private var distanceFilter = false
|
||||
@State private var maxDistance: Double = 800000
|
||||
@State private var hopsAway: Double = -1.0
|
||||
|
|
@ -38,6 +40,8 @@ struct NodeList: View {
|
|||
@State private var isPresentingPositionFailedAlert = false
|
||||
@State private var isPresentingDeleteNodeAlert = false
|
||||
@State private var deleteNodeId: Int64 = 0
|
||||
@State private var isPresentingShareContactQR = false
|
||||
@State private var shareContactNode: NodeInfoEntity?
|
||||
|
||||
var boolFilters: [Bool] {[
|
||||
isFavorite,
|
||||
|
|
@ -76,13 +80,21 @@ struct NodeList: View {
|
|||
/// Allow users to mute notifications for a node even if they are not connected
|
||||
if let user = node.user {
|
||||
NodeAlertsButton(context: context, node: node, user: user)
|
||||
if !user.unmessagable {
|
||||
Button(action: {
|
||||
shareContactNode = node
|
||||
isPresentingShareContactQR = true
|
||||
}) {
|
||||
Label("Share Contact QR", systemImage: "qrcode")
|
||||
}
|
||||
}
|
||||
}
|
||||
if let connectedNode {
|
||||
/// Favoriting a node requires being connected
|
||||
FavoriteNodeButton(bleManager: bleManager, context: context, node: node)
|
||||
/// Don't show message, trace route, position exchange or delete context menu items for the connected node
|
||||
if connectedNode.num != node.num {
|
||||
if !node.viaMqtt || node.viaMqtt && node.hopsAway == 0 {
|
||||
if !(node.user?.unmessagable ?? true) {
|
||||
Button(action: {
|
||||
if let url = URL(string: "meshtastic:///messages?userNum=\(node.num)") {
|
||||
UIApplication.shared.open(url)
|
||||
|
|
@ -91,21 +103,10 @@ struct NodeList: View {
|
|||
Label("Message", systemImage: "message")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
let traceRouteSent = bleManager.sendTraceRouteRequest(
|
||||
destNum: node.num,
|
||||
wantResponse: true
|
||||
)
|
||||
if traceRouteSent {
|
||||
isPresentingTraceRouteSentAlert = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
isPresentingTraceRouteSentAlert = false
|
||||
}
|
||||
}
|
||||
|
||||
} label: {
|
||||
Label("Trace Route", systemImage: "signpost.right.and.left")
|
||||
}
|
||||
TraceRouteButton(
|
||||
bleManager: bleManager,
|
||||
node: node
|
||||
)
|
||||
Button {
|
||||
let positionSent = bleManager.sendPosition(
|
||||
channel: node.channel,
|
||||
|
|
@ -142,6 +143,7 @@ struct NodeList: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
// Use forceRefreshID to completely rebuild the view when notifications update the selected node
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
List(nodes, id: \.self, selection: $selectedNode) { node in
|
||||
NodeListItem(
|
||||
|
|
@ -231,6 +233,13 @@ struct NodeList: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isPresentingShareContactQR) {
|
||||
if let node = shareContactNode {
|
||||
ShareContactQRDialog(node: node.toProto())
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500)
|
||||
.navigationBarItems(
|
||||
|
|
@ -243,6 +252,8 @@ struct NodeList: View {
|
|||
phoneOnly: true
|
||||
)
|
||||
}
|
||||
// Make sure the ZStack passes through accessibility to the ConnectedDevice component
|
||||
.accessibilityElement(children: .contain)
|
||||
)
|
||||
} content: {
|
||||
if let node = selectedNode {
|
||||
|
|
@ -261,6 +272,7 @@ struct NodeList: View {
|
|||
} label: {
|
||||
Image(systemName: "rectangle")
|
||||
}
|
||||
.accessibilityLabel("Hide sidebar")
|
||||
}
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
|
|
@ -269,6 +281,8 @@ struct NodeList: View {
|
|||
phoneOnly: true
|
||||
)
|
||||
}
|
||||
// Make sure the ZStack passes through accessibility to the ConnectedDevice component
|
||||
.accessibilityElement(children: .contain)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -326,16 +340,40 @@ struct NodeList: View {
|
|||
}
|
||||
.onChange(of: router.navigationState) {
|
||||
if let selected = router.navigationState.nodeListSelectedNodeNum {
|
||||
self.selectedNode = getNodeInfo(id: selected, context: context)
|
||||
// Force a complete view rebuild by generating a new UUID
|
||||
Logger.services.info("Forcing view rebuild with new ID: \(self.forceRefreshID)")
|
||||
// First clear selection
|
||||
self.forceRefreshID = UUID()
|
||||
self.selectedNode = nil
|
||||
// Then after a short delay, set the new selection. Makes it obvious to use page is refreshing too.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||||
// Generate another UUID to ensure view gets rebuilt
|
||||
self.forceRefreshID = UUID()
|
||||
self.selectedNode = getNodeInfo(id: selected, context: context)
|
||||
Logger.services.info("Complete view refresh with node: \(selected, privacy: .public)")
|
||||
}
|
||||
} else {
|
||||
self.selectedNode = nil
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Set up notification observer for forced refreshes from notifications
|
||||
NotificationCenter.default.addObserver(forName: NSNotification.Name("ForceNavigationRefresh"), object: nil, queue: .main) { notification in
|
||||
if let nodeNum = notification.userInfo?["nodeNum"] as? Int64 {
|
||||
// Force complete refresh of view
|
||||
self.forceRefreshID = UUID()
|
||||
self.selectedNode = getNodeInfo(id: nodeNum, context: self.context)
|
||||
Logger.services.info("NodeList directly updated from notification for node: \(nodeNum, privacy: .public)")
|
||||
}
|
||||
}
|
||||
Task {
|
||||
await searchNodeList()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
// Remove observer when view disappears
|
||||
NotificationCenter.default.removeObserver(self, name: NSNotification.Name("ForceNavigationRefresh"), object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func searchNodeList() async {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ struct AboutMeshtastic: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
Link("Help with App Development", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!)
|
||||
Link("Sponsor App Development", destination: URL(string: "https://github.com/sponsors/garthvh")!)
|
||||
.font(.title2)
|
||||
Link("GitHub Repository", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!)
|
||||
.font(.title2)
|
||||
Button("Review the app") {
|
||||
if let scene = UIApplication.shared.connectedScenes
|
||||
|
|
|
|||
|
|
@ -6,13 +6,17 @@ import MapKit
|
|||
import OSLog
|
||||
|
||||
struct AppSettings: View {
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@State var totalDownloadedTileSize = ""
|
||||
@State private var isPresentingCoreDataResetConfirm = false
|
||||
@State private var isPresentingDeleteMapTilesConfirm = false
|
||||
@State private var purgeStaleNodes: Bool = false
|
||||
@AppStorage("purgeStaleNodeDays") private var purgeStaleNodeDays: Double = 0
|
||||
@AppStorage("environmentEnableWeatherKit") private var environmentEnableWeatherKit: Bool = true
|
||||
@AppStorage("enableAdministration") private var enableAdministration: Bool = false
|
||||
@AppStorage("usageDataAndCrashReporting") private var usageDataAndCrashReporting: Bool = true
|
||||
var body: some View {
|
||||
VStack {
|
||||
Form {
|
||||
|
|
@ -30,6 +34,13 @@ struct AppSettings: View {
|
|||
Text("PKI based node administration, requires firmware version 2.5+")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
Toggle(isOn: $usageDataAndCrashReporting) {
|
||||
Label("Usage and Crash Data", systemImage: "pencil.and.list.clipboard")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Provide anonymous usage statistics and crash reports.")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
Section(header: Text("environment")) {
|
||||
VStack(alignment: .leading) {
|
||||
|
|
@ -40,6 +51,39 @@ struct AppSettings: View {
|
|||
}
|
||||
}
|
||||
Section(header: Text("App Data")) {
|
||||
Toggle(isOn: $purgeStaleNodes ) {
|
||||
Label {
|
||||
Text("Clear Stale Nodes")
|
||||
} icon: {
|
||||
Image(systemName: "list.bullet.circle")
|
||||
}
|
||||
}
|
||||
.onFirstAppear {
|
||||
purgeStaleNodes = purgeStaleNodeDays > 0
|
||||
Logger.services.info("ℹ️ Purge Stale Nodes toggle initialized to \(purgeStaleNodes)")
|
||||
}
|
||||
.onChange(of: purgeStaleNodes) { _, newValue in
|
||||
purgeStaleNodeDays = purgeStaleNodeDays > 0 ? purgeStaleNodeDays : 7
|
||||
purgeStaleNodeDays = newValue ? purgeStaleNodeDays : 0
|
||||
Logger.services.info("ℹ️ Purge Stale Nodes changed to \(purgeStaleNodeDays)")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
.listRowSeparator(purgeStaleNodes ? .hidden : .visible)
|
||||
if purgeStaleNodes {
|
||||
VStack(alignment: .leading) {
|
||||
Text(String(localized: "After \(Int(purgeStaleNodeDays)) Days"))
|
||||
Slider(value: $purgeStaleNodeDays, in: 1...180, step: 1) {
|
||||
} minimumValueLabel: {
|
||||
Text("1")
|
||||
} maximumValueLabel: {
|
||||
Text("180")
|
||||
}
|
||||
}
|
||||
Text("Favorited and ignored nodes are always retained. Nodes without PKC keys are cleared from the app database on the schedule set by the user, nodes with PKC keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database.")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(idiom == .phone ? .caption : .callout)
|
||||
}
|
||||
Button {
|
||||
isPresentingCoreDataResetConfirm = true
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -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,6 +125,7 @@ struct Channels: View {
|
|||
.brightness(0.1)
|
||||
VStack {
|
||||
HStack {
|
||||
ChannelLock(channel: channel)
|
||||
if channel.name?.isEmpty ?? false {
|
||||
if channel.role == 1 {
|
||||
Text(String("PrimaryChannel").camelCaseToWords()).font(.headline)
|
||||
|
|
@ -239,6 +241,7 @@ struct Channels: View {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil {
|
||||
|
||||
Button {
|
||||
|
|
@ -279,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 {
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ struct ChannelForm: View {
|
|||
.listRowSeparator(.visible)
|
||||
.onChange(of: preciseLocation) { _, pl in
|
||||
if pl == false {
|
||||
positionPrecision = 14
|
||||
positionPrecision = 15
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -157,11 +157,11 @@ struct ChannelForm: View {
|
|||
VStack(alignment: .leading) {
|
||||
Label("Approximate Location", systemImage: "location.slash.circle.fill")
|
||||
|
||||
Slider(value: $positionPrecision, in: 11...14, step: 1) {
|
||||
Slider(value: $positionPrecision, in: 12...15, step: 1) {
|
||||
} minimumValueLabel: {
|
||||
Image(systemName: "minus")
|
||||
} maximumValueLabel: {
|
||||
Image(systemName: "plus")
|
||||
} maximumValueLabel: {
|
||||
Image(systemName: "minus")
|
||||
}
|
||||
Text(PositionPrecision(rawValue: Int(positionPrecision))?.description ?? "")
|
||||
.foregroundColor(.gray)
|
||||
|
|
@ -228,7 +228,7 @@ struct ChannelForm: View {
|
|||
.onChange(of: positionsEnabled) { _, pe in
|
||||
if pe {
|
||||
if positionPrecision == 0 {
|
||||
positionPrecision = 14
|
||||
positionPrecision = 15
|
||||
}
|
||||
} else {
|
||||
positionPrecision = 0
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ struct BluetoothConfig: View {
|
|||
bc.enabled = enabled
|
||||
bc.mode = BluetoothModes(rawValue: mode)?.protoEnumValue() ?? Config.BluetoothConfig.PairingMode.randomPin
|
||||
bc.fixedPin = UInt32(fixedPin) ?? 123456
|
||||
let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -111,12 +111,11 @@ struct BluetoothConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.bluetoothConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired bluetooth config requesting via PKI admin")
|
||||
_ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty bluetooth config")
|
||||
_ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ struct ConfigHeader<T>: View {
|
|||
|
||||
var body: some View {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
Text("There has been no response to a request for device metadata via PKC admin for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ struct ConfigHeader<T>: View {
|
|||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
let expiration = node?.sessionExpiration ?? Date()
|
||||
if node?[keyPath: config] == nil || expiration < node?.sessionExpiration ?? Date() {
|
||||
Text("\(title) config data was requested over the admin channel but no response has been returned from the remote node.")
|
||||
Text("\(title) config data was requested via PKC admin but no response has been returned from the remote node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -204,11 +204,11 @@ struct DeviceConfig: View {
|
|||
.controlSize(.regular)
|
||||
.padding(.trailing)
|
||||
.confirmationDialog(
|
||||
"All device and app data will be deleted.",
|
||||
"Factory reset will delete device and app data.",
|
||||
isPresented: $isPresentingFactoryResetConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Factory reset your device and app? ", role: .destructive) {
|
||||
Button("Delete all config? ", role: .destructive) {
|
||||
if bleManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
bleManager.disconnectPeripheral()
|
||||
|
|
@ -218,6 +218,16 @@ struct DeviceConfig: View {
|
|||
Logger.mesh.error("Factory Reset Failed")
|
||||
}
|
||||
}
|
||||
Button("Delete all config, keys and BLE bonds? ", role: .destructive) {
|
||||
if bleManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!, resetDevice: true) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
bleManager.disconnectPeripheral()
|
||||
clearCoreDataDatabase(context: context, includeRoutes: false)
|
||||
}
|
||||
} else {
|
||||
Logger.mesh.error("Factory Reset Failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -235,7 +245,7 @@ struct DeviceConfig: View {
|
|||
dc.disableTripleClick = !tripleClickAsAdHocPing
|
||||
dc.tzdef = tzdef
|
||||
dc.ledHeartbeatDisabled = !ledHeartbeatEnabled
|
||||
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -268,13 +278,12 @@ struct DeviceConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.deviceConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired device config requesting via PKI admin")
|
||||
_ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
if node.deviceConfig == nil {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty device config")
|
||||
_ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ struct DisplayConfig: View {
|
|||
@State var oledType = 0
|
||||
@State var displayMode = 0
|
||||
@State var units = 0
|
||||
@State var use12HourClock = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
|
@ -74,6 +75,11 @@ struct DisplayConfig: View {
|
|||
.font(.callout)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Toggle(isOn: $use12HourClock) {
|
||||
Label("12 Hour Clock", systemImage: "clock")
|
||||
Text("Sets the screen clock format to 12-hour.")
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
}
|
||||
Section(header: Text("Timing & Format")) {
|
||||
VStack(alignment: .leading) {
|
||||
|
|
@ -141,8 +147,9 @@ struct DisplayConfig: View {
|
|||
dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue()
|
||||
dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue()
|
||||
dc.units = Units(rawValue: units)!.protoEnumValue()
|
||||
dc.use12HClock = use12HourClock
|
||||
|
||||
let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
|
|
@ -174,12 +181,11 @@ struct DisplayConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.displayConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired display config requesting via PKI admin")
|
||||
_ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty display config")
|
||||
_ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -212,6 +218,9 @@ struct DisplayConfig: View {
|
|||
.onChange(of: units) { oldUnits, newUnits in
|
||||
if oldUnits != newUnits && newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true }
|
||||
}
|
||||
.onChange(of: use12HourClock) { oldUse12HourClock, newUse12HourClock in
|
||||
if oldUse12HourClock != newUse12HourClock && newUse12HourClock != node?.displayConfig?.use12HClock { hasChanges = true }
|
||||
}
|
||||
}
|
||||
func setDisplayValues() {
|
||||
self.gpsFormat = Int(node?.displayConfig?.gpsFormat ?? 0)
|
||||
|
|
@ -223,6 +232,7 @@ struct DisplayConfig: View {
|
|||
self.oledType = Int(node?.displayConfig?.oledType ?? 0)
|
||||
self.displayMode = Int(node?.displayConfig?.displayMode ?? 0)
|
||||
self.units = Int(node?.displayConfig?.units ?? 0)
|
||||
self.hasChanges = false
|
||||
self.use12HourClock = node?.displayConfig?.use12HClock ?? false
|
||||
self.hasChanges = node?.displayConfig?.use12HClock ?? false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ struct LoRaConfig: View {
|
|||
if connectedNode?.num ?? -1 == node?.user?.num ?? 0 {
|
||||
UserDefaults.modemPreset = modemPreset
|
||||
}
|
||||
let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -249,12 +249,13 @@ struct LoRaConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.loRaConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired lora config requesting via PKI admin")
|
||||
_ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
if connectedNode.user != nil && node.user != nil {
|
||||
_ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty lora config")
|
||||
_ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ struct AmbientLightingConfig: View {
|
|||
al.blue = UInt32(components.blue * 255)
|
||||
}
|
||||
|
||||
let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -96,12 +96,11 @@ struct AmbientLightingConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.ambientLightingConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired ambient lighting module config requesting via PKI admin")
|
||||
_ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty ambient lighting module config")
|
||||
_ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ struct CannedMessagesConfig: View {
|
|||
totalBytes = messages.utf8.count
|
||||
}
|
||||
hasMessagesChanges = true
|
||||
hasChanges = true
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
|
@ -201,7 +202,7 @@ struct CannedMessagesConfig: View {
|
|||
cmc.inputbrokerEventCw = InputEventChars(rawValue: inputbrokerEventCw)!.protoEnumValue()
|
||||
cmc.inputbrokerEventCcw = InputEventChars(rawValue: inputbrokerEventCcw)!.protoEnumValue()
|
||||
cmc.inputbrokerEventPress = InputEventChars(rawValue: inputbrokerEventPress)!.protoEnumValue()
|
||||
let adminMessageId = bleManager.saveCannedMessageModuleConfig(config: cmc, fromUser: node!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveCannedMessageModuleConfig(config: cmc, fromUser: node!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -211,7 +212,7 @@ struct CannedMessagesConfig: View {
|
|||
}
|
||||
}
|
||||
if hasMessagesChanges {
|
||||
let adminMessageId = bleManager.saveCannedMessageModuleMessages(messages: messages, fromUser: node!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveCannedMessageModuleMessages(messages: messages, fromUser: node!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -244,12 +245,11 @@ struct CannedMessagesConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.cannedMessageConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired canned messages module config requesting via PKI admin")
|
||||
_ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty canned messages module config")
|
||||
_ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ struct DetectionSensorConfig: View {
|
|||
dsc.usePullup = self.usePullup
|
||||
dsc.minimumBroadcastSecs = UInt32(self.minimumBroadcastSecs)
|
||||
dsc.stateBroadcastSecs = UInt32(self.stateBroadcastSecs)
|
||||
let adminMessageId = bleManager.saveDetectionSensorModuleConfig(config: dsc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveDetectionSensorModuleConfig(config: dsc, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -202,12 +202,11 @@ struct DetectionSensorConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.detectionSensorConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired detection sensor module config requesting via PKI admin")
|
||||
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty detection sensor module config")
|
||||
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ struct ExternalNotificationConfig: View {
|
|||
enc.outputMs = UInt32(outputMilliseconds)
|
||||
enc.usePwm = usePWM
|
||||
enc.useI2SAsBuzzer = useI2SAsBuzzer
|
||||
let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -210,12 +210,11 @@ struct ExternalNotificationConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.externalNotificationConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired external notificaiton module config requesting via PKI admin")
|
||||
_ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty external notificaiton module config")
|
||||
_ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ struct MQTTConfig: View {
|
|||
Label("I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT.", systemImage: "hand.raised")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
|
|
@ -130,11 +129,11 @@ struct MQTTConfig: View {
|
|||
Text("To comply with privacy laws like CCPA and GDPR, we avoid sharing exact location data. Instead, we use anonymized or approximate (imprecise) location information to protect your privacy.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
Slider(value: $mapPositionPrecision, in: 11...14, step: 1) {
|
||||
Slider(value: $mapPositionPrecision, in: 12...15, step: 1) {
|
||||
} minimumValueLabel: {
|
||||
Image(systemName: "minus")
|
||||
} maximumValueLabel: {
|
||||
Image(systemName: "plus")
|
||||
} maximumValueLabel: {
|
||||
Image(systemName: "minus")
|
||||
}
|
||||
Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "")
|
||||
.foregroundColor(.gray)
|
||||
|
|
@ -269,7 +268,7 @@ struct MQTTConfig: View {
|
|||
mqtt.mapReportingEnabled = self.mapReportingEnabled
|
||||
mqtt.mapReportSettings.positionPrecision = UInt32(self.mapPositionPrecision)
|
||||
mqtt.mapReportSettings.publishIntervalSecs = UInt32(self.mapPublishIntervalSecs)
|
||||
let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -361,12 +360,11 @@ struct MQTTConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.mqttConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired mqtt module config requesting via PKI admin")
|
||||
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty mqtt module config")
|
||||
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -430,8 +428,13 @@ struct MQTTConfig: View {
|
|||
self.tlsEnabled = node?.mqttConfig?.tlsEnabled ?? false
|
||||
self.mqttConnected = bleManager.mqttProxyConnected
|
||||
self.mapReportingEnabled = node?.mqttConfig?.mapReportingEnabled ?? false
|
||||
self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600)
|
||||
if node?.mqttConfig?.mapPublishIntervalSecs ?? 0 < 3600 {
|
||||
self.mapPublishIntervalSecs = 3600
|
||||
} else {
|
||||
self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600)
|
||||
}
|
||||
self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 14)
|
||||
self.mapReportingOptIn = UserDefaults.mapReportingOptIn
|
||||
if mapPositionPrecision < 11 || mapPositionPrecision > 14 {
|
||||
self.mapPositionPrecision = 14
|
||||
self.hasChanges = true
|
||||
|
|
|
|||
|
|
@ -69,12 +69,11 @@ struct PaxCounterConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.paxCounterConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired pax counter module config requesting via PKI admin")
|
||||
_ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty pax counter module config")
|
||||
_ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -101,8 +100,7 @@ struct PaxCounterConfig: View {
|
|||
let adminMessageId = bleManager.savePaxcounterModuleConfig(
|
||||
config: config,
|
||||
fromUser: fromUser,
|
||||
toUser: toUser,
|
||||
adminIndex: connectedNode.myInfo?.adminIndex ?? 0
|
||||
toUser: toUser
|
||||
)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ struct RangeTestConfig: View {
|
|||
rtc.enabled = enabled
|
||||
rtc.save = save
|
||||
rtc.sender = UInt32(sender)
|
||||
let adminMessageId = bleManager.saveRangeTestModuleConfig(config: rtc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveRangeTestModuleConfig(config: rtc, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -92,12 +92,11 @@ struct RangeTestConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.rangeTestConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired range test module config requesting via PKI admin")
|
||||
_ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty range test module config")
|
||||
_ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ struct RtttlConfig: View {
|
|||
SaveConfigButton(node: node, hasChanges: $hasChanges) {
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if connectedNode != nil {
|
||||
let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone.trimmingCharacters(in: .whitespacesAndNewlines), fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone.trimmingCharacters(in: .whitespacesAndNewlines), fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -83,12 +83,11 @@ struct RtttlConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.rtttlConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired ringtone module config requesting via PKI admin")
|
||||
_ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty ringtone module config")
|
||||
_ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ struct SerialConfig: View {
|
|||
sc.overrideConsoleSerialPort = overrideConsoleSerialPort
|
||||
sc.mode = SerialModeTypes(rawValue: mode)!.protoEnumValue()
|
||||
|
||||
let adminMessageId = bleManager.saveSerialModuleConfig(config: sc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveSerialModuleConfig(config: sc, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
|
|
@ -147,12 +147,11 @@ struct SerialConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.serialConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired serial module config requesting via PKI admin")
|
||||
_ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty serial module config")
|
||||
_ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ struct StoreForwardConfig: View {
|
|||
sfc.records = UInt32(self.records)
|
||||
sfc.historyReturnMax = UInt32(self.historyReturnMax)
|
||||
sfc.historyReturnWindow = UInt32(self.historyReturnWindow)
|
||||
let adminMessageId = bleManager.saveStoreForwardModuleConfig(config: sfc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveStoreForwardModuleConfig(config: sfc, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -148,12 +148,11 @@ struct StoreForwardConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.storeForwardConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired store & forward module config requesting via PKI admin")
|
||||
_ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty store & forward module config")
|
||||
_ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ struct TelemetryConfig: View {
|
|||
tc.powerMeasurementEnabled = powerMeasurementEnabled
|
||||
tc.powerUpdateInterval = UInt32(powerUpdateInterval)
|
||||
tc.powerScreenEnabled = powerScreenEnabled
|
||||
let adminMessageId = bleManager.saveTelemetryModuleConfig(config: tc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let adminMessageId = bleManager.saveTelemetryModuleConfig(config: tc, fromUser: connectedNode!.user!, toUser: node!.user!)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
|
|
@ -145,12 +145,11 @@ struct TelemetryConfig: View {
|
|||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.telemetryConfig == nil {
|
||||
Logger.mesh.info("⚙️ Empty or expired telemetry module config requesting via PKI admin")
|
||||
_ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
_ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin, empty telemetry module config")
|
||||
_ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue