Merge branch 'main' of github.com:meshtastic/Meshtastic-Apple

This commit is contained in:
Nikola Dašić 2025-04-06 09:29:15 +02:00
commit 4534676fc7
119 changed files with 5067 additions and 4031 deletions

View file

@ -16,6 +16,6 @@
- [ ] My code adheres to the project's coding and style guidelines.
- [ ] I have conducted a self-review of my code.
- [ ] I have commented my code, particularly in complex areas.
- [ ] I have made corresponding changes to the documentation.
- [ ] I have verified whether these changes require an update to existing documentation or if new documentation is needed, and created an issue in the [docs repo](http://github.com/meshtastic/meshtastic/issues) if applicable.
- [ ] I have tested the change to ensure that it works as intended.

View file

@ -18,5 +18,6 @@ jobs:
- name: Stale PR+Issues
uses: actions/stale@v9.0.0
with:
days-before-stale: 30
exempt-issue-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue'
exempt-pr-labels: 'has sponsor,needs sponsor,help wanted,backlog,security issue'

View file

@ -4,29 +4,31 @@ Thank you for considering contributing to Meshtastic! We appreciate your time an
## Table of Contents
1. [Getting Started](#getting-started)
2. [Development Workflow](#development-workflow)
- [Targeting `main`](#targeting-main)
- [Small, Incremental Changes](#small-incremental-changes)
- [Rebase Commits](#rebase-commits)
3. [Creating a Branch](#creating-a-branch)
4. [Making Changes](#making-changes)
5. [Commit Messages](#commit-messages)
6. [Merging Changes](#merging-changes)
7. [Testing](#testing)
8. [Code Review](#code-review)
9. [Documentation](#documentation)
10. [Style Guides](#style-guides)
- [Contributing to Meshtastic](#contributing-to-meshtastic)
- [Table of Contents](#table-of-contents)
- [Getting Started](#getting-started)
- [Development Workflow](#development-workflow)
- [Targeting `main`](#targeting-main)
- [Small, Incremental Changes](#small-incremental-changes)
- [Rebase Commits](#rebase-commits)
- [Creating a Branch](#creating-a-branch)
- [Making Changes](#making-changes)
- [Commit Messages](#commit-messages)
- [Merging Changes](#merging-changes)
- [Testing](#testing)
- [Code Review](#code-review)
- [Documentation](#documentation)
- [Style Guides](#style-guides)
- [Git Commit Messages](#git-commit-messages)
- [Code Style](#code-style)
11. [Community](#community)
- [Community](#community)
## Getting Started
1. Fork the repository on GitLab.
1. Fork the repository on GitHub.
2. Clone your fork to your local machine:
```sh
git clone https://gitlab.com/<your-username>/Meshtastic-Apple.git
git clone https://github.com/<your-username>/Meshtastic-Apple.git
```
3. Navigate to the project directory:
```sh

View file

@ -2210,22 +2210,6 @@
}
}
},
"Backup Database" : {
"localizations" : {
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Резервна база података"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "备份数据库"
}
}
}
},
"Bandwidth" : {
"localizations" : {
"de" : {
@ -4281,6 +4265,9 @@
}
}
}
},
"Community Support" : {
},
"Config" : {
"localizations" : {
@ -5642,6 +5629,9 @@
}
}
}
},
"Confirm" : {
},
"Connect to a Node" : {
"localizations" : {
@ -5658,6 +5648,9 @@
}
}
}
},
"Connect to new radio?" : {
},
"connected" : {
"localizations" : {
@ -5738,6 +5731,9 @@
}
}
}
},
"Connected Radio" : {
},
"connected.radio" : {
"localizations" : {
@ -5866,6 +5862,9 @@
}
}
}
},
"Connecting to a new radio will clear all app data on the phone." : {
},
"Connection Attempt %lld of 10" : {
"localizations" : {
@ -6377,6 +6376,9 @@
}
}
}
},
"Currently showing modules that may not be supported by this node." : {
},
"Currently the recommended way to update ESP32 devices is using the web flasher on a desktop computer from a chrome based browser. It does not work on mobile devices or over BLE." : {
"localizations" : {
@ -9311,6 +9313,9 @@
}
}
}
},
"Enable broadcasting packets via UDP over the local network." : {
},
"Enable Notifications" : {
"localizations" : {
@ -9327,6 +9332,9 @@
}
}
}
},
"Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM." : {
},
"enabled" : {
"localizations" : {
@ -9424,21 +9432,8 @@
}
}
},
"Enables the store and forward module. Store and forward must be enabled on both client and router devices." : {
"localizations" : {
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Омогућава модул за чување и пренос. Чување и пренос мора бити омогућено на оба уређаја, клијенту и рутеру."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "启用存储和转发模块。客户端和路由器设备都必须启用存储和转发功能。"
}
}
}
"Enables the store and forward module." : {
},
"Enabling Ethernet will disable the bluetooth connection to the app." : {
"localizations" : {
@ -10694,6 +10689,9 @@
}
}
}
},
"Full Support" : {
},
"gas" : {
"extractionState" : "manual",
@ -22146,21 +22144,8 @@
}
}
},
"OTA Updates are not supported on the this NRF Device." : {
"localizations" : {
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "ОТА ажурирања нису подржана на овом NRF уређају."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "OTA 更新不支持 NRF 设备"
}
}
}
"OTA Updates are not supported on this NRF Device." : {
},
"OTA Updates are not supported on your platform." : {
"localizations" : {
@ -23546,6 +23531,9 @@
}
}
}
},
"Radiation" : {
},
"Radio Disconnected" : {
"extractionState" : "manual",
@ -24633,22 +24621,6 @@
}
}
},
"restore" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wiederherstellen"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Обнова"
}
}
}
},
"resume" : {
"localizations" : {
"de" : {
@ -24994,26 +24966,6 @@
}
}
},
"Router" : {
"localizations" : {
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Рутер"
}
}
}
},
"Router Options" : {
"localizations" : {
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Опције рутера"
}
}
}
},
"Routes" : {
"localizations" : {
"sr" : {
@ -27078,6 +27030,9 @@
}
}
}
},
"Send a heartbeat to advertise the server's presence." : {
},
"Send a message to a certain meshtastic channel" : {
"localizations" : {
@ -27827,6 +27782,9 @@
}
}
}
},
"Server Option" : {
},
"Set" : {
"localizations" : {
@ -27985,6 +27943,9 @@
}
}
}
},
"Settings" : {
},
"Share QR Code & Link" : {
"localizations" : {
@ -28337,6 +28298,9 @@
}
}
}
},
"Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press start the live activity." : {
},
"Shut Down" : {
"localizations" : {
@ -28452,6 +28416,12 @@
}
}
}
},
"Soil Moisture" : {
},
"Soil Temp" : {
},
"Specifies how long the monitored GPIO should output." : {
"localizations" : {
@ -28821,25 +28791,8 @@
}
}
},
"Store and forward clients can request history from routers on the network." : {
"localizations" : {
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Клијенти за складиштење и прослеђивање могу затражити историју од рутера на мрежи."
}
}
}
},
"Store and forward router devices require a ESP32 device with PSRAM." : {
"localizations" : {
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Рутер за складиштење и прослеђивање захтева ESP32 уређај са PSRAM."
}
}
}
"Store and forward servers require an ESP32 device with PSRAM or Linux Native." : {
},
"storeforward.heartbeat" : {
"localizations" : {
@ -30119,6 +30072,9 @@
}
}
}
},
"The Router roles are designed for high vantage locations like mountaintops and towers. This node needs to be able to have a good direct connection to most of the nodes on the network or else this will significantly hurt the network." : {
},
"The secondary public key authorized to send admin messages to this node." : {
"localizations" : {
@ -30292,6 +30248,9 @@
}
}
}
},
"This node does not support any configurable modules." : {
},
"This will disable fixed position and remove the currently set position." : {
"localizations" : {
@ -30531,134 +30490,6 @@
}
}
},
"tip.bluetooth.connect.message" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Affiche les informations de la radio Lora connectée via le bluetooth. Vous pouvez faire un glissé vers la gauche pour déconnecter la radio et un appui long pour voir les statistiques ou démarrer l'activité en direct."
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "מראה מידע אודות מכשיר המשטסטיק המחובר כעת לבלוטוס. ניתן לגרור שמאלה להתנתקות או לחיצה ארוכה לראות סטטיסטיקה או להתחיל פעילות."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mostra informações para o rádio LoRa conectado via bluetooth. Você pode deslizar para a esquerda para desconectar o rádio e pressionar por um longo período para ver estatísticas ou iniciar a atividade ao vivo."
}
},
"se" : {
"stringUnit" : {
"state" : "translated",
"value" : "Visar information för LoRa-radion ansluten via bluetooth. Du kan svepa åt vänster för att koppla från radion och långtryck för att visa statistik eller starta liveaktivitet."
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Приказује информације за LoRA радио повезан преко Блутута. Можете превући лево да бисте одспојили радио и дуго притиснути да бисте погледали статистику или започели активност у реалном времену."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "显示当前通过蓝牙连接的 Lora 电台的信息。您可以向左滑动断开电台,长按查看统计信息或开始实时活动。"
}
},
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "translated",
"value" : "顯示目前通過藍芽連接的 Lora 電台的信息。您可以向左滑動斷開電台,長按查看統計訊息或開始即時活動。"
}
}
}
},
"tip.bluetooth.connect.title" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connected LoRa Radio"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connected Radio"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Radio connectée"
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "מכשיר מחובר"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connected LoRa Radio"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "Rádio Conectado"
}
},
"se" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ansluten Radio"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Радио повезан"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "电台已连接"
}
},
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "translated",
"value" : "連接到 LoRa 電台"
}
}
}
},
"tip.channel.admin.message" : {
"localizations" : {
"de" : {
@ -31442,6 +31273,9 @@
}
}
}
},
"UDP Broadcast" : {
},
"Ukraine 433mhz" : {
"extractionState" : "manual",
@ -32622,6 +32456,9 @@
}
}
}
},
"Weight" : {
},
"What does the lock mean?" : {
"localizations" : {

View file

@ -11,13 +11,23 @@
231B3F222D087A4C0069A07D /* MetricsColumnList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */; };
231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */; };
231B3F272D0885240069A07D /* MetricsColumnDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F262D0885240069A07D /* MetricsColumnDetail.swift */; };
233E99B62D849C3D00CC3A77 /* WeatherConditionsCompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99B52D849C3D00CC3A77 /* WeatherConditionsCompactWidget.swift */; };
233E99B82D849C6500CC3A77 /* HumidityCompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99B72D849C6500CC3A77 /* HumidityCompactWidget.swift */; };
233E99BA2D849C7000CC3A77 /* PressureCompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99B92D849C7000CC3A77 /* PressureCompactWidget.swift */; };
233E99BC2D849C8C00CC3A77 /* WindCompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99BB2D849C8C00CC3A77 /* WindCompactWidget.swift */; };
233E99BE2D849D3200CC3A77 /* RadiationCompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99BD2D849D3200CC3A77 /* RadiationCompactWidget.swift */; };
233E99C12D849D6000CC3A77 /* DistanceCompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99C02D849D6000CC3A77 /* DistanceCompactWidget.swift */; };
233E99C32D849D7A00CC3A77 /* WeightCompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99C22D849D7A00CC3A77 /* WeightCompactWidget.swift */; };
233E99C52D84A0B600CC3A77 /* CompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99C42D84A0B600CC3A77 /* CompactWidget.swift */; };
233E99C72D84A70900CC3A77 /* SoilCompactWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99C62D84A70900CC3A77 /* SoilCompactWidgets.swift */; };
233E99CB2D85AAA900CC3A77 /* RainfallCompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99CA2D85AAA900CC3A77 /* RainfallCompactWidget.swift */; };
2344A2AB2D66974300170A77 /* ManagedAttributePropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */; };
2344A2AF2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2344A2AD2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift */; };
2344A2B02D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2344A2AE2D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift */; };
2344A2B12D68DFF800170A77 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C49D8F2C471AEA0024FBD1 /* Constants.swift */; };
2373AE132D0A216C0086C749 /* MetricsChartSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE122D0A216C0086C749 /* MetricsChartSeries.swift */; };
2373AE152D0A24930086C749 /* MetricsSeriesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE142D0A24930086C749 /* MetricsSeriesList.swift */; };
2373AE172D0A26620086C749 /* EnviornmentDefaultSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE162D0A26620086C749 /* EnviornmentDefaultSeries.swift */; };
2373AE172D0A26620086C749 /* EnvironmentDefaultSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.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 */; };
@ -56,7 +66,6 @@
BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; };
BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */; };
BCE2D3C92C7C377F008E6199 /* FactoryResetNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */; };
C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; };
D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; };
D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; };
D93068D72B8146690066FBC8 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D62B8146690066FBC8 /* MessageText.swift */; };
@ -64,7 +73,6 @@
D93068DB2B81C85E0066FBC8 /* PowerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */; };
D93068DD2B81CA820066FBC8 /* ConfigHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */; };
D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93069072B81DF040066FBC8 /* SaveConfigButton.swift */; };
D9BC22DB2B7DE8E2006A37D5 /* TileDownloadStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BC22DA2B7DE8E2006A37D5 /* TileDownloadStatus.swift */; };
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */; };
D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */; };
D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */; };
@ -90,7 +98,6 @@
DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553562855B02500E55709 /* LoRaConfig.swift */; };
DD2553592855B52700E55709 /* PositionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553582855B52700E55709 /* PositionConfig.swift */; };
DD268D8E2BCC90E2008073AE /* RouteEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD268D8D2BCC90E2008073AE /* RouteEnums.swift */; };
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; };
DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */; };
DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; };
DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD354FD82BD96A0B0061A25F /* IAQScale.swift */; };
@ -144,9 +151,7 @@
DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */; };
DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */; };
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; };
DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBE296E76EF007C176F /* WaypointFormMapKit.swift */; };
DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */; };
DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC32974767D007C176F /* MapViewFitExtension.swift */; };
DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC52975DBFD007C176F /* QueryCoreData.swift */; };
DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; };
DD97E96828EFE9A00056DDA4 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96728EFE9A00056DDA4 /* About.swift */; };
@ -203,8 +208,6 @@
DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26412AABF655003AFCB7 /* NodeListItem.swift */; };
DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */; };
DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */; };
DDDB26482AACD6D1003AFCB7 /* NodeMapMapkit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26472AACD6D1003AFCB7 /* NodeMapMapkit.swift */; };
DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443529F6287000EE2349 /* MapButtons.swift */; };
DDDB444029F79AB000EE2349 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443F29F79AB000EE2349 /* UserDefaults.swift */; };
DDDB444229F8A88700EE2349 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444129F8A88700EE2349 /* Double.swift */; };
DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444329F8A8DD00EE2349 /* Float.swift */; };
@ -277,12 +280,23 @@
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>"; };
231B3F262D0885240069A07D /* MetricsColumnDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsColumnDetail.swift; sourceTree = "<group>"; };
233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 50.xcdatamodel"; sourceTree = "<group>"; };
233E99B52D849C3D00CC3A77 /* WeatherConditionsCompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherConditionsCompactWidget.swift; sourceTree = "<group>"; };
233E99B72D849C6500CC3A77 /* HumidityCompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HumidityCompactWidget.swift; sourceTree = "<group>"; };
233E99B92D849C7000CC3A77 /* PressureCompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PressureCompactWidget.swift; sourceTree = "<group>"; };
233E99BB2D849C8C00CC3A77 /* WindCompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindCompactWidget.swift; sourceTree = "<group>"; };
233E99BD2D849D3200CC3A77 /* RadiationCompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadiationCompactWidget.swift; sourceTree = "<group>"; };
233E99C02D849D6000CC3A77 /* DistanceCompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistanceCompactWidget.swift; sourceTree = "<group>"; };
233E99C22D849D7A00CC3A77 /* WeightCompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeightCompactWidget.swift; sourceTree = "<group>"; };
233E99C42D84A0B600CC3A77 /* CompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactWidget.swift; sourceTree = "<group>"; };
233E99C62D84A70900CC3A77 /* SoilCompactWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoilCompactWidgets.swift; sourceTree = "<group>"; };
233E99CA2D85AAA900CC3A77 /* RainfallCompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RainfallCompactWidget.swift; sourceTree = "<group>"; };
2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedAttributePropertyWrapper.swift; sourceTree = "<group>"; };
2344A2AD2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TelemetryEntity+CoreDataClass.swift"; sourceTree = "<group>"; };
2344A2AE2D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TelemetryEntity+CoreDataProperties.swift"; sourceTree = "<group>"; };
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 /* EnviornmentDefaultSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnviornmentDefaultSeries.swift; sourceTree = "<group>"; };
2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultSeries.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>"; };
@ -324,7 +338,6 @@
D93068DC2B81CA820066FBC8 /* ConfigHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigHeader.swift; sourceTree = "<group>"; };
D93069062B81D8900066FBC8 /* MeshtasticDataModelV 27.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 27.xcdatamodel"; sourceTree = "<group>"; };
D93069072B81DF040066FBC8 /* SaveConfigButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveConfigButton.swift; sourceTree = "<group>"; };
D9BC22DA2B7DE8E2006A37D5 /* TileDownloadStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileDownloadStatus.swift; sourceTree = "<group>"; };
D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSize.swift; sourceTree = "<group>"; };
D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestPositionButton.swift; sourceTree = "<group>"; };
@ -359,7 +372,6 @@
DD268D8D2BCC90E2008073AE /* RouteEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteEnums.swift; sourceTree = "<group>"; };
DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = "<group>"; };
DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 41.xcdatamodel"; sourceTree = "<group>"; };
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = "<group>"; };
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = "<group>"; };
DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 36.xcdatamodel"; sourceTree = "<group>"; };
DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 23.xcdatamodel"; sourceTree = "<group>"; };
@ -431,10 +443,8 @@
DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelForm.swift; sourceTree = "<group>"; };
DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSettingsForm.swift; sourceTree = "<group>"; };
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = "<group>"; };
DD964FBE296E76EF007C176F /* WaypointFormMapKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointFormMapKit.swift; sourceTree = "<group>"; };
DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = "<group>"; };
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = "<group>"; };
DD964FC32974767D007C176F /* MapViewFitExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewFitExtension.swift; sourceTree = "<group>"; };
DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = "<group>"; };
DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV32.xcdatamodel; sourceTree = "<group>"; };
DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = "<group>"; };
@ -508,9 +518,7 @@
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListItem.swift; sourceTree = "<group>"; };
DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = "<group>"; };
DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoItem.swift; sourceTree = "<group>"; };
DDDB26472AACD6D1003AFCB7 /* NodeMapMapkit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapMapkit.swift; sourceTree = "<group>"; };
DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV18.xcdatamodel; sourceTree = "<group>"; };
DDDB443529F6287000EE2349 /* MapButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapButtons.swift; sourceTree = "<group>"; };
DDDB443F29F79AB000EE2349 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
DDDB444129F8A88700EE2349 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
DDDB444329F8A8DD00EE2349 /* Float.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Float.swift; sourceTree = "<group>"; };
@ -567,7 +575,6 @@
buildActionMask = 2147483647;
files = (
25A978BA2C13F8ED0003AAE7 /* MeshtasticProtobufs in Frameworks */,
C9697FA527933B8C00250207 /* SQLite in Frameworks */,
DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -600,12 +607,30 @@
isa = PBXGroup;
children = (
231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */,
2373AE162D0A26620086C749 /* EnviornmentDefaultSeries.swift */,
2373AE162D0A26620086C749 /* EnvironmentDefaultSeries.swift */,
231B3F262D0885240069A07D /* MetricsColumnDetail.swift */,
);
path = "Metrics Columns";
sourceTree = "<group>";
};
233E99B42D849C2D00CC3A77 /* Compact Widgets */ = {
isa = PBXGroup;
children = (
233E99C42D84A0B600CC3A77 /* CompactWidget.swift */,
233E99B72D849C6500CC3A77 /* HumidityCompactWidget.swift */,
233E99B52D849C3D00CC3A77 /* WeatherConditionsCompactWidget.swift */,
233E99B92D849C7000CC3A77 /* PressureCompactWidget.swift */,
233E99BB2D849C8C00CC3A77 /* WindCompactWidget.swift */,
DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */,
233E99BD2D849D3200CC3A77 /* RadiationCompactWidget.swift */,
233E99C02D849D6000CC3A77 /* DistanceCompactWidget.swift */,
233E99C22D849D7A00CC3A77 /* WeightCompactWidget.swift */,
233E99C62D84A70900CC3A77 /* SoilCompactWidgets.swift */,
233E99CA2D85AAA900CC3A77 /* RainfallCompactWidget.swift */,
);
path = "Compact Widgets";
sourceTree = "<group>";
};
2344A2AC2D66978000170A77 /* CoreData */ = {
isa = PBXGroup;
children = (
@ -665,27 +690,6 @@
path = AppIntents;
sourceTree = "<group>";
};
C9483F6B2773016700998F6B /* MapKitMap */ = {
isa = PBXGroup;
children = (
C9A7BC0E27759A6800760B50 /* Custom */,
DDDB26472AACD6D1003AFCB7 /* NodeMapMapkit.swift */,
DD964FBE296E76EF007C176F /* WaypointFormMapKit.swift */,
);
path = MapKitMap;
sourceTree = "<group>";
};
C9A7BC0E27759A6800760B50 /* Custom */ = {
isa = PBXGroup;
children = (
DD964FC32974767D007C176F /* MapViewFitExtension.swift */,
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */,
DDDB443529F6287000EE2349 /* MapButtons.swift */,
D9BC22DA2B7DE8E2006A37D5 /* TileDownloadStatus.swift */,
);
path = Custom;
sourceTree = "<group>";
};
D9C9839E2B79D0C600BDBE6A /* TextMessageField */ = {
isa = PBXGroup;
children = (
@ -777,7 +781,6 @@
isa = PBXGroup;
children = (
DD5E523E298F5A9E00D21B61 /* AirQualityIndex.swift */,
DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */,
DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */,
DD354FD82BD96A0B0061A25F /* IAQScale.swift */,
DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */,
@ -987,7 +990,6 @@
isa = PBXGroup;
children = (
DD6D5A312CA1176A00ED3032 /* Layouts */,
C9483F6B2773016700998F6B /* MapKitMap */,
DDC2E18D26CE25CB0042C5E4 /* Helpers */,
DD47E3D726F2F21A00029299 /* Bluetooth */,
DDC2E18B26CE25A70042C5E4 /* Messages */,
@ -1038,6 +1040,7 @@
DDC2E18D26CE25CB0042C5E4 /* Helpers */ = {
isa = PBXGroup;
children = (
233E99B42D849C2D00CC3A77 /* Compact Widgets */,
DD6F65772C6EAB860053C113 /* Help */,
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */,
DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */,
@ -1213,7 +1216,6 @@
);
name = Meshtastic;
packageProductDependencies = (
C9697FA427933B8C00250207 /* SQLite */,
DD0D3D212A55CEB10066DB71 /* CocoaMQTT */,
25A978B92C13F8ED0003AAE7 /* MeshtasticProtobufs */,
);
@ -1283,7 +1285,6 @@
);
mainGroup = DDC2E14B26CE248E0042C5E4;
packageReferences = (
C9697FA327933B8C00250207 /* XCRemoteSwiftPackageReference "SQLite.swift" */,
DD0D3D202A55CEB10066DB71 /* XCRemoteSwiftPackageReference "CocoaMQTT" */,
25A978B82C13F8ED0003AAE7 /* XCLocalSwiftPackageReference "MeshtasticProtobufs" */,
259792242C2F10B600AD1659 /* XCRemoteSwiftPackageReference "swift-protobuf" */,
@ -1380,11 +1381,9 @@
DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */,
D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */,
DD5E523F298F5A9E00D21B61 /* AirQualityIndex.swift in Sources */,
DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */,
DDD5BB182C2F9C36007E03CA /* OSLogEntryLog.swift in Sources */,
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */,
DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */,
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */,
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */,
DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */,
@ -1405,8 +1404,10 @@
DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */,
251926902C3CB44900249DF5 /* ClientHistoryButton.swift in Sources */,
DDD5BB102C285FB3007E03CA /* AppLogFilter.swift in Sources */,
2373AE172D0A26620086C749 /* EnviornmentDefaultSeries.swift in Sources */,
2373AE172D0A26620086C749 /* EnvironmentDefaultSeries.swift in Sources */,
233E99B82D849C6500CC3A77 /* HumidityCompactWidget.swift in Sources */,
DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */,
233E99C12D849D6000CC3A77 /* DistanceCompactWidget.swift in Sources */,
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */,
DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */,
DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */,
@ -1427,7 +1428,6 @@
DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */,
DDDB445429F8AD1600EE2349 /* Data.swift in Sources */,
DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */,
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */,
DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */,
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,
DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */,
@ -1436,7 +1436,7 @@
DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */,
B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */,
DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */,
DDDB26482AACD6D1003AFCB7 /* NodeMapMapkit.swift in Sources */,
233E99C32D849D7A00CC3A77 /* WeightCompactWidget.swift in Sources */,
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */,
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
@ -1453,6 +1453,7 @@
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */,
DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */,
DD6F65792C6EADE60053C113 /* DirectMessagesHelp.swift in Sources */,
233E99B62D849C3D00CC3A77 /* WeatherConditionsCompactWidget.swift in Sources */,
25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */,
DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */,
25C49D902C471AEA0024FBD1 /* Constants.swift in Sources */,
@ -1479,7 +1480,6 @@
D93068DB2B81C85E0066FBC8 /* PowerConfig.swift in Sources */,
D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */,
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */,
DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */,
BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */,
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */,
DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */,
@ -1488,18 +1488,20 @@
DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */,
DD0BE3102CB9FDC4000BA445 /* DetectionSensorEnums.swift in Sources */,
DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */,
233E99CB2D85AAA900CC3A77 /* RainfallCompactWidget.swift in Sources */,
DD2553592855B52700E55709 /* PositionConfig.swift in Sources */,
DD97E96828EFE9A00056DDA4 /* About.swift in Sources */,
DDDB444029F79AB000EE2349 /* UserDefaults.swift in Sources */,
233E99BA2D849C7000CC3A77 /* PressureCompactWidget.swift in Sources */,
DDB6ABE028B13AC700384BA1 /* DeviceEnums.swift in Sources */,
DD86D40C287F401000BAEB7A /* SaveChannelQRCode.swift in Sources */,
D93068DD2B81CA820066FBC8 /* ConfigHeader.swift in Sources */,
251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */,
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */,
D9BC22DB2B7DE8E2006A37D5 /* TileDownloadStatus.swift in Sources */,
DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */,
BC5EBA3C2D002A2000C442FF /* MessageNodeIntent.swift in Sources */,
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */,
233E99C52D84A0B600CC3A77 /* CompactWidget.swift in Sources */,
DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */,
DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */,
DDB75A112A059258006ED576 /* Url.swift in Sources */,
@ -1535,6 +1537,7 @@
8D3F8A3F2D44BB02009EAAA4 /* PowerMetrics.swift in Sources */,
2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */,
DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */,
233E99BE2D849D3200CC3A77 /* RadiationCompactWidget.swift in Sources */,
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */,
DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */,
DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */,
@ -1566,12 +1569,14 @@
DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */,
DD77093F2AA1B146007A8BF0 /* UIColor.swift in Sources */,
DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */,
233E99C72D84A70900CC3A77 /* SoilCompactWidgets.swift in Sources */,
BCE2D3C92C7C377F008E6199 /* FactoryResetNodeIntent.swift in Sources */,
DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */,
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */,
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */,
DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */,
DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */,
233E99BC2D849C8C00CC3A77 /* WindCompactWidget.swift in Sources */,
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */,
BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */,
D93068D72B8146690066FBC8 /* MessageText.swift in Sources */,
@ -1800,7 +1805,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.5.20;
MARKETING_VERSION = 2.5.22;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1834,7 +1839,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.5.20;
MARKETING_VERSION = 2.5.22;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1866,7 +1871,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.5.20;
MARKETING_VERSION = 2.5.22;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1899,7 +1904,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.5.20;
MARKETING_VERSION = 2.5.22;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1968,14 +1973,6 @@
minimumVersion = 1.26.0;
};
};
C9697FA327933B8C00250207 /* XCRemoteSwiftPackageReference "SQLite.swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/stephencelis/SQLite.swift.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.2;
};
};
DD0D3D202A55CEB10066DB71 /* XCRemoteSwiftPackageReference "CocoaMQTT" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/emqx/CocoaMQTT";
@ -1995,11 +1992,6 @@
isa = XCSwiftPackageProductDependency;
productName = MeshtasticProtobufs;
};
C9697FA427933B8C00250207 /* SQLite */ = {
isa = XCSwiftPackageProductDependency;
package = C9697FA327933B8C00250207 /* XCRemoteSwiftPackageReference "SQLite.swift" */;
productName = SQLite;
};
DD0D3D212A55CEB10066DB71 /* CocoaMQTT */ = {
isa = XCSwiftPackageProductDependency;
package = DD0D3D202A55CEB10066DB71 /* XCRemoteSwiftPackageReference "CocoaMQTT" */;
@ -2011,6 +2003,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */,
8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */,
DDA28B1B2D32C89200EF726F /* MeshtasticDataModelV 48.xcdatamodel */,
DDDFE7402D0D4A070044463C /* MeshtasticDataModelV 47.xcdatamodel */,
@ -2061,7 +2054,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = 8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */;
currentVersion = 233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -1,51 +0,0 @@
{
"originHash" : "2d0b85469585b0d6079eac292d63864096062c24848a49380b9d9727f0ceb96c",
"pins" : [
{
"identity" : "cocoamqtt",
"kind" : "remoteSourceControl",
"location" : "https://github.com/emqx/CocoaMQTT",
"state" : {
"revision" : "85387a2478551ad84f39be8a3c8587d34dd2bcf5",
"version" : "2.1.5"
}
},
{
"identity" : "mqttcocoaasyncsocket",
"kind" : "remoteSourceControl",
"location" : "https://github.com/leeway1208/MqttCocoaAsyncSocket",
"state" : {
"revision" : "ce3e18607fd01079495f86ff6195d8a3ca469f73",
"version" : "1.0.8"
}
},
{
"identity" : "sqlite.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/stephencelis/SQLite.swift.git",
"state" : {
"revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb",
"version" : "0.14.1"
}
},
{
"identity" : "starscream",
"kind" : "remoteSourceControl",
"location" : "https://github.com/daltoniam/Starscream.git",
"state" : {
"revision" : "a063fda2b8145a231953c20e7a646be254365396",
"version" : "3.1.2"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "9f0c76544701845ad98716f3f6a774a892152bcb",
"version" : "1.26.0"
}
}
],
"version" : 3
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View file

@ -1,5 +1,5 @@
{
"originHash" : "1571e0d09fede5d57a2c415019f30868d90fde5a53a863cc277593881c2dc4a5",
"originHash" : "a3033aea781828906c453276e3723177901ce64df5757de7ada28c854c9662eb",
"pins" : [
{
"identity" : "cocoamqtt",
@ -19,15 +19,6 @@
"version" : "1.0.8"
}
},
{
"identity" : "sqlite.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/stephencelis/SQLite.swift.git",
"state" : {
"revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8",
"version" : "0.15.3"
}
},
{
"identity" : "starscream",
"kind" : "remoteSourceControl",
@ -42,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "edb6ed4919f7756157fe02f2552b7e3850a538e5",
"version" : "1.28.1"
"revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
"version" : "1.29.0"
}
}
],

View file

@ -16,7 +16,7 @@ class AppIntentErrors {
var localizedStringResource: LocalizedStringResource {
switch self {
case let .message(message):
Logger.services.error("App Intent: \(message)")
Logger.services.error("App Intent: \(message,privacy: .public)")
return "Error: \(message)"
case .notConnected:
Logger.services.error("App Intent: No Connected Node")

View file

@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "soilMoisture.variable.svg",
"idiom" : "universal"
}
]
}

View file

@ -0,0 +1,366 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--Generator: Apple Native CoreSVG 341-->
<svg
version="1.1"
viewBox="0 0 3300 2200"
id="svg681"
sodipodi:docname="groundmoisture.variable.svg"
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
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">
<defs
id="defs685" />
<sodipodi:namedview
id="namedview683"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.55606061"
inkscape:cx="1991.6894"
inkscape:cy="935.14986"
inkscape:window-width="2560"
inkscape:window-height="1440"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="Symbols"
showguides="false" />
<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"
id="line562" />
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;"
transform="matrix(1 0 0 1 263 322)"
id="text564">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)"
id="text566">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)"
id="text568">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)"
id="text570">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)"
id="text572">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)"
id="text574">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)"
id="text576">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)"
id="text578">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)"
id="text580">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)"
id="text582">Black</text>
<line
style="fill:none;stroke:black;opacity:1;stroke-width:0.5;"
x1="263"
x2="3036"
y1="1903"
y2="1903"
id="line584" />
<g
transform="matrix(0.2 0 0 0.2 263 1933)"
id="g588">
<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"
id="path586" />
</g>
<g
transform="matrix(0.2 0 0 0.2 281.506 1933)"
id="g592">
<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"
id="path590" />
</g>
<g
transform="matrix(0.2 0 0 0.2 304.924 1933)"
id="g596">
<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"
id="path594" />
</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)"
id="text598">Design Variations</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 1971)"
id="text600">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)"
id="text602">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)"
id="text604">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"
id="line606" />
<g
transform="matrix(0.2 0 0 0.2 776 1933)"
id="g610">
<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"
id="path608" />
</g>
<line
style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
x1="792.836"
x2="792.836"
y1="1919"
y2="1933"
id="line612" />
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;"
transform="matrix(1 0 0 1 776 1953)"
id="text614">Margins</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 776 1971)"
id="text616">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)"
id="text618">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)"
id="text620">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)"
id="text622">scales and weights.</text>
<g
transform="matrix(0.2 0 0 0.2 1289 1933)"
id="g626">
<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"
id="path624" />
</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)"
id="text628">Exporting</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 1289 1971)"
id="text630">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)"
id="text632">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)"
id="text635">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 thermometer.variable</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)"
id="text638">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)"
id="text640">Small</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 1156)"
id="text642">Medium</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 1586)"
id="text644">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"
id="path647" />
</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="g654"
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"
id="path652" />
</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="g660"
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"
id="path658" />
</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="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="2994.5601"
x2="2994.5601"
y1="600.78497"
y2="720.12097" />
<line
id="left-margin-Black-S"
style="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="2882.24"
x2="2882.24"
y1="600.78497"
y2="720.12097" />
<line
id="right-margin-Regular-S"
style="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="1492"
x2="1492"
y1="600.78497"
y2="720.12097" />
<line
id="left-margin-Regular-S"
style="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="1395.6899"
x2="1395.6899"
y1="600.78497"
y2="720.12097" />
<line
id="right-margin-Ultralight-S"
style="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="605.70599"
x2="605.70599"
y1="600.78497"
y2="720.12097" />
<line
id="left-margin-Ultralight-S"
style="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="513.71698"
x2="513.71698"
y1="600.78497"
y2="720.12097" />
</g>
<g
id="Symbols">
<g
id="Black-S"
transform="matrix(1 0 0 1 2898.24 696)"
style="fill:#000000">
<path
id="path1811"
style="fill:#000000;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
d="m 40.160224,10.512322 c 13.8183,0 24.9508,-10.88863047 24.9508,-24.560501 0,-7.1289 -3.32,-13.6719 -5.5172,-17.7734 l -8.545,-15.869199 c -2.1972,-4.1504 -6.2011,-6.5918 -10.8886,-6.5918 -4.6387,0 -8.6426,2.4414 -10.8399,6.543 l -8.4961,15.869099 c -2.2461,4.1504 -5.6152,10.6446 -5.6152,17.8223 0,13.67187053 11.1816,24.560501 24.9512,24.560501 z m -18.6035,-24.560501 c 0,-5.5176 2.6855,-10.8399 4.8339,-14.7949 l 8.4961,-15.869199 c 1.1231,-2.0508 2.9785,-3.2226 5.2735,-3.2226 2.2949,0 4.1504,1.123 5.2734,3.2226 l 8.5449,15.869199 c 2.0996,3.955 4.7852,9.2773 4.7852,14.7949 0,10.1074205 -8.252,18.2128805 -18.6035,18.2128805 -10.3028,0 -18.6035,-8.10546 -18.6035,-18.2128805 z m 18.6035,6.5917905 c -4.9805,0 -8.9356,-3.8085895 -8.9356,-8.5448905 0,-3.2227 1.6602,-6.2989 3.7598,-10.2539 l 4.7851,-8.935599 c 0.1954,-0.3906 0.6348,-0.3906 0.8301,0 l 4.7364,8.935599 c 2.0996,3.955 3.7597,7.0312 3.7597,10.2539 0,4.736301 -3.9062,8.5448905 -8.9355,8.5448905 z M 93.819567,-0.549 c 0,2.346 -1.91,4.25 -4.25,4.25 -2.35,0 -4.25,-1.904 -4.25,-4.25 0,-0.295 -0.01,-0.589 -0.03,-0.881 -0.15,-2.341 1.62,-4.366 3.96,-4.519 2.34,-0.154 4.37,1.621 4.52,3.962 0.03,0.477 0.05,0.956 0.05,1.438 z m -5.03,-14.361 c 1.4,1.882 1.01,4.547 -0.87,5.947 -1.88,1.401 -4.55,1.011 -5.95,-0.871 -0.38,-0.515 -0.79,-1.02 -1.23,-1.516 -1.56,-1.755 -1.4,-4.443 0.36,-6 1.75,-1.556 4.44,-1.395 6,0.36 0.6,0.68 1.16,1.374 1.69,2.08 z m -11.75,-10.145 c 2.02,1.186 2.7,3.792 1.52,5.815 -1.19,2.024 -3.79,2.704 -5.82,1.518 -0.6,-0.352 -1.22,-0.695 -1.85,-1.027 -2.08,-1.088 -2.88,-3.659 -1.79,-5.737 1.09,-2.077 3.66,-2.881 5.73,-1.792 0.76,0.395 1.5,0.803 2.21,1.223 z M 5.8395672,-26.278 c 2.08,-1.089 4.6499998,-0.285 5.7399998,1.792 1.08,2.078 0.28,4.649 -1.7999998,5.737 -0.63,0.332 -1.25,0.675 -1.85,1.027 -2.02,1.186 -4.63,0.506 -5.82,-1.518 -1.17999998,-2.023 -0.5,-4.629 1.52,-5.815 0.72,-0.42 1.45,-0.828 2.21,-1.223 z m -12.26,9.288 c 1.55,-1.755 4.24,-1.916 6.00000002,-0.36 1.74999998,1.557 1.90999998,4.245 0.36,6 -0.44,0.496 -0.86,1.001 -1.24000002,1.516 -1.4,1.882 -4.06,2.272 -5.95,0.871 -1.88,-1.4 -2.27,-4.065 -0.87,-5.946 0.53,-0.707 1.09,-1.401 1.7,-2.081 z m -6.6800002,15.003 c 0.15,-2.341 2.18,-4.116 4.5200002,-3.962 2.34,0.153 4.12,2.178 3.96,4.519 -0.02,0.292 -0.03,0.586 -0.03,0.881 0,2.346 -1.9,4.25 -4.25,4.25 -2.3400002,0 -4.2500002,-1.904 -4.2500002,-4.25 0,-0.482 0.02,-0.961 0.05,-1.438 z" />
</g>
<g
id="Regular-S"
transform="matrix(1 0 0 1 1419.69 696)"
style="fill:#000000">
<path
id="path1814"
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:0.5;stroke-dasharray:none"
d="m 40.829821,-13.588987 c 0,-4.9805 -2.4903,-9.8633 -4.5899,-13.7695 l -8.3984,-15.5762 c -0.9766,-1.8555 -1.8555,-2.6855 -3.711,-2.6855 -1.8554,0 -2.7343,0.83 -3.7109,2.6855 l -8.3496,15.5762 c -2.0508,3.9062 -4.5410003,8.789 -4.5410003,13.7695 0,8.9843901 7.4218003,16.2597781 16.6015003,16.2597781 9.2774,0 16.6993,-7.275388 16.6993,-16.2597781 z m -16.6993,21.9726701 c -12.3047,0 -22.3144003,-9.76563 -22.3144003,-21.9726701 0,-6.4453 3.125,-12.4023 5.2246,-16.4062 l 8.3496003,-15.6739 c 1.9043,-3.5156 4.6387,-5.664 8.7402,-5.664 4.1504,0 6.8848,2.1484 8.7891,5.664 l 8.3984,15.625 c 2.0996,4.0528 5.1758,10.0098 5.1758,16.4551 0,12.2070401 -10.0098,21.9726701 -22.3633,21.9726701 z m 46.31122,-10.4926827 c 0,1.518 -1.23,2.75 -2.75,2.75 -1.51,0 -2.75,-1.232 -2.75,-2.75 0,-0.328 -0.01,-0.654 -0.03,-0.979 -0.1,-1.515 1.05,-2.825 2.57,-2.924 1.51,-0.1 2.82,1.049 2.92,2.564 0.03,0.444 0.04,0.891 0.04,1.339 z m -4.73,-13.4640004 c 0.91,1.217 0.66,2.941 -0.56,3.848 -1.22,0.906 -2.94,0.653 -3.85,-0.564 -0.41,-0.549 -0.85,-1.088 -1.32,-1.616 -1,-1.136 -0.9,-2.875 0.24,-3.882 1.13,-1.007 2.87,-0.903 3.88,0.232 0.57,0.648 1.11,1.309 1.61,1.982 z m -11.3,-9.748 c 1.31,0.768 1.75,2.454 0.98,3.763 -0.77,1.31 -2.45,1.75 -3.76,0.982 -0.63,-0.364 -1.26,-0.718 -1.92,-1.061 -1.34,-0.704 -1.86,-2.368 -1.16,-3.712 0.7,-1.345 2.37,-1.864 3.71,-1.16 0.74,0.384 1.45,0.78 2.15,1.188 z m -58.7500004,-1.188 c 1.34,-0.704 3.01,-0.185 3.71,1.16 0.71,1.344 0.19,3.008 -1.16,3.712 -0.65,0.343 -1.29,0.697 -1.91,1.061 -1.31,0.768 -3,0.328 -3.77,-0.982 -0.76,-1.309 -0.32,-2.995 0.98,-3.763 0.7,-0.408 1.42,-0.804 2.15,-1.188 z m -11.8399996,8.954 c 1.01,-1.135 2.75,-1.239 3.89,-0.232 1.13,1.007 1.24,2.746 0.23,3.882 -0.47,0.528 -0.91,1.067 -1.32,1.616 -0.9,1.217 -2.63,1.47 -3.85,0.564 -1.21,-0.907 -1.47,-2.631 -0.56,-3.848 0.5,-0.673 1.04,-1.334 1.61,-1.982 z m -6.3,14.1070004 c 0.1,-1.515 1.41,-2.664 2.93,-2.564 1.51,0.099 2.66,1.409 2.56,2.924 -0.02,0.325 -0.03,0.651 -0.03,0.979 0,1.518 -1.23,2.75 -2.75,2.75 -1.52,0 -2.75,-1.232 -2.75,-2.75 0,-0.448 0.01,-0.895 0.04,-1.339 z m 46.60878,0.4059027 c -6.0547,0 -10.8886,-4.63867 -10.8886,-10.5468901 0,-3.6621 1.8554,-7.1777 3.9062,-11.084 l 6.6895,-12.4023 c 0.1953,-0.3418 0.4882,-0.3418 0.6836,0 l 6.5918,12.4023 c 2.0507,3.9063 4.0039,7.4219 4.0039,11.084 0,5.9082201 -4.8829,10.5468901 -10.9864,10.5468901 z" />
</g>
<g
id="Ultralight-S"
transform="matrix(1 0 0 1 531.717 696)"
style="fill:#000000">
<path
id="path1817"
style="fill:#000000;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
d="m 8.8343368,-13.746838 c 0,-5.673299 2.7617002,-11.221699 5.0883002,-15.497999 l 9.1216,-17.0816 c 1.0869,-1.9716 2.8223,-3.0303 4.9712,-3.0303 2.1524,0 3.8878,1.0587 4.9747,3.1212 l 9.0796,16.9873 c 2.372,4.2797 5.0849,9.8281 5.0849,15.501399 0,10.4814908 -8.5112,18.7940007 -19.1392,18.7940007 -10.6245,0 -19.1811002,-8.3125099 -19.1811002,-18.7940007 z M 28.015437,1.9680428 c 8.9595,0 16.1089,-6.9575205 16.1089,-15.7148808 0,-4.934999 -2.4448,-9.863299 -4.726,-14.087399 l -9.125,-16.9838 c -0.5679,-1.0381 -1.2652,-1.4595 -2.2579,-1.4595 -1.0381,0 -1.7353,0.4214 -2.2578,1.414 l -9.1216,17.0293 c -2.2778,4.2241 -4.7226,9.1524 -4.7226,14.087399 0,8.7573603 7.1494,15.7148808 16.102,15.7148808 z m 0,-2.12551995 c -7.7802,0 -13.9765,-6.00096055 -13.9765,-13.58936085 0,-4.433999 2.2642,-8.903299 4.4966,-13.081999 l 8.9599,-16.7163 c 0.2407,-0.478 0.8062,-0.478 1.0469,0 l 8.9531,16.7163 c 2.2324,4.1787 4.458,8.648 4.458,13.081999 0,7.5884003 -6.1543,13.58936085 -13.938,13.58936085 z M 70.343736,-2.1090003 c 0,-0.4319999 -0.014,-0.8619999 -0.042,-1.2899999 -0.073,-1.102 -1.026,-1.937 -2.127,-1.865 -1.101,0.072 -1.937,1.025 -1.865,2.127 0.023,0.341 0.034,0.684 0.034,1.0279999 0,1.104 0.896,2.0000001 2,2.0000001 1.104,0 2,-0.8960001 2,-2.0000001 z M 65.759736,-15.126 c -0.488,-0.655 -1.013,-1.3 -1.573,-1.931 -0.732,-0.826 -1.997,-0.902 -2.823,-0.169 -0.826,0.732 -0.902,1.997 -0.169,2.823 0.483,0.545 0.936,1.1 1.357,1.666 0.659,0.885 1.913,1.069 2.798,0.41 0.886,-0.659 1.07,-1.913 0.41,-2.799 z m -11.082,-9.548 c -0.686,-0.402 -1.39,-0.792 -2.113,-1.171 -0.978,-0.512 -2.187,-0.134 -2.7,0.844 -0.512,0.978 -0.134,2.187 0.844,2.7 0.666,0.348 1.315,0.708 1.946,1.078 0.953,0.558 2.179,0.238 2.737,-0.714 0.558,-0.952 0.238,-2.179 -0.714,-2.737 z M 1.6577364,-25.845 c -0.72300001,0.379 -1.42800001,0.769 -2.11300001,1.171 -0.95299999,0.558 -1.27299999,1.785 -0.71399999,2.737 0.55799999,0.952 1.78399999,1.272 2.736,0.714 0.632,-0.37 1.281,-0.73 1.947,-1.078 0.978,-0.513 1.356,-1.722 0.843,-2.7 -0.512,-0.978 -1.722,-1.356 -2.699,-0.844 z m -11.622,8.788 c -0.5600004,0.631 -1.0850004,1.276 -1.5740004,1.931 -0.659,0.886 -0.475,2.14 0.41,2.799 0.886,0.659 2.1400004,0.476 2.7990004,-0.41 0.421,-0.566 0.874,-1.121 1.357,-1.666 0.733,-0.826 0.657,-2.091 -0.169,-2.823 -0.826,-0.733 -2.091,-0.657 -2.823,0.169 z m -6.1150004,13.6579998 c -0.028,0.428 -0.042,0.858 -0.042,1.2899999 0,1.104 0.896,2.0000001 2,2.0000001 1.104,0 2,-0.8960001 2,-2.0000001 0,-0.3439999 0.011,-0.6869999 0.033,-1.0279999 0.073,-1.102 -0.763,-2.055 -1.864,-2.127 -1.102,-0.072 -2.055,0.763 -2.127,1.865 z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "soilTemp.variable.svg",
"idiom" : "universal"
}
]
}

View file

@ -0,0 +1,363 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--Generator: Apple Native CoreSVG 341-->
<svg
version="1.1"
viewBox="0 0 3300 2200"
id="svg681"
sodipodi:docname="groundtemp.variable.svg"
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
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">
<defs
id="defs685" />
<sodipodi:namedview
id="namedview683"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.3396954"
inkscape:cx="2915.5504"
inkscape:cy="705.00629"
inkscape:window-width="1390"
inkscape:window-height="1205"
inkscape:window-x="65"
inkscape:window-y="150"
inkscape:window-maximized="0"
inkscape:current-layer="Guides"
showguides="false" />
<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"
id="line562" />
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;"
transform="matrix(1 0 0 1 263 322)"
id="text564">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)"
id="text566">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)"
id="text568">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)"
id="text570">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)"
id="text572">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)"
id="text574">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)"
id="text576">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)"
id="text578">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)"
id="text580">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)"
id="text582">Black</text>
<line
style="fill:none;stroke:black;opacity:1;stroke-width:0.5;"
x1="263"
x2="3036"
y1="1903"
y2="1903"
id="line584" />
<g
transform="matrix(0.2 0 0 0.2 263 1933)"
id="g588">
<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"
id="path586" />
</g>
<g
transform="matrix(0.2 0 0 0.2 281.506 1933)"
id="g592">
<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"
id="path590" />
</g>
<g
transform="matrix(0.2 0 0 0.2 304.924 1933)"
id="g596">
<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"
id="path594" />
</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)"
id="text598">Design Variations</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 1971)"
id="text600">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)"
id="text602">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)"
id="text604">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"
id="line606" />
<g
transform="matrix(0.2 0 0 0.2 776 1933)"
id="g610">
<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"
id="path608" />
</g>
<line
style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
x1="792.836"
x2="792.836"
y1="1919"
y2="1933"
id="line612" />
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;"
transform="matrix(1 0 0 1 776 1953)"
id="text614">Margins</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 776 1971)"
id="text616">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)"
id="text618">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)"
id="text620">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)"
id="text622">scales and weights.</text>
<g
transform="matrix(0.2 0 0 0.2 1289 1933)"
id="g626">
<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"
id="path624" />
</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)"
id="text628">Exporting</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 1289 1971)"
id="text630">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)"
id="text632">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)"
id="text635">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 thermometer.variable</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)"
id="text638">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)"
id="text640">Small</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 1156)"
id="text642">Medium</text>
<text
style="stroke:none;fill:black;font-family:sans-serif;font-size:13;"
transform="matrix(1 0 0 1 263 1586)"
id="text644">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"
id="path647" />
</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="g654"
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"
id="path652" />
</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="g660"
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"
id="path658" />
</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="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="2994.5601"
x2="2994.5601"
y1="600.78497"
y2="720.12097" />
<line
id="left-margin-Black-S"
style="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="2882.24"
x2="2882.24"
y1="600.78497"
y2="720.12097" />
<line
id="right-margin-Regular-S"
style="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="1492"
x2="1492"
y1="600.78497"
y2="720.12097" />
<line
id="left-margin-Regular-S"
style="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="1395.6899"
x2="1395.6899"
y1="600.78497"
y2="720.12097" />
<line
id="right-margin-Ultralight-S"
style="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="605.70599"
x2="605.70599"
y1="600.78497"
y2="720.12097" />
<line
id="left-margin-Ultralight-S"
style="opacity:1;fill:none;stroke:#00aeef;stroke-width:0.5"
x1="513.71698"
x2="513.71698"
y1="600.78497"
y2="720.12097" />
</g>
<g
id="Symbols">
<g
id="Black-S"
transform="matrix(1 0 0 1 2898.24 696)">
<path
d="m 93.819567,-0.549 c 0,2.346 -1.91,4.25 -4.25,4.25 -2.35,0 -4.25,-1.904 -4.25,-4.25 0,-0.295 -0.01,-0.589 -0.03,-0.881 -0.15,-2.341 1.62,-4.366 3.96,-4.519 2.34,-0.154 4.37,1.621 4.52,3.962 0.03,0.477 0.05,0.956 0.05,1.438 z m -5.03,-14.361 c 1.4,1.882 1.01,4.547 -0.87,5.947 -1.88,1.401 -4.55,1.011 -5.95,-0.871 -0.38,-0.515 -0.79,-1.02 -1.23,-1.516 -1.56,-1.755 -1.4,-4.443 0.36,-6 1.75,-1.556 4.44,-1.395 6,0.36 0.6,0.68 1.16,1.374 1.69,2.08 z m -11.75,-10.145 c 2.02,1.186 2.7,3.792 1.52,5.815 -1.19,2.024 -3.79,2.704 -5.82,1.518 -0.6,-0.352 -1.22,-0.695 -1.85,-1.027 -2.08,-1.088 -2.88,-3.659 -1.79,-5.737 1.09,-2.077 3.66,-2.881 5.73,-1.792 0.76,0.395 1.5,0.803 2.21,1.223 z M 5.8395672,-26.278 c 2.08,-1.089 4.6499998,-0.285 5.7399998,1.792 1.08,2.078 0.28,4.649 -1.7999998,5.737 -0.63,0.332 -1.25,0.675 -1.85,1.027 -2.02,1.186 -4.63,0.506 -5.82,-1.518 -1.17999998,-2.023 -0.5,-4.629 1.52,-5.815 0.72,-0.42 1.45,-0.828 2.21,-1.223 z m -12.26,9.288 c 1.55,-1.755 4.24,-1.916 6.00000002,-0.36 1.74999998,1.557 1.90999998,4.245 0.36,6 -0.44,0.496 -0.86,1.001 -1.24000002,1.516 -1.4,1.882 -4.06,2.272 -5.95,0.871 -1.88,-1.4 -2.27,-4.065 -0.87,-5.946 0.53,-0.707 1.09,-1.401 1.7,-2.081 z m -6.6800002,15.003 c 0.15,-2.341 2.18,-4.116 4.5200002,-3.962 2.34,0.153 4.12,2.178 3.96,4.519 -0.02,0.292 -0.03,0.586 -0.03,0.881 0,2.346 -1.9,4.25 -4.25,4.25 -2.3400002,0 -4.2500002,-1.904 -4.2500002,-4.25 0,-0.482 0.02,-0.961 0.05,-1.438 z m 53.49,13.315 c 13.96,0 25.34,-10.937 25.34,-24.951 0,-6.592 -2.44,-12.256 -6.99,-16.797 -0.73,-0.732 -0.83,-1.074 -0.83,-2.051 v -29.638 c 0,-11.182 -7.12,-18.702 -17.52,-18.702 -10.55,0 -17.63,7.52 -17.63,18.702 v 29.638 c 0,0.977 -0.1,1.367 -0.83,2.051 -4.69,4.346 -6.98,10.205 -6.98,16.797 0,14.014 11.37,24.951 25.44,24.951 z m 0,-11.474 c -7.72,0 -13.92,-6.25 -13.92,-13.965 0,-5.176 2.34,-8.985 6.3,-11.573 1.12,-0.732 1.56,-1.318 1.56,-2.832 v -33.3 c 0,-4.493 2.44,-7.471 6.06,-7.471 3.51,0 5.95,2.978 5.95,7.471 v 33.3 c 0,1.514 0.44,2.1 1.57,2.832 3.95,2.588 6.29,6.397 6.29,11.573 0,7.715 -6.2,13.965 -13.81,13.965 z m -0.05,-5.176 c 4.83,0 8.74,-3.907 8.74,-8.789 0,-3.369 -1.91,-6.153 -4.69,-7.666 -1.17,-0.635 -1.56,-1.075 -1.56,-2.832 v -10.254 c 0,-1.367 -1.13,-2.49 -2.49,-2.49 -1.37,0 -2.49,1.123 -2.49,2.49 v 10.254 c 0,1.757 -0.4,2.197 -1.57,2.832 -2.78,1.513 -4.68,4.297 -4.68,7.666 0,4.882 3.9,8.789 8.74,8.789 z m 0,-36.035 c 1.36,0 2.49,-1.123 2.49,-2.491 0,-1.367 -1.13,-2.49 -2.49,-2.49 -1.37,0 -2.49,1.123 -2.49,2.49 0,1.368 1.12,2.491 2.49,2.491 z m 0,-8.985 c 1.36,0 2.49,-1.123 2.49,-2.49 0,-1.367 -1.13,-2.49 -2.49,-2.49 -1.37,0 -2.49,1.123 -2.49,2.49 0,1.367 1.12,2.49 2.49,2.49 z m 0,-8.984 c 1.36,0 2.49,-1.123 2.49,-2.49 0,-1.368 -1.13,-2.491 -2.49,-2.491 -1.37,0 -2.49,1.123 -2.49,2.491 0,1.367 1.12,2.49 2.49,2.49 z"
style="clip-rule:evenodd;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5px;stroke-linejoin:round;stroke-miterlimit:2"
id="path4426" />
</g>
<g
id="Regular-S"
transform="matrix(1 0 0 1 1419.69 696)">
<path
d="m 70.441741,-2.1089996 c 0,1.518 -1.23,2.75 -2.75,2.75 -1.51,0 -2.75,-1.232 -2.75,-2.75 0,-0.328 -0.01,-0.654 -0.03,-0.979 -0.1,-1.515 1.05,-2.825 2.57,-2.924 1.51,-0.1 2.82,1.049 2.92,2.564 0.03,0.444 0.04,0.891 0.04,1.339 z m -4.73,-13.4640004 c 0.91,1.217 0.66,2.941 -0.56,3.848 -1.22,0.906 -2.94,0.653 -3.85,-0.564 -0.41,-0.549 -0.85,-1.088 -1.32,-1.616 -1,-1.136 -0.9,-2.875 0.24,-3.882 1.13,-1.007 2.87,-0.903 3.88,0.232 0.57,0.648 1.11,1.309 1.61,1.982 z m -11.3,-9.748 c 1.31,0.768 1.75,2.454 0.98,3.763 -0.77,1.31 -2.45,1.75 -3.76,0.982 -0.63,-0.364 -1.26,-0.718 -1.92,-1.061 -1.34,-0.704 -1.86,-2.368 -1.16,-3.712 0.7,-1.345 2.37,-1.864 3.71,-1.16 0.74,0.384 1.45,0.78 2.15,1.188 z m -58.7500004,-1.188 c 1.34,-0.704 3.01,-0.185 3.71,1.16 0.71,1.344 0.19,3.008 -1.16,3.712 -0.65,0.343 -1.29,0.697 -1.91,1.061 -1.31,0.768 -3,0.328 -3.77,-0.982 -0.76,-1.309 -0.32,-2.995 0.98,-3.763 0.7,-0.408 1.42,-0.804 2.15,-1.188 z m -11.8399996,8.954 c 1.01,-1.135 2.75,-1.239 3.89,-0.232 1.13,1.007 1.24,2.746 0.23,3.882 -0.47,0.528 -0.91,1.067 -1.32,1.616 -0.9,1.217 -2.63,1.47 -3.85,0.564 -1.21,-0.907 -1.47,-2.631 -0.56,-3.848 0.5,-0.673 1.04,-1.334 1.61,-1.982 z m -6.3,14.1070004 c 0.1,-1.515 1.41,-2.664 2.93,-2.564 1.51,0.099 2.66,1.409 2.56,2.924 -0.02,0.325 -0.03,0.651 -0.03,0.979 0,1.518 -1.23,2.75 -2.75,2.75 -1.52,0 -2.75,-1.232 -2.75,-2.75 0,-0.448 0.01,-0.895 0.04,-1.339 z m 46.47,10.772 c 11.23,0 20.36,-9.131 20.36,-20.3610004 0,-5.908 -2.44,-11.084 -6.98,-15.283 -0.79,-0.733 -0.93,-1.123 -0.93,-2.149 v -33.789 c 0,-8.154 -5.03,-13.623 -12.45,-13.623 -7.48,0 -12.55,5.469 -12.55,13.623 v 33.789 c 0,1.026 -0.15,1.416 -0.88,2.149 -4.5400004,4.199 -6.9800004,9.375 -6.9800004,15.283 0,11.2300004 9.1300004,20.3610004 20.4100004,20.3610004 z m 0,-6.347 c -7.77,0 -14.0200004,-6.299 -14.0200004,-14.0140004 0,-4.736 2.3000004,-9.033 6.3000004,-11.67 1.12,-0.732 1.56,-1.367 1.56,-2.881 v -36.377 c 0,-4.541 2.49,-7.519 6.16,-7.519 3.61,0 6.05,2.978 6.05,7.519 v 36.377 c 0,1.514 0.49,2.149 1.61,2.881 3.96,2.637 6.3,6.934 6.3,11.67 0,7.7150004 -6.25,14.0140004 -13.96,14.0140004 z m -0.05,-5.176 c 4.93,0 8.88,-3.955 8.88,-8.8870004 0,-3.418 -1.95,-6.25 -4.73,-7.764 -1.22,-0.683 -1.61,-1.123 -1.61,-2.88 v -13.819 c 0,-1.416 -1.13,-2.539 -2.54,-2.539 -1.37,0 -2.49,1.123 -2.49,2.539 v 13.819 c 0,1.757 -0.39,2.197 -1.62,2.88 -2.83,1.514 -4.73,4.346 -4.73,7.764 0,4.9320004 3.95,8.8870004 8.84,8.8870004 z m 0,-39.6000004 c 1.41,0 2.54,-1.172 2.54,-2.539 0,-1.416 -1.13,-2.539 -2.54,-2.539 -1.37,0 -2.49,1.123 -2.49,2.539 0,1.367 1.12,2.539 2.49,2.539 z m 0,-8.838 c 1.41,0 2.54,-1.172 2.54,-2.539 0,-1.416 -1.13,-2.539 -2.54,-2.539 -1.37,0 -2.49,1.123 -2.49,2.539 0,1.367 1.12,2.539 2.49,2.539 z m 0,-8.789 c 1.41,0 2.54,-1.172 2.54,-2.539 0,-1.416 -1.13,-2.539 -2.54,-2.539 -1.37,0 -2.49,1.123 -2.49,2.539 0,1.367 1.12,2.539 2.49,2.539 z"
style="clip-rule:evenodd;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5px;stroke-linejoin:round;stroke-miterlimit:2"
id="path4429" />
</g>
<g
id="Ultralight-S"
transform="matrix(1 0 0 1 531.717 696)">
<path
d="m 70.343736,-2.1090003 c 0,-0.4319999 -0.014,-0.8619999 -0.042,-1.2899999 -0.073,-1.102 -1.026,-1.937 -2.127,-1.865 -1.101,0.072 -1.937,1.025 -1.865,2.127 0.023,0.341 0.034,0.684 0.034,1.0279999 0,1.104 0.896,2.0000001 2,2.0000001 1.104,0 2,-0.8960001 2,-2.0000001 z M 65.759736,-15.126 c -0.488,-0.655 -1.013,-1.3 -1.573,-1.931 -0.732,-0.826 -1.997,-0.902 -2.823,-0.169 -0.826,0.732 -0.902,1.997 -0.169,2.823 0.483,0.545 0.936,1.1 1.357,1.666 0.659,0.885 1.913,1.069 2.798,0.41 0.886,-0.659 1.07,-1.913 0.41,-2.799 z m -11.082,-9.548 c -0.686,-0.402 -1.39,-0.792 -2.113,-1.171 -0.978,-0.512 -2.187,-0.134 -2.7,0.844 -0.512,0.978 -0.134,2.187 0.844,2.7 0.666,0.348 1.315,0.708 1.946,1.078 0.953,0.558 2.179,0.238 2.737,-0.714 0.558,-0.952 0.238,-2.179 -0.714,-2.737 z M 1.6577364,-25.845 c -0.72300001,0.379 -1.42800001,0.769 -2.11300001,1.171 -0.95299999,0.558 -1.27299999,1.785 -0.71399999,2.737 0.55799999,0.952 1.78399999,1.272 2.736,0.714 0.632,-0.37 1.281,-0.73 1.947,-1.078 0.978,-0.513 1.356,-1.722 0.843,-2.7 -0.512,-0.978 -1.722,-1.356 -2.699,-0.844 z m -11.622,8.788 c -0.5600004,0.631 -1.0850004,1.276 -1.5740004,1.931 -0.659,0.886 -0.475,2.14 0.41,2.799 0.886,0.659 2.1400004,0.476 2.7990004,-0.41 0.421,-0.566 0.874,-1.121 1.357,-1.666 0.733,-0.826 0.657,-2.091 -0.169,-2.823 -0.826,-0.733 -2.091,-0.657 -2.823,0.169 z m -6.1150004,13.6579998 c -0.028,0.428 -0.042,0.858 -0.042,1.2899999 0,1.104 0.896,2.0000001 2,2.0000001 1.104,0 2,-0.8960001 2,-2.0000001 0,-0.3439999 0.011,-0.6869999 0.033,-1.0279999 0.073,-1.102 -0.763,-2.055 -1.864,-2.127 -1.102,-0.072 -2.055,0.763 -2.127,1.865 z m 43.192,10.087 c 10.095,0 18.227,-8.1770001 18.227,-18.1809998 0,-5.409 -2.214,-10.131 -6.891,-13.876 -1.145,-0.914 -1.382,-1.577 -1.382,-3.192 v -38.103 c 0,-6.202 -4.121,-10.581 -9.954,-10.581 -5.836,0 -9.96,4.379 -9.96,10.581 v 38.103 c 0,1.615 -0.238,2.278 -1.379,3.192 -4.677,3.745 -6.8909996,8.467 -6.8909996,13.876 0,10.0039997 8.1319996,18.1809998 18.2299996,18.1809998 z m 0,-2.033 c -8.899,0 -16.148,-7.298 -16.148,-16.1479998 0,-4.873 2.023,-9.306 6.39,-12.487 1.395,-1.005 1.926,-2.23 1.926,-3.971 v -38.693 c 0,-4.95 3.262,-8.473 7.832,-8.473 4.567,0 7.826,3.523 7.826,8.473 v 38.693 c 0,1.741 0.534,2.966 1.929,3.971 4.364,3.181 6.39,7.614 6.39,12.487 0,8.8499998 -7.249,16.1479998 -16.145,16.1479998 z m -0.003,-5.13100005 c 6.112,0 10.975,-4.90799995 10.975,-11.02099975 0,-4.28 -2.362,-7.793 -5.917,-9.67 -1.447,-0.775 -1.929,-1.351 -1.929,-3.517 v -10.503 c 0,-1.734 -1.395,-3.13 -3.129,-3.13 -1.731,0 -3.126,1.396 -3.126,3.13 v 10.503 c 0,2.166 -0.482,2.742 -1.929,3.517 -3.559,1.877 -5.917,5.39 -5.917,9.67 0,6.1129998 4.863,11.02099975 10.972,11.02099975 z m 0,-42.05099975 c 1.734,0 3.129,-1.445 3.129,-3.175 0,-1.734 -1.395,-3.129 -3.129,-3.129 -1.731,0 -3.126,1.395 -3.126,3.129 0,1.73 1.395,3.175 3.126,3.175 z m 0,-10.473 c 1.734,0 3.129,-1.399 3.129,-3.129 0,-1.78 -1.395,-3.175 -3.129,-3.175 -1.731,0 -3.126,1.395 -3.126,3.175 0,1.73 1.395,3.129 3.126,3.129 z m 0,-10.515 c 1.734,0 3.129,-1.399 3.129,-3.129 0,-1.734 -1.395,-3.175 -3.129,-3.175 -1.731,0 -3.126,1.441 -3.126,3.175 0,1.73 1.395,3.129 3.126,3.129 z"
style="clip-rule:evenodd;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5px;stroke-linejoin:round;stroke-miterlimit:2"
id="path4432" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -29,6 +29,8 @@ public struct ManagedAttribute<Value: Numeric> {
converter = { $0.int32Value as? Value }
} else if Value.self == Int64.self {
converter = { $0.int64Value as? Value }
} else if Value.self == UInt32.self {
converter = { $0.uint32Value as? Value }
} else {
fatalError("Unsupported type: \(Value.self)")
}

View file

@ -10,17 +10,17 @@ import Foundation
extension Date {
var lastHeard: String {
if timeIntervalSince1970 > 0 {
if self.timeIntervalSince1970 > 0 && self < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
formatted()
} else {
"unknown"
"unknown.age".localized
}
}
func formattedDate(format: String) -> String {
let dateformat = DateFormatter()
dateformat.dateFormat = format
if self > Calendar.current.date(byAdding: .year, value: -5, to: Date())! {
if self.timeIntervalSince1970 > 0 && self < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
return dateformat.string(from: self)
} else {
return "unknown.age".localized

View file

@ -93,12 +93,51 @@ extension String {
// Filter out variation selectors from the string
var withoutVariationSelectors: String {
return self.unicodeScalars
.filter { scalar in
return !scalar.properties.isVariationSelector
var scalars: [UnicodeScalar] = []
var previousWasASCII = false
for scalar in self.unicodeScalars {
if scalar.properties.isVariationSelector {
// Only keep variation selector if the previous character was ASCII
if previousWasASCII {
scalars.append(scalar)
}
// No need to update previousWasASCII since variation selectors aren't characters
// Shouldn't have 2 in a row
} else {
scalars.append(scalar)
previousWasASCII = scalar.isASCII
}
.compactMap { UnicodeScalar($0) }
}
return scalars.compactMap { UnicodeScalar($0) }
.map { String($0) }
.joined()
}
// 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 {
var result = ""
let scalars = self.unicodeScalars
var index = scalars.startIndex
while index < scalars.endIndex {
let currentScalar = scalars[index]
result += String(currentScalar)
if currentScalar.properties.isEmoji && !currentScalar.properties.isEmojiPresentation && !currentScalar.isASCII {
// Check if the next scalar is U+FE0F
let nextIndex = scalars.index(after: index)
if nextIndex < scalars.endIndex && scalars[nextIndex].value == 0xFE0F {
// Already has variation selector; skip the next scalar
index = nextIndex
} else {
// Append variation selector
result += String(UnicodeScalar(0xFE0F)!)
}
}
// Move to the next scalar
index = scalars.index(after: index)
}
return result
}
}

View file

@ -119,7 +119,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
self.isConnected = false
self.isConnecting = false
self.lastConnectionError = "🚨 " + String.localizedStringWithFormat("Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth.".localized, timeoutTimerCount, name)
Logger.services.error("\(self.lastConnectionError)")
Logger.services.error("\(self.lastConnectionError, privacy: .public)")
self.timeoutTimerCount = 0
self.startScanning()
} else {
@ -295,7 +295,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
} else {
// Disconnected without error which indicates user intent to disconnect
// Happens when swiping to disconnect
Logger.services.info(" [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public): \(String(describing: "User Initiated Disconnect".localized))")
Logger.services.info(" [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public): \(String(describing: "User Initiated Disconnect".localized), privacy: .public)")
}
// Start a scan so the disconnected peripheral is moved to the peripherals[] if it is awake
self.startScanning()
@ -485,7 +485,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
let logString = String.localizedStringWithFormat("mesh.log.traceroute.sent %@".localized, destNum.toHex())
Logger.mesh.info("🪧 \(logString)")
Logger.mesh.info("🪧 \(logString, privacy: .public)")
} catch {
@ -498,14 +498,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return }
if FROMRADIO_characteristic == nil {
Logger.mesh.error("🚨 \("firmware.version.unsupported".localized)")
Logger.mesh.error("🚨 \("firmware.version.unsupported".localized, privacy: .public)")
invalidVersion = true
return
} else {
let nodeName = connectedPeripheral?.peripheral.name ?? "unknown".localized
let logString = String.localizedStringWithFormat("mesh.log.wantconfig %@".localized, nodeName)
Logger.mesh.info("🛎️ \(logString)")
Logger.mesh.info("🛎️ \(logString, privacy: .public)")
// BLE Characteristics discovered, issue wantConfig
var toRadio: ToRadio = ToRadio()
configNonce += 1
@ -537,7 +537,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if let coordsMatch = try CommonRegex.COORDS_REGEX.firstMatch(in: logString) {
log = "\(log.replacingOccurrences(of: "DEBUG |", with: "").trimmingCharacters(in: .whitespaces))"
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
Logger.radio.debug("🛰️ \(log.prefix(upTo: coordsMatch.range.lowerBound), privacy: .public) \(coordsMatch.0.replacingOccurrences(of: "[,]", with: "", options: .regularExpression), privacy: .private) \(log.suffix(from: coordsMatch.range.upperBound), privacy: .public)")
Logger.radio.debug("🛰️ \(log.prefix(upTo: coordsMatch.range.lowerBound), privacy: .public) \(coordsMatch.0.replacingOccurrences(of: "[,]", with: "", options: .regularExpression), privacy: .private(mask: .none)) \(log.suffix(from: coordsMatch.range.upperBound), privacy: .public)")
} else {
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
Logger.radio.debug("🕵🏻‍♂️ \(log.replacingOccurrences(of: "DEBUG |", with: "").trimmingCharacters(in: .whitespaces), privacy: .public)")
@ -690,24 +690,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
connectedPeripheral.longName = myInfo?.bleName ?? "unknown".localized
let newConnection = Int64(UserDefaults.preferredPeripheralNum) != Int64(decodedInfo.myInfo.myNodeNum)
if newConnection {
let container = NSPersistentContainer(name: "Meshtastic")
if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let databasePath = url.appendingPathComponent("backup")
.appendingPathComponent("\(UserDefaults.preferredPeripheralNum)")
.appendingPathComponent("Meshtastic.sqlite")
if FileManager.default.fileExists(atPath: databasePath.path) {
do {
disconnectPeripheral(reconnect: false)
try container.restorePersistentStore(from: databasePath)
UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0)
context.refreshAllObjects()
Logger.data.notice("🗂️ Restored Core data for /\(UserDefaults.preferredPeripheralNum, privacy: .public)")
connectTo(peripheral: peripheral)
} catch {
Logger.data.error("🗂️ Restore Core data copy error: \(error, privacy: .public)")
}
}
}
// Onboard a new device connection here
}
}
tryClearExistingChannels()
@ -767,7 +750,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
}
// Log any other unknownApp calls
if !nowKnown { Logger.mesh.info("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") }
if !nowKnown { Logger.mesh.info("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") }
case .textMessageApp, .detectionSensorApp:
textMessageAppPacket(
packet: decodedInfo.packet,
@ -786,7 +769,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
appState: appState
)
case .remoteHardwareApp:
Logger.mesh.info("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
Logger.mesh.info("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
case .positionApp:
upsertPositionPacket(packet: decodedInfo.packet, context: context)
case .waypointApp:
@ -811,11 +794,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
case .serialApp:
Logger.mesh.info("🕸️ MESH PACKET received for Serial App UNHANDLED UNHANDLED")
case .storeForwardApp:
if wantStoreAndForwardPackets {
storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context)
} else {
Logger.mesh.info("🕸️ MESH PACKET received for Store and Forward App - Store and Forward is disabled.")
}
storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context)
case .rangeTestApp:
if wantRangeTestPackets {
textMessageAppPacket(
@ -988,31 +967,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
Logger.data.error("Error Updating Core Data TraceRouteHop: \(nsError, privacy: .public)")
}
let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString)
Logger.mesh.info("🪧 \(logString)")
Logger.mesh.info("🪧 \(logString, privacy: .public)")
}
case .neighborinfoApp:
if let neighborInfo = try? NeighborInfo(serializedBytes: decodedInfo.packet.decoded.payload) {
Logger.mesh.info("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED \((try? neighborInfo.jsonString()) ?? "JSON Decode Failure")")
Logger.mesh.info("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED \((try? neighborInfo.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
}
case .paxcounterApp:
paxCounterPacket(packet: decodedInfo.packet, context: context)
case .mapReportApp:
Logger.mesh.info("🕸️ MESH PACKET received Map Report App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
Logger.mesh.info("🕸️ MESH PACKET received Map Report App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
case .UNRECOGNIZED:
Logger.mesh.info("🕸️ MESH PACKET received UNRECOGNIZED App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
Logger.mesh.info("🕸️ MESH PACKET received UNRECOGNIZED App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
case .max:
Logger.services.info("MAX PORT NUM OF 511")
case .atakPlugin:
Logger.mesh.info("🕸️ MESH PACKET received for ATAK Plugin App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
Logger.mesh.info("🕸️ MESH PACKET received for ATAK Plugin App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
case .powerstressApp:
Logger.mesh.info("🕸️ MESH PACKET received for Power Stress App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
Logger.mesh.info("🕸️ MESH PACKET received for Power Stress App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
case .reticulumTunnelApp:
Logger.mesh.info("🕸️ MESH PACKET received for Reticulum Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
}
if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce {
invalidVersion = false
lastConnectionError = ""
isSubscribed = true
Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID)")
Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)")
if sendTime() {
}
peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected })
@ -1046,7 +1027,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
} catch {
Logger.data.error("Failed to find a node info for the connected node \(error.localizedDescription)")
Logger.data.error("Failed to find a node info for the connected node \(error.localizedDescription, privacy: .public)")
}
}
@ -1090,7 +1071,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
let nodeName = connectedPeripheral?.peripheral.name ?? "unknown".localized
let logString = String.localizedStringWithFormat("mesh.log.textmessage.send.failed %@".localized, nodeName)
Logger.mesh.info("🚫 \(logString)")
Logger.mesh.info("🚫 \(logString, privacy: .public)")
success = false
} else if message.count < 1 {
@ -1177,7 +1158,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("mesh.log.textmessage.sent %@ %@ %@".localized, String(newMessage.messageId), fromUserNum.toHex(), toUserNum.toHex())
Logger.mesh.info("💬 \(logString)")
Logger.mesh.info("💬 \(logString, privacy: .public)")
do {
try context.save()
Logger.data.info("💾 Saved a new sent message from \(self.connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)")
@ -1224,7 +1205,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return false
}
let logString = String.localizedStringWithFormat("mesh.log.waypoint.sent %@".localized, String(fromNodeNum))
Logger.mesh.info("📍 \(logString)")
Logger.mesh.info("📍 \(logString, privacy: .public)")
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
success = true
@ -1388,7 +1369,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum))
Logger.services.debug("📍 \(logString)")
Logger.services.debug("📍 \(logString, privacy: .public)")
return true
} else {
Logger.services.error("Device no longer connected. Unable to send position information.")
@ -1704,7 +1685,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
}
} catch {
Logger.data.error("Failed to find a node MyInfo to save these channels to: \(error.localizedDescription)")
Logger.data.error("Failed to find a node MyInfo to save these channels to: \(error.localizedDescription, privacy: .public)")
}
}
@ -1743,7 +1724,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("mesh.log.channel.sent %@ %d".localized, String(connectedPeripheral.num), chan.index)
Logger.mesh.info("🎛️ \(logString)")
Logger.mesh.info("🎛️ \(logString, privacy: .public)")
}
}
// Save the LoRa Config and the device will reboot
@ -1772,7 +1753,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("mesh.log.lora.config.sent %@".localized, String(connectedPeripheral.num))
Logger.mesh.info("📻 \(logString)")
Logger.mesh.info("📻 \(logString, privacy: .public)")
}
if self.connectedPeripheral != nil {
@ -1849,7 +1830,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("🚫 Error deleting node from core data: \(nsError)")
Logger.data.error("🚫 Error deleting node from core data: \(nsError, privacy: .public)")
}
}
return false
@ -2692,7 +2673,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat("mesh.log.cannedmessages.messages.get %@".localized, String(connectedPeripheral.num))
Logger.mesh.info("🥫 \(logString)")
Logger.mesh.info("🥫 \(logString, privacy: .public)")
return true
}
@ -3272,7 +3253,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
Logger.mesh.debug("\(adminDescription)")
Logger.mesh.debug("\(adminDescription, privacy: .public)")
return true
}
return false
@ -3469,7 +3450,7 @@ extension BLEManager: CBCentralManagerDelegate {
default:
status = "default"
}
Logger.services.info("📜 [BLE] Bluetooth status: \(status)")
Logger.services.info("📜 [BLE] Bluetooth status: \(status, privacy: .public)")
}
// Called each time a peripheral is discovered

View file

@ -75,7 +75,7 @@ class LocalNotificationManager {
UNUserNotificationCenter.current().add(request) { error in
if let error {
Logger.services.error("Error Scheduling Notification: \(error.localizedDescription)")
Logger.services.error("Error Scheduling Notification: \(error.localizedDescription, privacy: .public)")
}
}
}

View file

@ -69,7 +69,7 @@ import OSLog
}
}
} catch {
Logger.services.error("💥 [App] Could not start location updates: \(error.localizedDescription)")
Logger.services.error("💥 [App] Could not start location updates: \(error.localizedDescription, privacy: .public)")
}
return
}
@ -84,15 +84,15 @@ import OSLog
if smartPostion {
let age = -location.timestamp.timeIntervalSinceNow
if age > 10 {
Logger.services.info("📍 [App] Smart Position - Bad Location: Too Old \(age, privacy: .public) seconds ago \(location, privacy: .private)")
Logger.services.info("📍 [App] Smart Position - Bad Location: Too Old \(age, privacy: .public) seconds ago \(location, privacy: .private(mask: .none))")
return false
}
if location.horizontalAccuracy < 0 {
Logger.services.info("📍 [App] Smart Position - Bad Location: Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)")
Logger.services.info("📍 [App] Smart Position - Bad Location: Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private(mask: .none))")
return false
}
if location.horizontalAccuracy > 5 {
Logger.services.info("📍 [App] Smart Position - Bad Location: Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)")
Logger.services.info("📍 [App] Smart Position - Bad Location: Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private(mask: .none))")
return false
}
}

View file

@ -104,7 +104,7 @@ func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNu
func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedObjectContext) -> MyInfoEntity? {
let logString = String.localizedStringWithFormat("mesh.log.myinfo %@".localized, String(myInfo.myNodeNum))
Logger.mesh.info(" \(logString)")
Logger.mesh.info(" \(logString, privacy: .public)")
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(myInfo.myNodeNum))
@ -155,7 +155,7 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
if channel.isInitialized && channel.hasSettings && channel.role != Channel.Role.disabled {
let logString = String.localizedStringWithFormat("mesh.log.channel.received %d %@".localized, channel.index, String(fromNum))
Logger.mesh.info("🎛️ \(logString)")
Logger.mesh.info("🎛️ \(logString, privacy: .public)")
let fetchedMyInfoRequest = MyInfoEntity.fetchRequest()
fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", fromNum)
@ -194,7 +194,7 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
} catch {
Logger.data.error("💥 Failed to save channel: \(error.localizedDescription, privacy: .public)")
}
Logger.data.info("💾 Updated MyInfo channel \(channel.index) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum)")
Logger.data.info("💾 Updated MyInfo channel \(channel.index, privacy: .public) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum, privacy: .public)")
} else if channel.role.rawValue > 0 {
Logger.data.error("💥Trying to save a channel to a MyInfo that does not exist: \(fromNum.toHex(), privacy: .public)")
}
@ -210,7 +210,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPass
if metadata.isInitialized {
let logString = String.localizedStringWithFormat("mesh.log.device.metadata.received %@".localized, fromNum.toHex())
Logger.mesh.info("🏷️ \(logString)")
Logger.mesh.info("🏷️ \(logString, privacy: .public)")
let fetchedNodeRequest = NodeInfoEntity.fetchRequest()
fetchedNodeRequest.predicate = NSPredicate(format: "num == %lld", fromNum)
@ -226,6 +226,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPass
newMetadata.hasEthernet = metadata.hasEthernet_p
newMetadata.role = Int32(metadata.role.rawValue)
newMetadata.positionFlags = Int32(metadata.positionFlags)
newMetadata.excludedModules = Int32(metadata.excludedModules)
// Swift does strings weird, this does work to get the version without the github hash
let lastDotIndex = metadata.firmwareVersion.lastIndex(of: ".")
var version = metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: metadata.firmwareVersion))]
@ -261,7 +262,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPass
func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? {
let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, String(nodeInfo.num))
Logger.mesh.info("📟 \(logString)")
Logger.mesh.info("📟 \(logString, privacy: .public)")
guard nodeInfo.num > 0 else { return nil }
@ -349,12 +350,12 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
}
do {
try context.save()
Logger.data.info("💾 Saved a new Node Info For: \(String(nodeInfo.num))")
Logger.data.info("💾 Saved a new Node Info For: \(String(nodeInfo.num), privacy: .public)")
return newNode
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError)")
Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError, privacy: .public)")
}
} catch {
Logger.data.error("Fetch MyInfo Error")
@ -472,7 +473,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
if !cmmc.messages.isEmpty {
let logString = String.localizedStringWithFormat("mesh.log.cannedmessages.messages.received %@".localized, packet.from.toHex())
Logger.mesh.info("🥫 \(logString)")
Logger.mesh.info("🥫 \(logString, privacy: .public)")
let fetchNodeRequest = NodeInfoEntity.fetchRequest()
fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
@ -547,7 +548,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let ringtone = adminMessage.getRingtoneResponse
upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: Int64(packet.from), context: context)
} else {
Logger.mesh.error("🕸️ MESH PACKET received Admin App UNHANDLED \((try? packet.decoded.jsonString()) ?? "JSON Decode Failure")")
Logger.mesh.error("🕸️ MESH PACKET received Admin App UNHANDLED \((try? packet.decoded.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
}
// Save an ack for the admin message log for each admin message response received as we stopped sending acks if there is also a response to reduce airtime.
adminResponseAck(packet: packet, context: context)
@ -572,17 +573,17 @@ func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) {
do {
try context.save()
} catch {
Logger.data.error("Failed to save admin message response as an ack: \(error.localizedDescription)")
Logger.data.error("Failed to save admin message response as an ack: \(error.localizedDescription, privacy: .public)")
}
}
} catch {
Logger.data.error("Failed to fetch admin message by requestID: \(error.localizedDescription)")
Logger.data.error("Failed to fetch admin message by requestID: \(error.localizedDescription, privacy: .public)")
}
}
func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.paxcounter %@".localized, String(packet.from))
Logger.mesh.info("🧑‍🤝‍🧑 \(logString)")
Logger.mesh.info("🧑‍🤝‍🧑 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
@ -607,7 +608,7 @@ func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
do {
try context.save()
} catch {
Logger.data.error("Failed to save pax: \(error.localizedDescription)")
Logger.data.error("Failed to save pax: \(error.localizedDescription, privacy: .public)")
}
} else {
Logger.data.info("Node Info Not Found")
@ -626,7 +627,7 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
let routingErrorString = routingError?.display ?? "unknown".localized
let logString = String.localizedStringWithFormat("mesh.log.routing.message %@ %@".localized, String(packet.decoded.requestID), routingErrorString)
Logger.mesh.info("🕸️ \(logString)")
Logger.mesh.info("🕸️ \(logString, privacy: .public)")
let fetchMessageRequest = MessageEntity.fetchRequest()
fetchMessageRequest.predicate = NSPredicate(format: "messageId == %lld", Int64(packet.decoded.requestID))
@ -673,11 +674,11 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
return
}
try context.save()
Logger.data.info("💾 ACK Saved for Message: \(packet.decoded.requestID)")
Logger.data.info("💾 ACK Saved for Message: \(packet.decoded.requestID, privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving ACK for message: \(packet.id) Error: \(nsError)")
Logger.data.error("Error Saving ACK for message: \(packet.id, privacy: .public) Error: \(nsError, privacy: .public)")
}
}
}
@ -687,7 +688,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) {
let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from))
Logger.mesh.info("📈 \(logString)")
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) {
/// Other unhandled telemetry packets
@ -723,10 +724,20 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
telemetry.current = telemetryMessage.environmentMetrics.hasCurrent.then(telemetryMessage.environmentMetrics.current)
telemetry.voltage = telemetryMessage.environmentMetrics.hasVoltage.then(telemetryMessage.environmentMetrics.voltage)
telemetry.weight = telemetryMessage.environmentMetrics.hasWeight.then(telemetryMessage.environmentMetrics.weight)
telemetry.distance = telemetryMessage.environmentMetrics.hasDistance.then(telemetryMessage.environmentMetrics.distance)
telemetry.windSpeed = telemetryMessage.environmentMetrics.hasWindSpeed.then(telemetryMessage.environmentMetrics.windSpeed)
telemetry.windGust = telemetryMessage.environmentMetrics.hasWindGust.then(telemetryMessage.environmentMetrics.windGust)
telemetry.windLull = telemetryMessage.environmentMetrics.hasWindLull.then(telemetryMessage.environmentMetrics.windLull)
telemetry.windDirection = telemetryMessage.environmentMetrics.hasWindDirection.then(Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.windDirection))
telemetry.irLux = telemetryMessage.environmentMetrics.hasIrLux.then(telemetryMessage.environmentMetrics.irLux)
telemetry.lux = telemetryMessage.environmentMetrics.hasLux.then(telemetryMessage.environmentMetrics.lux)
telemetry.whiteLux = telemetryMessage.environmentMetrics.hasWhiteLux.then(telemetryMessage.environmentMetrics.whiteLux)
telemetry.uvLux = telemetryMessage.environmentMetrics.hasUvLux.then(telemetryMessage.environmentMetrics.uvLux)
telemetry.radiation = telemetryMessage.environmentMetrics.hasRadiation.then(telemetryMessage.environmentMetrics.radiation)
telemetry.rainfall1H = telemetryMessage.environmentMetrics.hasRainfall1H.then(telemetryMessage.environmentMetrics.rainfall1H)
telemetry.rainfall24H = telemetryMessage.environmentMetrics.hasRainfall24H.then(telemetryMessage.environmentMetrics.rainfall24H)
telemetry.soilTemperature = telemetryMessage.environmentMetrics.hasSoilTemperature.then(telemetryMessage.environmentMetrics.soilTemperature)
telemetry.soilMoisture = telemetryMessage.environmentMetrics.hasSoilMoisture.then(telemetryMessage.environmentMetrics.soilMoisture)
telemetry.metricsType = 1
} else if telemetryMessage.variant == Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) {
// Local Stats for Live activity
@ -771,7 +782,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
}
try context.save()
Logger.data.info("💾 [TelemetryEntity] of type \(MetricsTypes(rawValue: Int(telemetry.metricsType))?.name ?? "Unknown Metrics Type") Saved for Node: \(packet.from.toHex())")
Logger.data.info("💾 [TelemetryEntity] of type \(MetricsTypes(rawValue: Int(telemetry.metricsType))?.name ?? "Unknown Metrics Type", privacy: .public) Saved for Node: \(packet.from.toHex(), privacy: .public)")
if telemetry.metricsType == 0 {
// Connected Device Metrics
// ------------------------
@ -872,7 +883,7 @@ func textMessageAppPacket(
}
if messageText?.count ?? 0 > 0 {
Logger.mesh.info("💬 \("mesh.log.textmessage.received".localized)")
Logger.mesh.info("💬 \("mesh.log.textmessage.received".localized, privacy: .public)")
let messageUsers = UserEntity.fetchRequest()
messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from])
do {
@ -898,11 +909,14 @@ func textMessageAppPacket(
if packet.decoded.replyID > 0 {
newMessage.replyID = Int64(packet.decoded.replyID)
}
// Updated logic for handling toUser
if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != Constants.maximumNodeNum {
if !storeForwardBroadcast {
newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to })
} else if storeForwardBroadcast {
// For S&F broadcast messages, treat as a channel message (not a DM)
newMessage.toUser = nil
} else {
/// Make a new to user if they are unknown
newMessage.toUser = createUser(num: Int64(truncatingIfNeeded: packet.to), context: context)
}
}
@ -948,7 +962,7 @@ func textMessageAppPacket(
var messageSaved = false
do {
try context.save()
Logger.data.info("💾 Saved a new message for \(newMessage.messageId)")
Logger.data.info("💾 Saved a new message for \(newMessage.messageId, privacy: .public)")
messageSaved = true
if messageSaved {
@ -961,7 +975,7 @@ func textMessageAppPacket(
appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0
}
if !(newMessage.fromUser?.mute ?? false) {
// Create an iOS Notification for the received DM message and schedule it immediately
// Create an iOS Notification for the received DM message
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
@ -978,23 +992,21 @@ func textMessageAppPacket(
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
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 private channel message and schedule it immediately
// Create an iOS Notification for the received channel message
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
@ -1007,10 +1019,11 @@ func textMessageAppPacket(
messageId: newMessage.messageId,
channel: newMessage.channel,
userNum: Int64(newMessage.fromUser?.userId ?? "0"),
critical: critical)
critical: critical
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized, privacy: .public)")
}
}
}
@ -1022,7 +1035,7 @@ func textMessageAppPacket(
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Failed to save new MessageEntity \(nsError)")
Logger.data.error("Failed to save new MessageEntity \(nsError, privacy: .public)")
}
} catch {
Logger.data.error("Fetch Message To and From Users Error")
@ -1033,7 +1046,7 @@ func textMessageAppPacket(
func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from))
Logger.mesh.info("📍 \(logString)")
Logger.mesh.info("📍 \(logString, privacy: .public)")
let fetchWaypointRequest = WaypointEntity.fetchRequest()
fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(packet.id))
@ -1060,7 +1073,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
waypoint.created = Date()
do {
try context.save()
Logger.data.info("💾 Added Node Waypoint App Packet For: \(waypoint.id)")
Logger.data.info("💾 Added Node Waypoint App Packet For: \(waypoint.id, privacy: .public)")
let manager = LocalNotificationManager()
let icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍")
let latitude = Double(waypoint.latitudeI) / 1e7
@ -1075,12 +1088,12 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
path: "meshtastic:///map?waypointid=\(waypoint.id)"
)
]
Logger.data.debug("meshtastic:///map?waypointid=\(waypoint.id)")
Logger.data.debug("meshtastic:///map?waypointid=\(waypoint.id, privacy: .public)")
manager.schedule()
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError)")
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)")
}
} else {
fetchedWaypoint[0].id = Int64(packet.id)
@ -1098,11 +1111,11 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
fetchedWaypoint[0].lastUpdated = Date()
do {
try context.save()
Logger.data.info("💾 Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id)")
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)")
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)")
}
}
}

View file

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>MeshtasticDataModelV 49.xcdatamodel</string>
<string>MeshtasticDataModelV 50.xcdatamodel</string>
</dict>
</plist>

View file

@ -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="24D70" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="23G80" 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"/>
@ -373,7 +373,8 @@
<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="isRouter" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isServer" optional="YES" attributeType="Boolean" 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"/>

View file

@ -0,0 +1,504 @@
<?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="">
<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="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>

View file

@ -38,6 +38,8 @@ struct MeshtasticAppleApp: App {
// Wire up router
self.appDelegate.router = appState.router
// Show Tips
try? Tips.resetDatastore()
}
var body: some Scene {
@ -55,7 +57,7 @@ struct MeshtasticAppleApp: App {
.presentationDragIndicator(.visible)
}
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
Logger.mesh.debug("URL received \(userActivity)")
Logger.mesh.debug("URL received \(userActivity, privacy: .public)")
self.incomingUrl = userActivity.webpageURL
if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#")) != nil {
@ -72,18 +74,18 @@ struct MeshtasticAppleApp: App {
}
self.channelSettings = cs
}
Logger.services.debug("Add Channel \(self.addChannels)")
Logger.services.debug("Add Channel \(self.addChannels, privacy: .public)")
}
self.saveChannels = true
Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")")
}
if self.saveChannels {
Logger.mesh.debug("User wants to open Channel Settings URL: \(String(describing: self.incomingUrl!.relativeString))")
Logger.mesh.debug("User wants to open Channel Settings URL: \(String(describing: self.incomingUrl!.relativeString), privacy: .public)")
}
}
.onOpenURL(perform: { (url) in
Logger.mesh.debug("Some sort of URL was received \(url)")
Logger.mesh.debug("Some sort of URL was received \(url, privacy: .public)")
self.incomingUrl = url
if url.absoluteString.lowercased().contains("meshtastic.org/e/#") {
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
@ -99,21 +101,15 @@ struct MeshtasticAppleApp: App {
}
self.channelSettings = cs
}
Logger.services.debug("Add Channel \(self.addChannels)")
Logger.services.debug("Add Channel \(self.addChannels, privacy: .public)")
}
self.saveChannels = true
Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")")
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)
}
})
.task {
#if DEBUG
/// Optionally, call `Tips.resetDatastore()` before `Tips.configure()` to reset the state of all tips. This will allow tips to re-appear even after they have been dismissed by the user.
/// This is for testing only, and should not be enabled in release builds.
try? Tips.resetDatastore()
#endif
try? Tips.configure(
[
// Reset which tips have been shown and what parameters have been tracked, useful during testing and for this sample project

View file

@ -97,10 +97,10 @@ 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) \(deepLink)")
Logger.services.info("userNotificationCenter didReceiveResponse \(targetValue, privacy: .public) \(deepLink, privacy: .public)")
router?.route(url: url)
} else {
Logger.services.error("Failed to handle notification response: \(userInfo)")
Logger.services.error("Failed to handle notification response: \(userInfo, privacy: .public)")
}
completionHandler()
}

View file

@ -42,5 +42,14 @@ public class TelemetryEntity: NSManagedObject, Identifiable {
@ManagedAttribute<Float>(attributeName: "windGust") public var windGust: Float?
@ManagedAttribute<Float>(attributeName: "windLull") public var windLull: Float?
@ManagedAttribute<Float>(attributeName: "windSpeed") public var windSpeed: Float?
@ManagedAttribute<Float>(attributeName: "irLux") public var irLux: Float?
@ManagedAttribute<Float>(attributeName: "lux") public var lux: Float?
@ManagedAttribute<Float>(attributeName: "uvLux") public var uvLux: Float?
@ManagedAttribute<Float>(attributeName: "whiteLux") public var whiteLux: Float?
@ManagedAttribute<Float>(attributeName: "radiation") public var radiation: Float?
@ManagedAttribute<Float>(attributeName: "rainfall1H") public var rainfall1H: Float?
@ManagedAttribute<Float>(attributeName: "rainfall24H") public var rainfall24H: Float?
@ManagedAttribute<Float>(attributeName: "soilTemperature") public var soilTemperature: Float?
@ManagedAttribute<UInt32>(attributeName: "soilMoisture") public var soilMoisture: UInt32?
}

View file

@ -38,12 +38,18 @@ class MetricsChartSeries: ObservableObject {
// Possibly converted to the proper units
let valueClosure: (TelemetryEntity) -> Float?
// Used for scaling the Y-axis
let initialYAxisRange: ClosedRange<Float>?
let minumumYAxisSpan: Float?
// Main initializer
init<Value, ChartBody: ChartContent, ForegroundStyle: ShapeStyle>(
id: String,
keyPath: KeyPath<TelemetryEntity, Value>,
name: String,
abbreviatedName: String,
initialYAxisRange: ClosedRange<Float>? = nil,
minumumYAxisSpan: Float? = nil,
conversion: ((Value) -> Value)? = nil,
visible: Bool = true,
foregroundStyle: @escaping ((ClosedRange<Float>?) -> ForegroundStyle?) = { _ in nil },
@ -54,6 +60,8 @@ class MetricsChartSeries: ObservableObject {
self.id = id
self.name = name
self.abbreviatedName = abbreviatedName
self.initialYAxisRange = initialYAxisRange
self.minumumYAxisSpan = minumumYAxisSpan
self.visible = visible
// By saving these closures, MetricsChartSeries can be type agnostic

View file

@ -7,6 +7,7 @@
import Foundation
import SwiftUI
class MetricsSeriesList: ObservableObject, RandomAccessCollection, RangeReplaceableCollection {
@Published var series: [MetricsChartSeries]
@ -38,23 +39,82 @@ class MetricsSeriesList: ObservableObject, RandomAccessCollection, RangeReplacea
return nil
}
// Calculates the chartRange based on the series configuration and data provided
// Besides checkign the range of the data, this function also obeys some series-level
// configuraiton, such as:
// 1. starting with a desired fixed range
// 2. obeying a minimum span
func chartRange(forData data: [TelemetryEntity]) -> ClosedRange<Float> {
var lower: Float?
var upper: Float?
var globalLower: Float = .infinity
var globalUpper: Float = -.infinity
// Keep track of the range of each series
var range: [MetricsChartSeries: ClosedRange<Float>] = [:]
// Determine if there is an initial fixed range.
// The range might exapand past this initial range if the data goes beyond.
for aSeries in self.visible {
if let thisRange = aSeries.initialYAxisRange {
range[aSeries] = thisRange
if thisRange.upperBound > globalUpper {globalUpper = thisRange.upperBound}
if thisRange.lowerBound < globalLower {globalLower = thisRange.lowerBound}
}
}
// Iterate through all the data. It would be easier to iterate
// the series then the data, but this way we only iterate the data once
for te in data {
for aSeries in self.visible {
var seriesUpper = range[aSeries]?.upperBound ?? -.infinity
var seriesLower = range[aSeries]?.lowerBound ?? .infinity
if let value = aSeries.valueFor(te) {
if value > (upper ?? -.infinity) {upper = value}
if value < (lower ?? .infinity) {lower = value}
// Update the global bounds
if value > globalUpper {globalUpper = value}
if value < globalLower {globalLower = value}
// Update the series bounds if necessary
if value > seriesUpper || value < seriesLower {
if value > seriesUpper {
seriesUpper = value
}
if value < seriesLower {
seriesLower = value
}
if seriesUpper.isFinite && seriesLower.isFinite {
range[aSeries] = seriesLower...seriesUpper
}
}
}
}
}
// Return default range if no data or nil
guard let lower, let upper else {
// Go through each series one last time to obey the minimum span
for aSeries in self.visible {
if let minimumSpan = aSeries.minumumYAxisSpan,
let currentRange = range[aSeries] {
let currentSpan = currentRange.upperBound - currentRange.lowerBound
if currentSpan < minimumSpan {
// Calculate the center of the range
let centerOfRange = currentRange.lowerBound + (currentSpan / 2)
let newLower = centerOfRange - (minimumSpan / 2.0)
let newUpper = centerOfRange + (minimumSpan / 2.0)
if newUpper > globalUpper {
globalUpper = newUpper
}
if newLower < globalLower {
globalLower = newLower
}
}
}
}
// Return default range if no data
if !globalLower.isFinite || !globalUpper.isFinite {
return 0.0...100.0
}
return lower...upper
return globalLower...globalUpper
}
// Collection conformance

View file

@ -48,7 +48,7 @@ class PersistenceController {
if let error = error as NSError? {
Logger.data.error("CoreData Error: \(error.localizedDescription). Now attempting to truncate CoreData database. All app data will be lost.")
Logger.data.error("CoreData Error: \(error.localizedDescription, privacy: .public). Now attempting to truncate CoreData database. All app data will be lost.")
self.clearDatabase()
}
})
@ -65,11 +65,11 @@ class PersistenceController {
do {
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
} catch let error {
Logger.data.error("Failed to re-create CoreData database: \(error.localizedDescription)")
Logger.data.error("Failed to re-create CoreData database: \(error.localizedDescription, privacy: .public)")
}
} catch let error {
Logger.data.error("Failed to destroy CoreData database, delete the app and re-install to clear data. Attempted to clear persistent store: \(error.localizedDescription)")
Logger.data.error("Failed to destroy CoreData database, delete the app and re-install to clear data. Attempted to clear persistent store: \(error.localizedDescription, privacy: .public)")
}
}
}
@ -103,123 +103,4 @@ extension NSPersistentContainer {
case invalidSource(String)
}
/// Restore backup persistent stores located in the directory referenced by `backupURL`.
/// **Be very careful with this**. To restore a persistent store, the current persistent store must be removed from the container. When that happens, **all currently loaded Core Data objects** will become invalid. Using them after restoring will cause your app to crash.
/// When calling this method you **must** ensure that you do not continue to use any previously fetched managed objects or existing fetched results controllers. **If this method does not throw, that does not mean your app is safe.** You need to take extra steps to prevent crashes. The details vary depending on the nature of your app.
/// - Parameter backupURL: A file URL containing backup copies of all currently loaded persistent stores.
/// - Throws: `CopyPersistentStoreError` in various situations.
/// - Returns: Nothing. If no errors are thrown, the restore is complete.
func restorePersistentStore(from backupURL: URL) throws {
guard backupURL.isFileURL else {
throw CopyPersistentStoreErrors.invalidSource("Backup URL must be a file URL")
}
var isDirectory: ObjCBool = false
if FileManager.default.fileExists(atPath: backupURL.path, isDirectory: &isDirectory) {
if !isDirectory.boolValue {
throw CopyPersistentStoreErrors.invalidSource("Source URL must be a directory")
}
} else {
throw CopyPersistentStoreErrors.invalidSource("Source URL must exist")
}
for persistentStoreDescription in persistentStoreDescriptions {
guard let loadedStoreURL = persistentStoreDescription.url else {
continue
}
let backupStoreURL = backupURL.appendingPathComponent(loadedStoreURL.lastPathComponent)
guard FileManager.default.fileExists(atPath: backupStoreURL.path) else {
throw CopyPersistentStoreErrors.invalidSource("Missing backup store for \(backupStoreURL)")
}
do {
let storeOptions = persistentStoreDescription.options
let configurationName = persistentStoreDescription.configuration
let storeType = persistentStoreDescription.type
// Replace the current store with the backup copy. This has a side effect of removing the current store from the Core Data stack.
// When restoring, it's necessary to use the current persistent store coordinator.
try persistentStoreCoordinator.replacePersistentStore(at: loadedStoreURL, destinationOptions: storeOptions, withPersistentStoreFrom: backupStoreURL, sourceOptions: storeOptions, ofType: storeType)
// Add the persistent store at the same location we've been using, because it was removed in the previous step.
try persistentStoreCoordinator.addPersistentStore(ofType: storeType, configurationName: configurationName, at: loadedStoreURL, options: storeOptions)
} catch {
throw CopyPersistentStoreErrors.copyStoreError("Could not restore: \(error.localizedDescription)")
}
}
}
/// Copy all loaded persistent stores to a new directory. Each currently loaded file-based persistent store will be copied (including journal files, external binary storage, and anything else Core Data needs) into the destination directory to a persistent store with the same name and type as the existing store. In-memory stores, if any, are skipped.
/// - Parameters:
/// - destinationURL: Destination for new persistent store files. Must be a file URL. If `overwriting` is `false` and `destinationURL` exists, it must be a directory.
/// - overwriting: If `true`, any existing copies of the persistent store will be replaced or updated. If `false`, existing copies will not be changed or remoted. When this is `false`, the destination persistent store file must not already exist.
/// - Throws: `CopyPersistentStoreError`
/// - Returns: Nothing. If no errors are thrown, all loaded persistent stores will be copied to the destination directory.
func copyPersistentStores(to destinationURL: URL, overwriting: Bool = false) throws {
guard !destinationURL.relativeString.contains("/0/") else {
throw CopyPersistentStoreErrors.invalidDestination("Invalid 0 Node Id")
}
guard destinationURL.isFileURL else {
throw CopyPersistentStoreErrors.invalidDestination("Destination URL must be a file URL")
}
// If the destination exists and we aren't overwriting it, then it must be a directory. (If we are overwriting, we'll remove it anyway, so it doesn't matter whether it's a directory).
var isDirectory: ObjCBool = false
if !overwriting && FileManager.default.fileExists(atPath: destinationURL.path, isDirectory: &isDirectory) {
if !isDirectory.boolValue {
throw CopyPersistentStoreErrors.invalidDestination("Destination URL must be a directory")
}
// Don't check if destination stores exist in the destination dir, that comes later on a per-store basis.
}
// If we're overwriting, remove the destination.
if overwriting && FileManager.default.fileExists(atPath: destinationURL.path) {
do {
try FileManager.default.removeItem(at: destinationURL)
} catch {
throw CopyPersistentStoreErrors.destinationNotRemoved("Can't overwrite destination at \(destinationURL)")
}
}
// Create the destination directory
do {
try FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
} catch {
throw CopyPersistentStoreErrors.destinationError("Could not create destination directory at \(destinationURL)")
}
for persistentStoreDescription in persistentStoreDescriptions {
guard let storeURL = persistentStoreDescription.url else {
continue
}
guard persistentStoreDescription.type != NSInMemoryStoreType else {
continue
}
let destinationStoreURL = destinationURL.appendingPathComponent(storeURL.lastPathComponent)
if !overwriting && FileManager.default.fileExists(atPath: destinationStoreURL.path) {
// If the destination exists, the replacePersistentStore call will update it in place. That's fine unless we're not overwriting.
throw CopyPersistentStoreErrors.destinationError("Destination already exists at \(destinationStoreURL)")
}
do {
// Replace an existing backup, if any, with a new one with the same options and type. This doesn't affect the current Core Data stack.
// The function name says "replace", but it works if there's nothing at the destination yet. In that case it creates a new persistent store.
// Note that for backup, it doesn't matter if the persistent store coordinator is the one currently in use or a different one. It could be a class function, for this use.
try persistentStoreCoordinator.replacePersistentStore(at: destinationStoreURL, destinationOptions: persistentStoreDescription.options, withPersistentStoreFrom: storeURL, sourceOptions: persistentStoreDescription.options, ofType: persistentStoreDescription.type)
/// Cleanup extra files
let directory = destinationStoreURL.deletingLastPathComponent()
/// Delete -wal file
do {
try FileManager.default.removeItem(at: directory.appendingPathComponent("Meshtastic.sqlite-wal"))
/// Delete -shm file
do {
try FileManager.default.removeItem(at: directory.appendingPathComponent("Meshtastic.sqlite-shm"))
} catch {
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite-shm file \(error, privacy: .public)")
}
} catch {
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite-wal file \(error, privacy: .public)")
}
} catch {
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite file \(error, privacy: .public)")
throw CopyPersistentStoreErrors.copyStoreError("\(error.localizedDescription)")
}
}
}
}

View file

@ -85,7 +85,7 @@ public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObje
}
try context.save()
} catch let error as NSError {
Logger.data.error("\(error.localizedDescription)")
Logger.data.error("\(error.localizedDescription, privacy: .public)")
}
}
@ -98,7 +98,7 @@ public func deleteUserMessages(user: UserEntity, context: NSManagedObjectContext
}
try context.save()
} catch let error as NSError {
Logger.data.error("\(error.localizedDescription)")
Logger.data.error("\(error.localizedDescription, privacy: .public)")
}
}
@ -122,7 +122,7 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes
do {
try context.executeAndMergeChanges(using: deleteRequest)
} catch {
Logger.data.error("\(error.localizedDescription)")
Logger.data.error("\(error.localizedDescription, privacy: .public)")
}
}
}
@ -130,7 +130,7 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes
func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, packet.from.toHex())
Logger.mesh.info("📟 \(logString)")
Logger.mesh.info("📟 \(logString, privacy: .public)")
guard packet.from > 0 else { return }
@ -313,7 +313,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.position.received %@".localized, String(packet.from))
Logger.mesh.info("📍 \(logString)")
Logger.mesh.info("📍 \(logString, privacy: .public)")
let fetchNodePositionRequest = NodeInfoEntity.fetchRequest()
fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
@ -407,7 +407,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.bluetooth.config %@".localized, String(nodeNum))
Logger.mesh.info("📶 \(logString)")
Logger.mesh.info("📶 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -451,7 +451,7 @@ func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64,
func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.device.config %@".localized, String(nodeNum))
Logger.mesh.info("📟 \(logString)")
Logger.mesh.info("📟 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -506,7 +506,7 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi
func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.display.config %@".localized, nodeNum.toHex())
Logger.data.info("🖥️ \(logString)")
Logger.data.info("🖥️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -551,19 +551,15 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses
Logger.data.info("💾 [DisplayConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("💥 [DisplayConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
}
} else {
Logger.data.error("💥 [DisplayConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex()) unable to save Display Config")
Logger.data.error("💥 [DisplayConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Display Config")
}
} catch {
let nsError = error as NSError
Logger.data.error("💥 [DisplayConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
}
@ -572,7 +568,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses
func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.lora.config %@".localized, nodeNum.toHex())
Logger.data.info("📻 \(logString)")
Logger.data.info("📻 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", nodeNum)
@ -643,7 +639,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPa
func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.network.config %@".localized, String(nodeNum))
Logger.data.info("🌐 \(logString)")
Logger.data.info("🌐 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -658,12 +654,14 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, ses
newNetworkConfig.wifiSsid = config.wifiSsid
newNetworkConfig.wifiPsk = config.wifiPsk
newNetworkConfig.ethEnabled = config.ethEnabled
newNetworkConfig.enabledProtocols = Int32(config.enabledProtocols)
fetchedNode[0].networkConfig = newNetworkConfig
} else {
fetchedNode[0].networkConfig?.ethEnabled = config.ethEnabled
fetchedNode[0].networkConfig?.wifiEnabled = config.wifiEnabled
fetchedNode[0].networkConfig?.wifiSsid = config.wifiSsid
fetchedNode[0].networkConfig?.wifiPsk = config.wifiPsk
fetchedNode[0].networkConfig?.enabledProtocols = Int32(config.enabledProtocols)
}
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
@ -690,7 +688,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("mesh.log.position.config %@".localized, String(nodeNum))
Logger.data.info("🗺️ \(logString)")
Logger.data.info("🗺️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -753,7 +751,7 @@ func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, s
func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.power.config %@".localized, String(nodeNum))
Logger.data.info("🗺️ \(logString)")
Logger.data.info("🗺️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -805,7 +803,7 @@ func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, session
func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.security.config %@".localized, String(nodeNum))
Logger.data.info("🛡️ \(logString)")
Logger.data.info("🛡️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -866,7 +864,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s
func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.ambientlighting.config %@".localized, String(nodeNum))
Logger.data.info("🏮 \(logString)")
Logger.data.info("🏮 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -919,7 +917,7 @@ func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightin
func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.cannedmessage.config %@".localized, String(nodeNum))
Logger.data.info("🥫 \(logString)")
Logger.data.info("🥫 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -978,7 +976,7 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo
func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.detectionsensor.config %@".localized, String(nodeNum))
Logger.data.info("🕵️ \(logString)")
Logger.data.info("🕵️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -1035,7 +1033,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso
func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.externalnotification.config %@".localized, String(nodeNum))
Logger.data.info("📣 \(logString)")
Logger.data.info("📣 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -1104,7 +1102,7 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN
func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.paxcounter.config %@".localized, String(nodeNum))
Logger.data.info("🧑‍🤝‍🧑 \(logString)")
Logger.data.info("🧑‍🤝‍🧑 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -1146,7 +1144,7 @@ func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, n
func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.ringtone.config %@".localized, String(nodeNum))
Logger.data.info("⛰️ \(logString)")
Logger.data.info("⛰️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -1186,7 +1184,7 @@ func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: D
func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.mqtt.config %@".localized, String(nodeNum))
Logger.data.info("🌉 \(logString)")
Logger.data.info("🌉 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -1248,7 +1246,7 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6
func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.rangetest.config %@".localized, String(nodeNum))
Logger.data.info("⛰️ \(logString)")
Logger.data.info("⛰️ \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -1292,7 +1290,7 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod
func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.serial.config %@".localized, String(nodeNum))
Logger.data.info("🤖 \(logString)")
Logger.data.info("🤖 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -1347,7 +1345,7 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum:
func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.storeforward.config %@".localized, String(nodeNum))
Logger.data.info("📬 \(logString)")
Logger.data.info("📬 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
@ -1363,6 +1361,7 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi
newConfig.records = Int32(config.records)
newConfig.historyReturnMax = Int32(config.historyReturnMax)
newConfig.historyReturnWindow = Int32(config.historyReturnWindow)
newConfig.isRouter = config.isServer
fetchedNode[0].storeForwardConfig = newConfig
} else {
fetchedNode[0].storeForwardConfig?.enabled = config.enabled
@ -1395,7 +1394,7 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi
func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.telemetry.config %@".localized, String(nodeNum))
Logger.data.info("📈 \(logString)")
Logger.data.info("📈 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))

View file

@ -87,7 +87,8 @@
"images": [
"t-echo.svg"
],
"requiresDfu": true
"requiresDfu": true,
"hasInkHud": true
},
{
"hwModel": 8,
@ -153,17 +154,21 @@
"images": [
"tbeam-s3-core.svg"
],
"requiresDfu": true
"requiresDfu": true,
"partitionScheme": "8MB"
},
{
"hwModel": 13,
"hwModelSlug": "RAK11200",
"platformioTarget": "rak11200",
"architecture": "esp32",
"activelySupported": false,
"activelySupported": true,
"displayName": "RAK WisBlock 11200",
"tags": [
"RAK"
],
"images": [
"rak11200.svg"
]
},
{
@ -188,7 +193,7 @@
"displayName": "LILYGO T-LoRa V2.1-1.8",
"tags": [
"LilyGo",
"2.4G LoRA"
"2.4GHz"
],
"images": [
"tlora-v2-1-1_8.svg"
@ -338,7 +343,8 @@
"requiresDfu": true,
"images": [
"station-g2.svg"
]
],
"partitionScheme": "16MB"
},
{
"hwModel": 39,
@ -404,7 +410,8 @@
"images": [
"heltec-v3.svg",
"heltec-v3-case.svg"
]
],
"partitionScheme": "8MB"
},
{
"hwModel": 44,
@ -419,7 +426,8 @@
],
"images": [
"heltec-wsl-v3.svg"
]
],
"partitionScheme": "8MB"
},
{
"hwModel": 47,
@ -469,7 +477,8 @@
"images": [
"heltec-wireless-tracker.svg"
],
"requiresDfu": true
"requiresDfu": true,
"partitionScheme": "8MB"
},
{
"hwModel": 58,
@ -482,7 +491,8 @@
"images": [
"heltec-wireless-tracker.svg"
],
"requiresDfu": true
"requiresDfu": true,
"partitionScheme": "8MB"
},
{
"hwModel": 49,
@ -497,7 +507,9 @@
],
"images": [
"heltec-wireless-paper.svg"
]
],
"hasInkHud": true,
"partitionScheme": "8MB"
},
{
"hwModel": 50,
@ -513,7 +525,9 @@
"images": [
"t-deck.svg"
],
"requiresDfu": true
"requiresDfu": true,
"hasMui": true,
"partitionScheme": "16MB"
},
{
"hwModel": 51,
@ -528,7 +542,8 @@
],
"images": [
"t-watch-s3.svg"
]
],
"partitionScheme": "16MB"
},
{
"hwModel": 52,
@ -537,7 +552,9 @@
"architecture": "esp32-s3",
"activelySupported": true,
"supportLevel": 3,
"displayName": "Pi Computer S3"
"displayName": "Pi Computer S3",
"hasMui": true,
"partitionScheme": "8MB"
},
{
"hwModel": 53,
@ -567,7 +584,8 @@
"displayName": "Heltec Wireless Paper V1.0",
"images": [
"heltec-wireless-paper-v1_0.svg"
]
],
"partitionScheme": "8MB"
},
{
"hwModel": 59,
@ -577,36 +595,41 @@
"activelySupported": true,
"supportLevel": 3,
"displayName": "unPhone",
"requiresDfu": true
"requiresDfu": true,
"hasMui": true,
"partitionScheme": "8MB"
},
{
"hwModel": 48,
"hwModelSlug": "HELTEC_WIRELESS_TRACKER",
"platformioTarget": "tracksenger",
"architecture": "esp32-s3",
"activelySupported": true,
"activelySupported": false,
"supportLevel": 3,
"displayName": "TrackSenger (small TFT)",
"requiresDfu": true
"requiresDfu": true,
"partitionScheme": "8MB"
},
{
"hwModel": 48,
"hwModelSlug": "HELTEC_WIRELESS_TRACKER",
"platformioTarget": "tracksenger-lcd",
"architecture": "esp32-s3",
"activelySupported": true,
"activelySupported": false,
"supportLevel": 3,
"displayName": "TrackSenger (big TFT)",
"requiresDfu": true
"requiresDfu": true,
"partitionScheme": "8MB"
},
{
"hwModel": 48,
"hwModelSlug": "HELTEC_WIRELESS_TRACKER",
"platformioTarget": "tracksenger-oled",
"architecture": "esp32-s3",
"activelySupported": true,
"activelySupported": false,
"supportLevel": 3,
"displayName": "TrackSenger (big OLED)"
"displayName": "TrackSenger (big OLED)",
"partitionScheme": "8MB"
},
{
"hwModel": 61,
@ -647,7 +670,8 @@
"images": [
"heltec-vision-master-t190.svg"
],
"requiresDfu": true
"requiresDfu": true,
"partitionScheme": "8MB"
},
{
"hwModel": 67,
@ -663,7 +687,9 @@
"images": [
"heltec-vision-master-e213.svg"
],
"requiresDfu": true
"requiresDfu": true,
"hasInkHud": true,
"partitionScheme": "8MB"
},
{
"hwModel": 68,
@ -679,7 +705,9 @@
"images": [
"heltec-vision-master-e290.svg"
],
"requiresDfu": true
"requiresDfu": true,
"hasInkHud": true,
"partitionScheme": "8MB"
},
{
"hwModel": 69,
@ -711,7 +739,9 @@
],
"images": [
"seeed-sensecap-indicator.svg"
]
],
"hasMui": true,
"partitionScheme": "8MB"
},
{
"hwModel": 71,
@ -730,8 +760,8 @@
"requiresDfu": true
},
{
"hwModel": 72,
"hwModelSlug": "Seeed_XIAO_S3",
"hwModel": 81,
"hwModelSlug": "SEEED_XIAO_S3",
"platformioTarget": "seeed-xiao-s3",
"architecture": "esp32-s3",
"activelySupported": true,
@ -743,14 +773,15 @@
"images": [
"seeed-xiao-s3.svg"
],
"requiresDfu": true
"requiresDfu": true,
"partitionScheme": "8MB"
},
{
"hwModel": 84,
"hwModelSlug": "WISMESH_TAP",
"platformioTarget": "rak_wismeshtap",
"architecture": "nrf52840",
"activelySupported": false,
"activelySupported": true,
"supportLevel": 1,
"displayName": "RAK WisMesh Tap",
"tags": [
@ -760,5 +791,79 @@
"rak-wismeshtap.svg"
],
"requiresDfu": true
},
{
"hwModel": 22,
"hwModelSlug": "WISMESH_HUB",
"platformioTarget": "rak2560",
"architecture": "nrf52840",
"activelySupported": true,
"supportLevel": 1,
"displayName": "RAK WisMesh Repeater",
"tags": [
"RAK"
],
"images": [
"rak2560.svg"
],
"requiresDfu": true
},
{
"hwModel": 63,
"hwModelSlug": "NRF52_PROMICRO_DIY",
"platformioTarget": "nrf52_promicro_diy_tcxo",
"architecture": "nrf52840",
"activelySupported": true,
"supportLevel": 3,
"displayName": "NRF52 Pro-micro DIY",
"tags": [
"DIY"
],
"images": [
"promicro.svg"
],
"requiresDfu": true
},
{
"hwModel": 88,
"hwModelSlug": "XIAO_NRF52_KIT",
"platformioTarget": "seeed_xiao_nrf52840_kit",
"architecture": "nrf52840",
"activelySupported": true,
"supportLevel": 1,
"displayName": "Seeed Xiao NRF52840 Kit",
"tags": [
"Seeed"
],
"requiresDfu": true,
"images": [
"seeed_xiao_nrf52_kit.svg"
]
},
{
"hwModel": 89,
"hwModelSlug": "THINKNODE_M1",
"platformioTarget": "thinknode_m1",
"architecture": "nrf52840",
"activelySupported": false,
"supportLevel": 1,
"displayName": "ThinkNode M1",
"tags": [
"Elecrow"
],
"requiresDfu": true
},
{
"hwModel": 90,
"hwModelSlug": "THINKNODE_M2",
"platformioTarget": "thinknode_m2",
"architecture": "esp32-s3",
"activelySupported": false,
"supportLevel": 1,
"displayName": "ThinkNode M2",
"tags": [
"Elecrow"
],
"requiresDfu": false
}
]

View file

@ -44,7 +44,7 @@ class Router: ObservableObject {
} else if components.path.hasPrefix("/settings") {
routeSettings(components)
} else {
Logger.services.warning("Failed to route url: \(url)")
Logger.services.warning("Failed to route url: \(url, privacy: .public)")
}
}

View file

@ -13,10 +13,10 @@ struct BluetoothConnectionTip: Tip {
return "tip.bluetooth.connect"
}
var title: Text {
Text("tip.bluetooth.connect.title")
Text("Connected Radio")
}
var message: Text? {
Text("tip.bluetooth.connect.message")
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.")
}
var image: Image? {
Image(systemName: "flipphone")

View file

@ -24,6 +24,7 @@ struct Connect: View {
@State var isUnsetRegion = false
@State var invalidFirmwareVersion = false
@State var liveActivityStarted = false
@State var presentingSwitchPreferredPeripheral = false
@State var selectedPeripherialId = ""
init () {
@ -34,7 +35,7 @@ struct Connect: View {
if success {
Logger.services.info("Notifications are all set!")
} else if let error = error {
Logger.services.error("\(error.localizedDescription)")
Logger.services.error("\(error.localizedDescription, privacy: .public)")
}
}
}
@ -61,9 +62,9 @@ struct Connect: View {
.padding(.trailing)
VStack(alignment: .leading) {
if node != nil {
Text(connectedPeripheral.longName).font(.title2)
Text(connectedPeripheral.longName.addingVariationSelectors).font(.title2)
}
Text("BLE Name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)")
Text("BLE Name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "unknown".localized)")
.font(.callout).foregroundColor(Color.gray)
if node != nil {
Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)")
@ -120,7 +121,7 @@ struct Connect: View {
#endif
Text("Num: \(String(node!.num))")
Text("Short Name: \(node?.user?.shortName ?? "?")")
Text("Long Name: \(node?.user?.longName ?? "unknown".localized)")
Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "unknown".localized)")
Text("BLE RSSI: \(connectedPeripheral.rssi)")
Button {
@ -214,22 +215,11 @@ struct Connect: View {
if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == CBPeripheralState.connected {
bleManager.disconnectPeripheral()
}
let container = NSPersistentContainer(name: "Meshtastic")
guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
Logger.data.error("nil File path for back")
return
}
do {
try container.copyPersistentStores(to: url.appendingPathComponent("backup").appendingPathComponent("\(UserDefaults.preferredPeripheralNum)"), overwriting: true)
Logger.data.notice("🗂️ Made a core data backup to backup/\(UserDefaults.preferredPeripheralNum)")
} catch {
Logger.data.error("🗂️ Core data backup copy error: \(error, privacy: .public)")
}
clearCoreDataDatabase(context: context, includeRoutes: false)
presentingSwitchPreferredPeripheral = true
selectedPeripherialId = peripheral.peripheral.identifier.uuidString
} else {
self.bleManager.connectTo(peripheral: peripheral.peripheral)
}
UserDefaults.preferredPeripheralId = selectedPeripherialId
self.bleManager.connectTo(peripheral: peripheral.peripheral)
}) {
Text(peripheral.name).font(.callout)
}
@ -240,6 +230,21 @@ struct Connect: View {
}.padding([.bottom, .top])
}
}
.confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) {
Button("Connect to new radio?", role: .destructive) {
UserDefaults.preferredPeripheralId = selectedPeripherialId
UserDefaults.preferredPeripheralNum = 0
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
bleManager.disconnectPeripheral()
}
clearCoreDataDatabase(context: context, includeRoutes: false)
let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId })
if radio != nil {
bleManager.connectTo(peripheral: radio!.peripheral)
}
}
}
.textCase(nil)
}
} else {
@ -319,7 +324,7 @@ struct Connect: View {
isUnsetRegion = false
}
} catch {
Logger.data.error("💥 Error fetching node info: \(error.localizedDescription)")
Logger.data.error("💥 Error fetching node info: \(error.localizedDescription, privacy: .public)")
}
}
}
@ -333,7 +338,7 @@ struct Connect: View {
let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 4"))
let mostRecent = localStats?.lastObject as? TelemetryEntity
let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown")
let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName?.addingVariationSelectors ?? "unknown")
let future = Date(timeIntervalSinceNow: Double(timerSeconds))
let initialContentState = MeshActivityAttributes.ContentState(uptimeSeconds: UInt32(mostRecent?.uptimeSeconds ?? 0),
@ -356,7 +361,7 @@ struct Connect: View {
pushType: nil)
Logger.services.info("Requested MyActivity live activity. ID: \(myActivity.id)")
} catch {
Logger.services.error("Error requesting live activity: \(error.localizedDescription)")
Logger.services.error("Error requesting live activity: \(error.localizedDescription, privacy: .public)")
}
}

View file

@ -53,6 +53,5 @@ struct ContentView: View {
}
.tag(NavigationState.Tab.settings)
}
.toolbarBackground(.visible, for: .tabBar)
}
}

View file

@ -16,7 +16,7 @@ struct CircleText: View {
Circle()
.fill(color)
.frame(width: circleSize, height: circleSize)
Text(text)
Text(text.addingVariationSelectors)
.frame(width: circleSize * 0.9, height: circleSize * 0.9, alignment: .center)
.foregroundColor(color.isLight() ? .black : .white)
.minimumScaleFactor(0.001)

View file

@ -0,0 +1,47 @@
//
// CompactWidget.swift
// Meshtastic
//
// Created by Jake Bordens on 3/14/25.
//
import SwiftUI
// This file was created for the purpose of previewing
// all of the Compact Widgets in one place.
// In the future, it could be used for a CompactWidget superclass, if desired.
#Preview {
let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
Form {
LazyVGrid(columns: gridItemLayout) {
HumidityCompactWidget(humidity: 27, dewPoint: "32°")
HumidityCompactWidget(humidity: 27, dewPoint: nil)
WeatherConditionsCompactWidget(temperature: "24°F", symbolName: "sun.rain.fill", description: "Raining")
PressureCompactWidget(pressure: "1004.76", unit: "hPA", low: true)
PressureCompactWidget(pressure: "1004.76", unit: "hPA", low: false)
WindCompactWidget(speed: "12 mph", gust: "15 mph", direction: "SW")
WindCompactWidget(speed: "12 mph", gust: nil, direction: "SW")
WindCompactWidget(speed: "12 mph", gust: "15 mph", direction: nil)
WindCompactWidget(speed: "12 mph", gust: nil, direction: nil)
RadiationCompactWidget(radiation: "15", unit: "µR/hr")
DistanceCompactWidget(distance: "123", unit: "mm")
WeightCompactWidget(weight: "123", unit: "kg")
SoilTemperatureCompactWidget(temperature: "23", unit: "°C")
SoilMoistureCompactWidget(moisture: "23", unit: "%")
let rain: Float = 10.1
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(rain), 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)
RainfallCompactWidget(timespan: .rainfall24H, rainfall: formattedRain, unit: unitLabel)
}
}
}

View file

@ -17,6 +17,7 @@ struct CurrentConditionsCompact: View {
.symbolRenderingMode(.multicolor)
}
}
struct CurrentConditionsCompact_Previews: PreviewProvider {
static var previews: some View {

View file

@ -0,0 +1,39 @@
//
// DistanceCompactWidget.swift
// Meshtastic
//
// Created by Jake Bordens on 3/14/25.
//
import SwiftUI
struct DistanceCompactWidget: View {
let distance: String
let unit: String
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "ruler")
.imageScale(.small)
.foregroundColor(.accentColor)
Text("Distance")
.textCase(.uppercase)
.font(.callout)
}
HStack {
Text("\(distance)")
.font(distance.length < 4 ? .system(size: 50) : .system(size: 40) )
Text(unit)
.font(.system(size: 14))
}
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
#Preview {
DistanceCompactWidget(distance: "123", unit: "mm")
}

View file

@ -0,0 +1,47 @@
//
// HumidityCompactWidget.swift
// Meshtastic
//
// Created by Jake Bordens on 3/14/25.
//
import SwiftUI
struct HumidityCompactWidget: View {
let humidity: Int
let dewPoint: String?
var body: some View {
VStack(alignment: .leading) {
HStack(spacing: 5.0) {
Image(systemName: "humidity")
.foregroundColor(.accentColor)
.font(.callout)
Text("Humidity")
.textCase(.uppercase)
.font(.caption)
}
Text("\(humidity)%")
.font(.largeTitle)
.padding(.bottom, 5)
if let dewPoint {
Text("The dew point is \(dewPoint) right now.")
.lineLimit(3)
.allowsTightening(true)
.fixedSize(horizontal: false, vertical: true)
.font(.caption2)
}
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
#Preview {
let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
Form {
LazyVGrid(columns: gridItemLayout) {
HumidityCompactWidget(humidity: 27, dewPoint: "32°")
HumidityCompactWidget(humidity: 27, dewPoint: nil)
}
}
}

View file

@ -0,0 +1,43 @@
//
// PressureCompactWidget.swift
// Meshtastic
//
// Created by Jake Bordens on 3/14/25.
//
import SwiftUI
struct PressureCompactWidget: View {
let pressure: String
let unit: String
let low: Bool
var body: some View {
VStack(alignment: .leading) {
HStack(spacing: 5.0) {
Image(systemName: "gauge")
.foregroundColor(.accentColor)
.font(.callout)
Text("Pressure")
.textCase(.uppercase)
.font(.caption)
}
Text(pressure)
.font(pressure.length < 7 ? .system(size: 35) : .system(size: 30) )
Text(low ? "LOW" : "HIGH")
.padding(.bottom, 10)
Text(unit)
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
#Preview {
let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
Form {
LazyVGrid(columns: gridItemLayout) {
PressureCompactWidget(pressure: "1004.76", unit: "hPA", low: true)
PressureCompactWidget(pressure: "1004.76", unit: "hPA", low: false)
}
}
}

View file

@ -0,0 +1,39 @@
//
// RadiationCompactWidget.swift
// Meshtastic
//
// Created by Jake Bordens on 3/14/25.
//
import SwiftUI
struct RadiationCompactWidget: View {
let radiation: String
let unit: String
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .firstTextBaseline) {
Text(verbatim: "")
.font(.system(size: 30, design: .monospaced))
.foregroundColor(.accentColor)
Text("Radiation")
.textCase(.uppercase)
.font(.callout)
}
HStack {
Text("\(radiation)")
.font(radiation.length < 4 ? .system(size: 50) : .system(size: 34) )
Text(unit)
.font(.system(size: 14))
}
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
#Preview {
RadiationCompactWidget(radiation: "15", unit: "µR/hr")
}

View file

@ -0,0 +1,66 @@
//
// RainfallCompactWidgets.swift
// Meshtastic
//
// Created by Jake Bordens on 3/15/25.
//
import SwiftUI
struct RainfallCompactWidget: View {
enum RainfallTimeSpan: String {
case rainfall1H = "Rainfall 1H"
case rainfall24H = "Rainfall 24H"
}
let timespan: RainfallTimeSpan
let rainfall: String
let unit: String
private var icon: Image {
if timespan == .rainfall1H {
return Image(systemName: "cloud.rain.fill")
}
return Image(systemName: "cloud.heavyrain.fill")
}
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .firstTextBaseline) {
icon.imageScale(.small)
.foregroundColor(.accentColor)
Text(timespan.rawValue)
.textCase(.uppercase)
.font(.callout)
}
HStack {
Text("\(rainfall)")
.font(rainfall.length < 4 ? .system(size: 50) : .system(size: 40) )
Text(unit)
.font(.system(size: 14))
}
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
#Preview {
let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
Form {
LazyVGrid(columns: gridItemLayout) {
let rain: Float = 10.1
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(rain), 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)
RainfallCompactWidget(timespan: .rainfall24H, rainfall: formattedRain, unit: unitLabel)
}
}
}

View file

@ -0,0 +1,72 @@
//
// SoilCompactWidgets.swift
// Meshtastic
//
// Created by Jake Bordens on 3/14/25.
//
import SwiftUI
struct SoilTemperatureCompactWidget: View {
let temperature: String
let unit: String
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .firstTextBaseline) {
Image("soil.temperature")
.imageScale(.small)
.foregroundColor(.accentColor)
Text("Soil Temp")
.textCase(.uppercase)
.font(.callout)
}
HStack {
Text("\(temperature)")
.font(temperature.length < 4 ? .system(size: 50) : .system(size: 40) )
Text(unit)
.font(.system(size: 14))
}
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
struct SoilMoistureCompactWidget: View {
let moisture: String
let unit: String
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .firstTextBaseline) {
Image("soil.moisture")
.imageScale(.small)
.foregroundColor(.accentColor)
Text("Soil Moisture")
.textCase(.uppercase)
.font(.callout)
}
HStack {
Text("\(moisture)")
.font(moisture.length < 4 ? .system(size: 50) : .system(size: 40) )
Text(unit)
.font(.system(size: 14))
}
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
#Preview {
let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
Form {
LazyVGrid(columns: gridItemLayout) {
SoilTemperatureCompactWidget(temperature: "23", unit: "°C")
SoilMoistureCompactWidget(moisture: "23", unit: "%")
}
}
}

View file

@ -0,0 +1,42 @@
//
// WeatherConditionsCompactWidget.swift
// Meshtastic
//
// Created by Jake Bordens on 3/14/25.
//
import SwiftUI
struct WeatherConditionsCompactWidget: View {
let temperature: String
let symbolName: String
let description: String
var body: some View {
VStack(alignment: .leading) {
HStack(spacing: 5.0) {
Image(systemName: symbolName)
.foregroundColor(.accentColor)
.font(.callout)
Text(description)
.lineLimit(2)
.allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
.fixedSize(horizontal: false, vertical: true)
.font(.caption)
}
Text(temperature)
.font(temperature.length < 4 ? .system(size: 72) : .system(size: 54) )
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
#Preview {
let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
Form {
LazyVGrid(columns: gridItemLayout) {
WeatherConditionsCompactWidget(temperature: "24°F", symbolName: "sun.rain.fill", description: "Raining")
}
}
}

View file

@ -0,0 +1,39 @@
//
// WeightCompactWidget.swift
// Meshtastic
//
// Created by Jake Bordens on 3/14/25.
//
import SwiftUI
struct WeightCompactWidget: View {
let weight: String
let unit: String
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "scalemass")
.imageScale(.small)
.foregroundColor(.accentColor)
Text("Weight")
.textCase(.uppercase)
.font(.callout)
}
HStack {
Text("\(weight)")
.font(weight.length < 4 ? .system(size: 50) : .system(size: 40) )
Text(unit)
.font(.system(size: 14))
}
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
#Preview {
WeightCompactWidget(weight: "123", unit: "kg")
}

View file

@ -0,0 +1,45 @@
//
// WindCompactWidget.swift
// Meshtastic
//
// Created by Jake Bordens on 3/14/25.
//
import SwiftUI
struct WindCompactWidget: View {
let speed: String
let gust: String?
let direction: String?
var body: some View {
let hasGust = ((gust ?? "").isEmpty == false)
VStack(alignment: .leading) {
Label { Text("Wind").textCase(.uppercase) } icon: { Image(systemName: "wind").foregroundColor(.accentColor) }
if let direction {
Text("\(direction)")
.font(!hasGust ? .callout : .caption)
.padding(.bottom, 10)
}
Text(speed)
.font(.system(size: 35))
if let gust, !gust.isEmpty {
Text("Gusts \(gust)")
}
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
#Preview {
let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
Form {
LazyVGrid(columns: gridItemLayout) {
WindCompactWidget(speed: "12 mph", gust: "15 mph", direction: "SW")
WindCompactWidget(speed: "12 mph", gust: nil, direction: "SW")
WindCompactWidget(speed: "12 mph", gust: "15 mph", direction: nil)
WindCompactWidget(speed: "12 mph", gust: nil, direction: nil)
}
}
}

View file

@ -28,7 +28,7 @@ struct ConnectedDevice: View {
.imageScale(.large)
.foregroundColor(.green)
.symbolRenderingMode(.hierarchical)
Text(name).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray)
Text(name.addingVariationSelectors).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray)
} else {
Image(systemName: "antenna.radiowaves.left.and.right.slash")
.imageScale(.medium)

View file

@ -55,7 +55,7 @@ struct SecureInput: View {
}) {
Image(systemName: self.isSecure ? "eye.slash" : "eye")
.accentColor(.secondary)
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}

View file

@ -64,7 +64,7 @@ struct LocalWeatherConditions: View {
attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL
}
} catch {
Logger.services.error("Could not gather weather information: \(error.localizedDescription)")
Logger.services.error("Could not gather weather information: \(error.localizedDescription, privacy: .public)")
condition = .clear
symbolName = "cloud.fill"
}
@ -89,113 +89,6 @@ struct LocalWeatherConditions: View {
}
}
struct WeatherConditionsCompactWidget: View {
let temperature: String
let symbolName: String
let description: String
var body: some View {
VStack(alignment: .leading) {
HStack(spacing: 5.0) {
Image(systemName: symbolName)
.foregroundColor(.accentColor)
.font(.callout)
Text(description)
.lineLimit(2)
.allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
.fixedSize(horizontal: false, vertical: true)
.font(.caption)
}
Text(temperature)
.font(temperature.length < 4 ? .system(size: 72) : .system(size: 54) )
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
struct HumidityCompactWidget: View {
let humidity: Int
let dewPoint: String?
var body: some View {
VStack(alignment: .leading) {
HStack(spacing: 5.0) {
Image(systemName: "humidity")
.foregroundColor(.accentColor)
.font(.callout)
Text("Humidity")
.textCase(.uppercase)
.font(.caption)
}
Text("\(humidity)%")
.font(.largeTitle)
.padding(.bottom, 5)
if let dewPoint {
Text("The dew point is \(dewPoint) right now.")
.lineLimit(3)
.allowsTightening(true)
.fixedSize(horizontal: false, vertical: true)
.font(.caption2)
}
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
struct PressureCompactWidget: View {
let pressure: String
let unit: String
let low: Bool
var body: some View {
VStack(alignment: .leading) {
HStack(spacing: 5.0) {
Image(systemName: "gauge")
.foregroundColor(.accentColor)
.font(.callout)
Text("Pressure")
.textCase(.uppercase)
.font(.caption)
}
Text(pressure)
.font(pressure.length < 7 ? .system(size: 35) : .system(size: 30) )
Text(low ? "LOW" : "HIGH")
.padding(.bottom, 10)
Text(unit)
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
struct WindCompactWidget: View {
let speed: String
let gust: String?
let direction: String?
var body: some View {
let hasGust = ((gust ?? "").isEmpty == false)
VStack(alignment: .leading) {
Label { Text("Wind").textCase(.uppercase) } icon: { Image(systemName: "wind").foregroundColor(.accentColor) }
if let direction {
Text("\(direction)")
.font(!hasGust ? .callout : .caption)
.padding(.bottom, 10)
}
Text(speed)
.font(!hasGust ? .system(size: 45) : .system(size: 35))
if let gust, !gust.isEmpty {
Text("Gusts \(gust)")
}
}
.frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140)
.padding()
.background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
}
}
/// Magnus Formula
func calculateDewPoint(temp: Float, relativeHumidity: Float) -> Double {
let a: Float = 17.27

View file

@ -35,7 +35,7 @@ struct NodeWeatherForecastView: View {
)
})
} catch {
Logger.services.error("Could not load weather: \(error.localizedDescription)")
Logger.services.error("Could not load weather: \(error.localizedDescription, privacy: .public)")
}
}
}

View file

@ -1,64 +0,0 @@
//
//// MapButtons.swift
//// Meshtastic
////
//// Copyright © Garth Vander Houwen 4/23/23.
////
//
//import SwiftUI
//
//struct MapButtons: View {
// let buttonWidth: CGFloat = 22
// let width: CGFloat = 45
// @Binding var tracking: UserTrackingModes
// @Binding var isPresentingInfoSheet: Bool
// var body: some View {
// VStack {
// let impactLight = UIImpactFeedbackGenerator(style: .light)
// Button(action: {
// self.isPresentingInfoSheet.toggle()
// }) {
// Image(systemName: isPresentingInfoSheet ? "info.circle.fill" : "info.circle")
// .resizable()
// .frame(width: buttonWidth, height: buttonWidth, alignment: .center)
// .offset(y: -2)
// }
// Divider()
// Button(action: {
// switch self.tracking {
// case .none:
// self.tracking = .follow
// case .follow:
// self.tracking = .followWithHeading
// case .followWithHeading:
// self.tracking = .none
// }
// impactLight.impactOccurred()
// }) {
// Image(systemName: tracking.icon)
// .frame(width: buttonWidth, height: buttonWidth, alignment: .center)
// .offset(y: 3)
// }
// }
// .frame(width: width, height: width*2, alignment: .center)
// .background(Color(UIColor.systemBackground))
// .cornerRadius(8)
// .shadow(radius: 1)
// .offset(x: 3, y: 25)
// }
//}
//
//// MARK: Previews
//// struct MapControl_Previews: PreviewProvider {
//// @State static var tracking: UserTrackingModes = .none
//// @State static var isPresentingInfoSheet = false
//// static var previews: some View {
//// Group {
//// MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet)
//// .environment(\.colorScheme, .light)
//// MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet)
//// .environment(\.colorScheme, .dark)
//// }
//// .previewLayout(.fixed(width: 60, height: 100))
//// }
//// }

View file

@ -1,37 +0,0 @@
//
// MapViewFitExtension.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 1/15/23.
//
import MapKit
extension MKMapView {
func fitAllAnnotations(with padding: UIEdgeInsets = UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100)) {
var zoomRect: MKMapRect = .null
annotations.forEach({
let annotationPoint = MKMapPoint($0.coordinate)
let pointRect = MKMapRect(x: annotationPoint.x, y: annotationPoint.y, width: 0.01, height: 0.01)
zoomRect = zoomRect.union(pointRect)
})
setVisibleMapRect(zoomRect, edgePadding: padding, animated: true)
}
func fit(annotations: [MKAnnotation], andShow show: Bool, with padding: UIEdgeInsets = UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100)) {
var zoomRect: MKMapRect = .null
annotations.forEach({
let aPoint = MKMapPoint($0.coordinate)
let rect = MKMapRect(x: aPoint.x, y: aPoint.y, width: 0.1, height: 0.1)
zoomRect = zoomRect.isNull ? rect : zoomRect.union(rect)
})
if show {
addAnnotations(annotations)
}
setVisibleMapRect(zoomRect, edgePadding: padding, animated: true)
}
}

View file

@ -1,434 +0,0 @@
////
//// MapViewSwitUI.swift
//// Meshtastic
////
//// Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22.
//
//import Foundation
//import SwiftUI
//import MapKit
//import OSLog
//
//struct PolygonInfo: Codable {
// let stroke: String?
// let strokeWidth, strokeOpacity: Int?
// let fill: String?
// let fillOpacity: Double?
// let title, subtitle: String?
//}
//
//func degreesToRadians(_ number: Double) -> Double {
// return number * .pi / 180
//}
//var currentMapLayer: MapLayer?
//
//struct MapViewSwiftUI: UIViewRepresentable {
// var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void
// var onWaypointEdit: (_ waypointId: Int ) -> Void
// let mapView = MKMapView()
// // Parameters
// let selectedMapLayer: MapLayer
// let selectedWeatherLayer: MapOverlayServer = UserDefaults.mapOverlayServer
// let positions: [PositionEntity]
// let waypoints: [WaypointEntity]
// let userTrackingMode: MKUserTrackingMode
// let showNodeHistory: Bool
// let showRouteLines: Bool
// let mapViewType: MKMapType = MKMapType.standard
// // Offline Map Tiles
// @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0
// @State private var loadedLastUpdatedLocalMapFile = 0
// var customMapOverlay: CustomMapOverlay?
// @State private var presentCustomMapOverlayHash: CustomMapOverlay?
// // MARK: Private methods
// private func configureMap(mapView: MKMapView) {
// // Map View Parameters
// mapView.mapType = mapViewType
// mapView.addAnnotations(waypoints)
// // Do the initial map centering
// let latest = positions
// .filter { $0.latest == true }
// .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 }
// let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003)
// let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation
// let region = MKCoordinateRegion(center: center, span: span)
// mapView.addAnnotations(showNodeHistory ? positions : latest)
// mapView.setRegion(region, animated: true)
// // Set user (phone gps) tracking options
// mapView.setUserTrackingMode(userTrackingMode, animated: true)
// if userTrackingMode == MKUserTrackingMode.none {
// if latest.count == 1 {
// mapView.fit(annotations: showNodeHistory ? positions: latest, andShow: false)
// } else {
// mapView.fitAllAnnotations()
// }
// mapView.showsUserLocation = false
// } else {
// mapView.showsUserLocation = true
// }
// // Other MKMapView Settings
// mapView.preferredConfiguration.elevationStyle = .realistic// .flat
// mapView.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll
// mapView.isPitchEnabled = true
// mapView.isRotateEnabled = true
// mapView.isScrollEnabled = true
// mapView.isZoomEnabled = true
// mapView.showsBuildings = true
// mapView.showsScale = true
// mapView.showsTraffic = true
//
// mapView.showsCompass = false
// let compass = MKCompassButton(mapView: mapView)
// compass.translatesAutoresizingMaskIntoConstraints = false
// #if targetEnvironment(macCatalyst)
// // Show the default always visible compass and the mac only controls
// compass.compassVisibility = .visible
// mapView.addSubview(compass)
// mapView.showsZoomControls = true
// mapView.showsPitchControl = true
// compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -115).isActive = true
// compass.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -5).isActive = true
// #else
// compass.compassVisibility = .adaptive
// mapView.addSubview(compass)
// compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true
// compass.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 145).isActive = true
// #endif
// }
// private func setMapBaseLayer(mapView: MKMapView) {
// // Avoid refreshing UI if selectedLayer has not changed
// guard currentMapLayer != selectedMapLayer else { return }
// currentMapLayer = selectedMapLayer
// for overlay in mapView.overlays where overlay is MKTileOverlay {
// mapView.removeOverlay(overlay)
// }
// switch selectedMapLayer {
// case .offline:
// mapView.mapType = .standard
// let overlay = TileOverlay()
// overlay.canReplaceMapContent = false
// overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex
// overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex
// mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads)
// case .satellite:
// mapView.mapType = .satellite
// case .hybrid:
// mapView.mapType = .hybrid
// default:
// mapView.mapType = .standard
// }
// }
// private func setMapOverlays(mapView: MKMapView) {
// // Weather radar
// if UserDefaults.enableOverlayServer {
// let locale = Locale.current
// if locale.region?.identifier ?? "no locale" == "US" {
// let overlay = MKTileOverlay(urlTemplate: selectedWeatherLayer.tileUrl)
// overlay.canReplaceMapContent = false
// overlay.minimumZ = selectedWeatherLayer.zoomRange.startIndex
// overlay.maximumZ = selectedWeatherLayer.zoomRange.endIndex
// mapView.addOverlay(overlay, level: .aboveLabels)
// }
// }
// }
//
// func makeUIView(context: Context) -> MKMapView {
// currentMapLayer = nil
// mapView.delegate = context.coordinator
// self.configureMap(mapView: mapView)
// return mapView
// }
// func updateUIView(_ mapView: MKMapView, context: Context) {
// // Set selected map base layer
// setMapBaseLayer(mapView: mapView)
// // Set map tile server and weather overlay layers
// setMapOverlays(mapView: mapView)
// let latest = positions
// .filter { $0.latest == true }
// .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 }
// // Node Route Lines
// if showRouteLines {
// // Remove all existing PolyLine Overlays
// for overlay in mapView.overlays where overlay is MKPolyline {
// mapView.removeOverlay(overlay)
// }
// var lineIndex = 0
// for position in latest {
// let nodePositions = positions.filter { $0.nodeCoordinate != nil && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 }
// let lineCoords = nodePositions.compactMap({(position) -> CLLocationCoordinate2D in
// return position.nodeCoordinate ?? LocationHelper.DefaultLocation
// })
// let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count)
// polyline.title = "\(String(position.nodePosition?.num ?? 0))"
// mapView.addOverlay(polyline, level: .aboveLabels)
// lineIndex += 1
// // There are 18 colors for lines, start over if we are at index 17
// if lineIndex > 17 {
// lineIndex = 0
// }
// }
// } else {
// // Remove all existing PolyLine Overlays
// for overlay in mapView.overlays where overlay is MKPolyline {
// mapView.removeOverlay(overlay)
// }
// }
// let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count)
// if annotationCount != mapView.annotations.count {
// Logger.services.info("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)")
// mapView.removeAnnotations(mapView.annotations)
// mapView.addAnnotations(waypoints)
// }
// mapView.addAnnotations(showNodeHistory ? positions : latest)
// if userTrackingMode == MKUserTrackingMode.none {
// mapView.showsUserLocation = false
// if UserDefaults.enableMapRecentering {
// if latest.count == 1 {
// mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: true)
// } else {
// mapView.fitAllAnnotations()
// }
// }
// } else {
// mapView.showsUserLocation = true
// }
// mapView.setUserTrackingMode(userTrackingMode, animated: true)
// }
// func makeCoordinator() -> MapCoordinator {
// return Coordinator(self)
// }
// final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate {
// var parent: MapViewSwiftUI
// var longPressRecognizer = UILongPressGestureRecognizer()
// init(_ parent: MapViewSwiftUI) {
// self.parent = parent
// super.init()
// self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler))
// self.longPressRecognizer.minimumPressDuration = 0.5
// self.longPressRecognizer.cancelsTouchesInView = true
// self.longPressRecognizer.delegate = self
// self.parent.mapView.addGestureRecognizer(longPressRecognizer)
// }
// func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// switch annotation {
// case let positionAnnotation as PositionEntity:
// let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0)
// let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID )
// annotationView.tag = -1
// annotationView.canShowCallout = true
// if positionAnnotation.latest {
// annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).darker()
// annotationView.displayPriority = .required
// annotationView.titleVisibility = .visible
// } else {
// annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).lighter()
// annotationView.displayPriority = .defaultHigh
// annotationView.titleVisibility = .adaptive
// }
// annotationView.tag = -1
// annotationView.canShowCallout = true
// annotationView.titleVisibility = .adaptive
// let leftIcon = UIImageView(image: annotationView.glyphText?.image())
// leftIcon.backgroundColor = UIColor(.indigo)
// annotationView.leftCalloutAccessoryView = leftIcon
// let subtitle = UILabel()
// subtitle.text = "Long Name: \(positionAnnotation.nodePosition?.user?.longName ?? "Unknown") \n"
// subtitle.text? += "Latitude: \(String(format: "%.5f", positionAnnotation.coordinate.latitude)) \n"
// subtitle.text! += "Longitude: \(String(format: "%.5f", positionAnnotation.coordinate.longitude)) \n"
// let distanceFormatter = MKDistanceFormatter()
// subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n"
// if positionAnnotation.nodePosition?.metadata != nil {
// if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client ||
// DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute ||
// DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient {
// annotationView.glyphImage = UIImage(systemName: "flipphone")
// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.repeater {
// annotationView.glyphImage = UIImage(systemName: "repeat")
// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.router {
// annotationView.glyphImage = UIImage(systemName: "wifi.router.fill")
// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.tracker {
// annotationView.glyphImage = UIImage(systemName: "location.viewfinder")
// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor {
// annotationView.glyphImage = UIImage(systemName: "sensor")
// }
// let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3))
// if pf.contains(.Satsinview) {
// subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n"
// }
// if pf.contains(.SeqNo) {
// subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n"
// }
// if pf.contains(.Heading) {
// if parent.userTrackingMode != MKUserTrackingMode.followWithHeading {
// annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading))))
// subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n"
// } else {
// annotationView.glyphImage = UIImage(systemName: "flipphone")
// }
// }
// if pf.contains(.Speed) {
// let formatter = MeasurementFormatter()
// formatter.locale = Locale.current
// if positionAnnotation.speed <= 1 {
// annotationView.glyphImage = UIImage(systemName: "hexagon")
// }
// subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n"
// }
// } else {
// // node metadata is nil
// annotationView.glyphImage = UIImage(systemName: "flipphone")
// }
// if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 {
// let metersAway = positionAnnotation.coordinate.distance(from: LocationHelper.currentLocation)
// subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n"
// }
// subtitle.text! += positionAnnotation.time?.formatted() ?? "Unknown \n"
// subtitle.numberOfLines = 0
// annotationView.detailCalloutAccessoryView = subtitle
// let detailsIcon = UIButton(type: .detailDisclosure)
// detailsIcon.setImage(UIImage(systemName: "trash"), for: .normal)
// annotationView.rightCalloutAccessoryView = detailsIcon
// return annotationView
// case let waypointAnnotation as WaypointEntity:
// let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: String(waypointAnnotation.id))
// annotationView.tag = Int(waypointAnnotation.id)
// annotationView.isEnabled = true
// annotationView.canShowCallout = true
// if waypointAnnotation.icon == 0 {
// annotationView.glyphText = "📍"
// } else {
// annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "📍")
// }
// annotationView.markerTintColor = UIColor(.accentColor)
// annotationView.displayPriority = .required
// annotationView.titleVisibility = .adaptive
// let leftIcon = UIImageView(image: annotationView.glyphText?.image())
// leftIcon.backgroundColor = UIColor(.accentColor)
// annotationView.leftCalloutAccessoryView = leftIcon
// let subtitle = UILabel()
// if waypointAnnotation.longDescription?.count ?? 0 > 0 {
// subtitle.text = (waypointAnnotation.longDescription ?? "") + "\n"
// } else {
// subtitle.text = ""
// }
// if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 {
// let metersAway = waypointAnnotation.coordinate.distance(from: LocationHelper.currentLocation)
// let distanceFormatter = MKDistanceFormatter()
// subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n"
// }
// if waypointAnnotation.created != nil {
// subtitle.text! += "Created: \(waypointAnnotation.created?.formatted() ?? "Unknown") \n"
// }
// if waypointAnnotation.lastUpdated != nil {
// subtitle.text! += "Updated: \(waypointAnnotation.lastUpdated?.formatted() ?? "Unknown") \n"
// }
// if waypointAnnotation.expire != nil {
// subtitle.text! += "Expires: \(waypointAnnotation.expire?.formatted() ?? "Unknown") \n"
// }
// subtitle.numberOfLines = 0
// annotationView.detailCalloutAccessoryView = subtitle
// let editIcon = UIButton(type: .detailDisclosure)
// editIcon.setImage(UIImage(systemName: "square.and.pencil"), for: .normal)
// annotationView.rightCalloutAccessoryView = editIcon
// return annotationView
// default: return nil
// }
// }
// func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
// switch view.annotation {
// case _ as WaypointEntity:
// // Only Allow Edit for waypoint annotations with a id
// if view.tag > 0 {
// parent.onWaypointEdit(view.tag)
// }
// default: break
// }
// }
// @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) {
// if gesture.state != UIGestureRecognizer.State.ended {
// return
// } else if gesture.state != UIGestureRecognizer.State.began {
// // Screen Position - CGPoint
// let location = longPressRecognizer.location(in: self.parent.mapView)
// // Map Coordinate - CLLocationCoordinate2D
// let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView)
// let annotation = MKPointAnnotation()
// annotation.title = "Dropped Pin"
// annotation.coordinate = coordinate
// parent.mapView.addAnnotation(annotation)
// UINotificationFeedbackGenerator().notificationOccurred(.success)
// parent.onLongPress(coordinate)
// }
// }
// public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
// if let tileOverlay = overlay as? MKTileOverlay {
// return MKTileOverlayRenderer(tileOverlay: tileOverlay)
// } else {
// if let routePolyline = overlay as? MKPolyline {
// let titleString = routePolyline.title ?? "0"
// let renderer = MKPolylineRenderer(polyline: routePolyline)
// renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0).lighter()
// renderer.lineWidth = 8
// return renderer
// }
// if let polygon = overlay as? MKPolygon {
// let renderer = MKPolygonRenderer(polygon: polygon)
// renderer.fillColor = UIColor.purple.withAlphaComponent(0.2)
// renderer.strokeColor = .purple.withAlphaComponent(0.7)
// return renderer
// }
// return MKOverlayRenderer(overlay: overlay)
// }
// }
// }
// /// is supposed to be located in the folder with the map name
// public struct DefaultTile: Hashable {
// let tileName: String
// let tileType: String
// public init(tileName: String, tileType: String) {
// self.tileName = tileName
// self.tileType = tileType
// }
// }
// public struct CustomMapOverlay: Equatable, Hashable {
// let mapName: String
// let tileType: String
// var canReplaceMapContent: Bool
// var minimumZoomLevel: Int?
// var maximumZoomLevel: Int?
// let defaultTile: DefaultTile?
// public init(
// mapName: String,
// tileType: String,
// canReplaceMapContent: Bool = true, // false for transparent tiles
// minimumZoomLevel: Int? = nil,
// maximumZoomLevel: Int? = nil,
// defaultTile: DefaultTile? = nil
// ) {
// self.mapName = mapName
// self.tileType = tileType
// self.canReplaceMapContent = canReplaceMapContent
// self.minimumZoomLevel = minimumZoomLevel
// self.maximumZoomLevel = maximumZoomLevel
// self.defaultTile = defaultTile
// }
// public init?(
// mapName: String?,
// tileType: String,
// canReplaceMapContent: Bool = true, // false for transparent tiles
// minimumZoomLevel: Int? = nil,
// maximumZoomLevel: Int? = nil,
// defaultTile: DefaultTile? = nil
// ) {
// if mapName == nil || mapName! == "" {
// return nil
// }
// self.mapName = mapName!
// self.tileType = tileType
// self.canReplaceMapContent = canReplaceMapContent
// self.minimumZoomLevel = minimumZoomLevel
// self.maximumZoomLevel = maximumZoomLevel
// self.defaultTile = defaultTile
// }
// }
//}

View file

@ -1,14 +0,0 @@
import SwiftUI
struct TileDownloadStatus: View {
@ObservedObject var tileManager = OfflineTileManager.shared
var body: some View {
if tileManager.status == .downloading {
Image(systemName: "arrow.down.circle.fill")
.foregroundColor(.gray)
} else {
EmptyView()
}
}
}

View file

@ -1,164 +0,0 @@
////
//// NodeMapControl.swift
//// Meshtastic
////
//// Created by Garth Vander Houwen on 9/9/23.
////
//import SwiftUI
//import CoreLocation
//import MapKit
//import WeatherKit
//import OSLog
//
//struct NodeMapMapkit: View {
//
// @Environment(\.managedObjectContext) var context
// @EnvironmentObject var bleManager: BLEManager
// /// Weather
// /// The current weather condition for the city.
// @State private var condition: WeatherCondition?
// @State private var temperature: Measurement<UnitTemperature>?
// @State private var humidity: Int?
// @State private var symbolName: String = "cloud.fill"
// @State private var attributionLink: URL?
// @State private var attributionLogo: URL?
//
// @Environment(\.colorScheme) var colorScheme: ColorScheme
// @AppStorage("meshMapType") private var meshMapType = 0
// @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
// @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false
// @State private var selectedMapLayer: MapLayer = .standard
// @State var waypointCoordinate: WaypointCoordinate?
// @State var editingWaypoint: Int = 0
// @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
// mapName: "offlinemap",
// tileType: "png",
// canReplaceMapContent: true
// )
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
// predicate: NSPredicate(
// format: "expire == nil || expire >= %@", Date() as NSDate
// ), animation: .none)
// private var waypoints: FetchedResults<WaypointEntity>
// @ObservedObject var node: NodeInfoEntity
//
// var body: some View {
//
// NavigationStack {
// GeometryReader { bounds in
// VStack {
// if node.hasPositions {
// ZStack {
// let positionArray = node.positions?.array as? [PositionEntity] ?? []
// let lastTenThousand = Array(positionArray.prefix(10000))
// // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) }
// ZStack {
// MapViewSwiftUI(onLongPress: { coord in
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
// }, onWaypointEdit: { wpId in
// if wpId > 0 {
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
// }
// },
// selectedMapLayer: selectedMapLayer,
// positions: lastTenThousand,
// waypoints: Array(waypoints),
// userTrackingMode: MKUserTrackingMode.none,
// showNodeHistory: meshMapShowNodeHistory,
// showRouteLines: meshMapShowRouteLines,
// customMapOverlay: self.customMapOverlay
// )
// VStack(alignment: .leading) {
// Spacer()
// HStack(alignment: .bottom, spacing: 1) {
// Picker("Map Type", selection: $selectedMapLayer) {
// ForEach(MapLayer.allCases, id: \.self) { layer in
// if layer == MapLayer.offline && UserDefaults.enableOfflineMaps {
// Text(layer.localized)
// } else if layer != MapLayer.offline {
// Text(layer.localized)
// }
// }
// }
// .onChange(of: (selectedMapLayer)) { newMapLayer in
// UserDefaults.mapLayer = newMapLayer
// }
// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
// .pickerStyle(.menu)
// .padding(5)
// VStack {
// VStack {
// Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
// .font(.caption)
//
// Label("\(humidity ?? 0)%", systemImage: "humidity")
// .font(.caption2)
//
// AsyncImage(url: attributionLogo) { image in
// image
// .resizable()
// .scaledToFit()
// } placeholder: {
// ProgressView()
// .controlSize(.mini)
// }
// .frame(height: 10)
//
// Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!)
// .font(.caption2)
// }
// .padding(5)
//
// }
// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
// .padding(5)
// .task {
// do {
// if node.hasPositions {
// let mostRecent = node.positions?.lastObject as? PositionEntity
// let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude))
// condition = weather.currentWeather.condition
// temperature = weather.currentWeather.temperature
// humidity = Int(weather.currentWeather.humidity * 100)
// symbolName = weather.currentWeather.symbolName
// let attribution = try await WeatherService.shared.attribution
// attributionLink = attribution.legalPageURL
// attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL
// }
// } catch {
// Logger.services.error("Could not gather weather information: \(error.localizedDescription)")
// condition = .clear
// symbolName = "cloud.fill"
// }
// }
// }
// }
// }
// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
// .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
// }
// } else {
// HStack {
// }
// .padding([.top], 20)
// }
// }
// .edgesIgnoringSafeArea([.leading, .trailing])
// .sheet(item: $waypointCoordinate, content: { wpc in
// WaypointFormMapKit(coordinate: wpc)
// .presentationDetents([.medium, .large])
// .presentationDragIndicator(.automatic)
// })
// .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
// .navigationBarItems(trailing:
// ZStack {
// ConnectedDevice(
// bluetoothOn: bleManager.isSwitchedOn,
// deviceConnected: bleManager.connectedPeripheral != nil,
// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
// })
// }
// .padding(.bottom, 2)
// }
// }
//}

View file

@ -1,266 +0,0 @@
////
//// WaypointFormView.swift
//// Meshtastic
////
//// Copyright Garth Vander Houwen 1/10/23.
////
//
//import CoreLocation
//import MeshtasticProtobufs
//import OSLog
//import SwiftUI
//
//struct WaypointFormMapKit: View {
//
// @EnvironmentObject var bleManager: BLEManager
// @Environment(\.dismiss) private var dismiss
// @State var coordinate: WaypointCoordinate
// @FocusState private var iconIsFocused: Bool
// @State private var name: String = ""
// @State private var description: String = ""
// @State private var icon: String = "📍"
// @State private var latitude: Double = 0
// @State private var longitude: Double = 0
// @State private var expires: Bool = false
// @State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours
// @State private var locked: Bool = false
// @State private var lockedTo: Int64 = 0
//
// var body: some View {
//
// Form {
// let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.coordinate?.latitude ?? 0, longitude: coordinate.coordinate?.longitude ?? 0))
// Section(header: Text((coordinate.waypointId > 0) ? "Editing Waypoint" : "Create Waypoint")) {
// HStack {
// Text("Location: \(String(format: "%.5f", latitude) + "," + String(format: "%.5f", longitude))")
// .textSelection(.enabled)
// .foregroundColor(Color.gray)
// .font(.caption2)
// if coordinate.coordinate?.latitude ?? 0 != 0 && coordinate.coordinate?.longitude ?? 0 != 0 {
// DistanceText(meters: distance)
// .foregroundColor(Color.gray)
// .font(.caption2)
// }
// }
// HStack {
// Text("Name")
// Spacer()
// TextField(
// "Name",
// text: $name,
// axis: .vertical
// )
// .foregroundColor(Color.gray)
// .onChange(of: name) {
// var totalBytes = name.utf8.count
// // Only mess with the value if it is too big
// while totalBytes > 30 {
// name = String(name.dropLast())
// totalBytes = name.utf8.count
// }
// if totalBytes > 30 {
// name = String(name.dropLast())
// }
// }
// }
// HStack {
// Text("Description")
// Spacer()
// TextField(
// "Description",
// text: $description,
// axis: .vertical
// )
// .foregroundColor(Color.gray)
// .onChange(of: description) {
// var totalBytes = description.utf8.count
// // Only mess with the value if it is too big
// while totalBytes > 100 {
// description = String(description.dropLast())
// totalBytes = description.utf8.count
// }
// }
// }
// HStack {
// Text("Icon")
// Spacer()
// EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
// .font(.title)
// .focused($iconIsFocused)
// .onChange(of: icon) { _, value in
//
// // If you have anything other than emojis in your string make it empty
// if !value.onlyEmojis() {
// icon = ""
// }
// // If a second emoji is entered delete the first one
// if value.count >= 1 {
//
// if value.count > 1 {
// let index = value.index(value.startIndex, offsetBy: 1)
// icon = String(value[index])
// }
// iconIsFocused = false
// }
// }
//
// }
// Toggle(isOn: $expires) {
// Label("Expires", systemImage: "clock.badge.xmark")
// }
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
// if expires {
// DatePicker("Expire", selection: $expire, in: Date.now...)
// .datePickerStyle(.compact)
// .font(.callout)
// }
// Toggle(isOn: $locked) {
// Label("Locked", systemImage: "lock")
// }
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
// }
// }
// HStack {
// Button {
//
// var newWaypoint = Waypoint()
// // Loading a waypoint from edit
// if coordinate.waypointId > 0 {
// newWaypoint.id = UInt32(coordinate.waypointId)
// let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context)
// newWaypoint.latitudeI = waypoint.latitudeI
// newWaypoint.longitudeI = waypoint.longitudeI
// } else {
// // New waypoint
// newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
// newWaypoint.latitudeI = Int32(Double(coordinate.coordinate?.latitude ?? 0) * 1e7)
// newWaypoint.longitudeI = Int32(Double(coordinate.coordinate?.longitude ?? 0) * 1e7)
// }
// newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
// newWaypoint.description_p = description
// // Unicode scalar value for the icon emoji string
// let unicodeScalers = icon.unicodeScalars
// // First element as an UInt32
// let unicode = unicodeScalers[unicodeScalers.startIndex].value
// newWaypoint.icon = unicode
// if locked {
// if lockedTo == 0 {
// newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num)
// } else {
// newWaypoint.lockedTo = UInt32(lockedTo)
// }
// }
// if expires {
// newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
// } else {
// newWaypoint.expire = 0
// }
// if bleManager.sendWaypoint(waypoint: newWaypoint) {
// dismiss()
// } else {
// dismiss()
// Logger.mesh.error("Send waypoint failed")
// }
// } label: {
// Label("Send", systemImage: "arrow.up")
// }
// .buttonStyle(.bordered)
// .buttonBorderShape(.capsule)
// .controlSize(.regular)
// .disabled(bleManager.connectedPeripheral == nil)
// .padding(.bottom)
//
// Button(role: .cancel) {
// dismiss()
// } label: {
// Label("cancel", systemImage: "x.circle")
// }
// .buttonStyle(.bordered)
// .buttonBorderShape(.capsule)
// .controlSize(.regular)
// .padding(.bottom)
//
// if coordinate.waypointId > 0 {
//
// Menu {
// Button("For me", action: {
// let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context)
// bleManager.context.delete(waypoint)
// do {
// try bleManager.context.save()
// } catch {
// bleManager.context.rollback()
// }
// dismiss() })
// Button("For everyone", action: {
// var newWaypoint = Waypoint()
//
// if coordinate.waypointId > 0 {
// newWaypoint.id = UInt32(coordinate.waypointId)
// }
// newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
// newWaypoint.description_p = description
// newWaypoint.latitudeI = Int32(coordinate.coordinate?.latitude ?? 0 * 1e7)
// newWaypoint.longitudeI = Int32(coordinate.coordinate?.longitude ?? 0 * 1e7)
// // Unicode scalar value for the icon emoji string
// let unicodeScalers = icon.unicodeScalars
// // First element as an UInt32
// let unicode = unicodeScalers[unicodeScalers.startIndex].value
// newWaypoint.icon = unicode
// if locked {
// if lockedTo == 0 {
// newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num)
// } else {
// newWaypoint.lockedTo = UInt32(lockedTo)
// }
// }
// newWaypoint.expire = 1
// if bleManager.sendWaypoint(waypoint: newWaypoint) {
// dismiss()
// } else {
// dismiss()
// Logger.mesh.error("Send waypoint failed")
// }
// })
// }
// label: {
// Label("delete", systemImage: "trash")
// .foregroundColor(.red)
// }
// .buttonStyle(.bordered)
// .buttonBorderShape(.capsule)
// .controlSize(.regular)
// .padding(.bottom)
// }
// }
// .onAppear {
// if coordinate.waypointId > 0 {
// let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context)
// name = waypoint.name ?? "Dropped Pin"
// description = waypoint.longDescription ?? ""
// icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍")
// latitude = Double(waypoint.latitudeI) / 1e7
// longitude = Double(waypoint.longitudeI) / 1e7
// if waypoint.expire != nil {
// expires = true
// expire = waypoint.expire ?? Date()
// } else {
// expires = false
// }
// if waypoint.locked > 0 {
// locked = true
// lockedTo = waypoint.locked
// }
// } else {
// name = ""
// description = ""
// locked = false
// expires = false
// expire = Date.now.addingTimeInterval(60 * 480)
// icon = "📍"
// latitude = coordinate.coordinate?.latitude ?? 0
// longitude = coordinate.coordinate?.longitude ?? 0
// }
// }
// }
//}

View file

@ -115,12 +115,15 @@ struct ChannelMessageList: View {
if !message.read {
message.read = true
do {
for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) {
unreadMessage.read = true
}
try context.save()
Logger.data.info("📖 [App] Read message \(message.messageId) ")
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
appState.unreadChannelMessages = myInfo.unreadMessages
context.refresh(myInfo, mergeChanges: true)
} catch {
Logger.data.error("Failed to read message \(message.messageId): \(error.localizedDescription)")
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
}

View file

@ -37,14 +37,28 @@ struct MessageText: View {
HStack {
Spacer()
Image(systemName: "lock.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .green)
.font(.system(size: 20))
.offset(x: 8, y: 8)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .green)
.font(.system(size: 20))
.offset(x: 8, y: 8)
}
}
}
let isStoreAndForward = message.portNum == Int32(PortNum.storeForwardApp.rawValue)
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
if isStoreAndForward {
VStack(alignment: .trailing) {
Spacer()
HStack {
Spacer()
Image(systemName: "envelope.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .gray)
.font(.system(size: 20))
.offset(x: 8, y: 8)
}
}
}
if tapBackDestination.overlaySensorMessage {
VStack {
isDetectionSensorMessage ? Image(systemName: "sensor.fill")
@ -59,6 +73,7 @@ struct MessageText: View {
} else {
EmptyView()
}
}
.contextMenu {
MessageContextMenuItems(
@ -79,7 +94,7 @@ struct MessageText: View {
do {
try context.save()
} catch {
Logger.data.error("Failed to delete message \(message.messageId): \(error.localizedDescription)")
Logger.data.error("Failed to delete message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
Button("Cancel", role: .cancel) {}

View file

@ -37,7 +37,7 @@ struct RetryButton: View {
do {
try context.save()
} catch {
Logger.data.error("Failed to delete message \(messageID): \(error.localizedDescription)")
Logger.data.error("Failed to delete message \(messageID, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
if !bleManager.sendMessage(
message: payload,
@ -47,7 +47,7 @@ struct RetryButton: View {
replyID: replyID
) {
// Best effort, unlikely since we already checked BLE state
Logger.services.warning("Failed to resend message \(messageID)")
Logger.services.warning("Failed to resend message \(messageID, privacy: .public)")
} else {
switch destination {
case .user:

View file

@ -31,10 +31,10 @@ struct TapbackResponses: View {
tapback.read = true
do {
try context.save()
Logger.data.info("📖 Read tapback \(tapback.messageId) ")
Logger.data.info("📖 Read tapback \(tapback.messageId, privacy: .public) ")
onRead()
} catch {
Logger.data.error("Failed to read tapback \(tapback.messageId): \(error.localizedDescription)")
Logger.data.error("Failed to read tapback \(tapback.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
}

View file

@ -103,11 +103,11 @@ struct UserMessageList: View {
message.read = true
do {
try context.save()
Logger.data.info("📖 [App] Read message \(message.messageId) ")
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
appState.unreadDirectMessages = user.unreadMessages
} catch {
Logger.data.error("Failed to read message \(message.messageId): \(error.localizedDescription)")
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
}
}

View file

@ -135,7 +135,7 @@ struct DetectionSensorLog: View {
self.isExporting = false
Logger.services.info("Detection Sensor metrics log download succeeded.")
case .failure(let error):
Logger.services.error("Detection Sensor log download failed: \(error.localizedDescription).")
Logger.services.error("Detection Sensor log download failed: \(error.localizedDescription, privacy: .public).")
}
}
)

View file

@ -211,7 +211,7 @@ struct DeviceMetricsLog: View {
) {
Button("device.metrics.delete", role: .destructive) {
if clearTelemetry(destNum: node.num, metricsType: 0, context: context) {
Logger.data.notice("Cleared Device Metrics for \(node.num)")
Logger.data.notice("Cleared Device Metrics for \(node.num, privacy: .public)")
} else {
Logger.data.error("Clear Device Metrics Log Failed")
}
@ -257,7 +257,7 @@ struct DeviceMetricsLog: View {
self.isExporting = false
Logger.services.info("Device metrics log download succeeded.")
case .failure(let error):
Logger.services.error("Device metrics log download failed: \(error.localizedDescription)")
Logger.services.error("Device metrics log download failed: \(error.localizedDescription, privacy: .public)")
}
}
)

View file

@ -177,7 +177,7 @@ struct EnvironmentMetricsLog: View {
self.isExporting = false
Logger.services.info("Environment metrics log download succeeded.")
case .failure(let error):
Logger.services.error("Environment metrics log download failed: \(error.localizedDescription)")
Logger.services.error("Environment metrics log download failed: \(error.localizedDescription, privacy: .public)")
}
}
)

View file

@ -41,7 +41,7 @@ struct DeleteNodeButton: View {
id: node.num,
context: context
) else {
Logger.data.error("Unable to find node info to delete node \(node.num)")
Logger.data.error("Unable to find node info to delete node \(node.num, privacy: .public)")
return
}
let success = bleManager.removeNode(
@ -49,7 +49,7 @@ struct DeleteNodeButton: View {
connectedNodeNum: connectedNode.num
)
if !success {
Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "unknown".localized)")
Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "unknown".localized, privacy: .public)")
} else {
dismiss()
}

View file

@ -19,20 +19,20 @@ struct NavigateToButton: View {
Logger.services.error("NavigateToAction: Selected node does not exist")
return
}
Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum)")
Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum, privacy: .public)")
let fetchRequest: NSFetchRequest<NodeInfoEntity> = NSFetchRequest(entityName: "NodeInfoEntity")
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum))
do {
let fetchedNodes = try PersistenceController.shared.container.viewContext.fetch(fetchRequest)
guard let nodeInfo = fetchedNodes.first else {
Logger.services.error("NavigateToAction: Node with userNum \(userNum) not found in Core Data")
Logger.services.error("NavigateToAction: Node with userNum \(userNum, privacy: .public) not found in Core Data")
return
}
if let latitude = nodeInfo.latestPosition?.latitude,
let longitude = nodeInfo.latestPosition?.longitude {
if let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)") {
@ -41,10 +41,10 @@ struct NavigateToButton: View {
Logger.services.error("Failed to create URL for navigation")
}
} else {
Logger.services.warning("NavigateToAction: Node \(userNum) has invalid or missing coordinates")
Logger.services.warning("NavigateToAction: Node \(userNum, privacy: .public) has invalid or missing coordinates")
}
} catch {
Logger.services.error("NavigateToAction: Failed to fetch node with userNum \(userNum): \(error.localizedDescription)")
Logger.services.error("NavigateToAction: Failed to fetch node with userNum \(userNum, privacy: .public): \(error.localizedDescription, privacy: .public)")
}
} label: {
Label {

View file

@ -1,233 +0,0 @@
//
// EnvironmentDefaultSeries.swift
// Meshtastic
//
// Created by Jake Bordens on 12/11/24.
//
import Charts
import Foundation
import SwiftUI
// This is the default configuration used by the EnvironmentMetricsLog view for the chart
extension MetricsSeriesList {
static var environmentDefaultChartSeries: MetricsSeriesList {
MetricsSeriesList([
// Temperature Series Configuration
MetricsChartSeries(
id: "temperature",
keyPath: \.temperature,
name: "Temperature",
abbreviatedName: "Temp",
conversion: { t in t.map { Float($0.localeTemperature()) } },
foregroundStyle: { chartRange in
let locale = NSLocale.current as NSLocale
let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey"))
let format: UnitTemperature = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? .fahrenheit : .celsius
let lowerBound = chartRange.map { Double($0.lowerBound) } ?? 0.0
let upperBound = chartRange.map { Double($0.upperBound) } ?? 100.0
let stops: [Gradient.Stop] = generateStops(minTemp: lowerBound, maxTemp: upperBound, tempUnit: format, opacity: 1.0)
return LinearGradient(stops: stops, startPoint: .bottom, endPoint: .top)
},
chartBody: { series, chartRange, time, temperature in
if let temperature {
AreaMark(
x: .value("Time", time),
yStart: .value(series.abbreviatedName, chartRange?.lowerBound.doubleValue ?? 0.0),
yEnd: .value(
series.abbreviatedName, temperature.localeTemperature())
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.alignsMarkStylesWithPlotArea()
.accessibilityHidden(true)
.opacity(0.6)
LineMark(
x: .value("Time", time),
y: .value(
series.abbreviatedName, temperature.localeTemperature())
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Relative Humidity Series Configuration
MetricsChartSeries(
id: "relativeHumidity",
keyPath: \.relativeHumidity,
name: "Relative Humidity",
abbreviatedName: "Hum",
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.purple.darker(componentDelta: 0.2)), .purple],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, humidity in
if let humidity {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, humidity)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Barometric Pressure Series Configuration
MetricsChartSeries(
id: "barometricPressure",
keyPath: \.barometricPressure,
name: "Barometric Pressure",
abbreviatedName: "Bar",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.green.darker(componentDelta: 0.3)), .green],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, pressure in
if let pressure {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, pressure)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Indoor Air Quality Series Configuration
MetricsChartSeries(
id: "iaq",
keyPath: \.iaq,
name: "Indoor Air Quality",
abbreviatedName: "IAQ",
visible: false,
foregroundStyle: { _ in .gray },
chartBody: { series, _, time, iaq in
if let iaq {
let iaqEnum = Iaq.getIaq(for: Int(iaq))
PointMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, Float(iaq))
)
.symbol(Circle())
.foregroundStyle(iaqEnum.color)
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, Float(iaq))
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Combined Wind Speed and Direction Series Configuration -- For use in Chart only
MetricsChartSeries(
id: "windSpeedAndDirection",
keyPath: \.windSpeedAndDirection,
name: "Wind Speed/Direction",
abbreviatedName: "Speed/Dir",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.yellow.darker(componentDelta: 0.3)), Color(UIColor.yellow.darker(componentDelta: 0.1))],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, wsad in
if let wsad {
// debug data: var wsad = WindSpeedAndDirection(windSpeed:Float.random(in:0...25), windDirection: Int32.random(in:0..<3)*90 )
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, wsad.windSpeed)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
PointMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, wsad.windSpeed)
)
.symbol {
if let wd = wsad.windDirection {
Image(systemName: "location.north.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(Color.white, Color(UIColor.yellow.darker(componentDelta: 0.3)))
.rotationEffect(
.degrees(Double(wd)))
}
}.foregroundStyle(.yellow)
}
})
])
}
}
// Extension to combine windspeed and direction into one attribute for rendering
// for rendering on the chart.
@objc class WindSpeedAndDirection: NSObject, Plottable, Comparable {
let windSpeed: Float
let windDirection: Int32?
init(windSpeed: Float, windDirection: Int32?) {
self.windSpeed = windSpeed
self.windDirection = windDirection
}
// Plottable Conformance
required init?(primitivePlottable: Float) { nil }
var primitivePlottable: Float { windSpeed }
static func < (lhs: WindSpeedAndDirection, rhs: WindSpeedAndDirection) -> Bool {
lhs.windSpeed < rhs.windSpeed
}
}
@objc extension TelemetryEntity {
var windSpeedAndDirection: WindSpeedAndDirection? {
guard let windSpeed = self.windSpeed else { return nil }
return WindSpeedAndDirection(windSpeed: windSpeed, windDirection: self.windDirection)
}
}
// From: https://github.com/meshtastic/Meshtastic-Apple/pull/1013/commits/bc932567c742c8fa9fd30752237b10cb762c5ef3
// Set up gradient stops relative to the scale of the temperature chart
func generateStops(minTemp: Double, maxTemp: Double, tempUnit: UnitTemperature, opacity: Double) -> [Gradient.Stop] {
var gradientStops = [Gradient.Stop]()
let stopTargets: [(Double, Color)] = [
((tempUnit == .celsius ? 0 : 32), .blue),
((tempUnit == .celsius ? 20 : 68), .yellow),
((tempUnit == .celsius ? 30 : 86), .orange),
((tempUnit == .celsius ? 55 : 125), .red)
]
for (stopValue, color) in stopTargets {
let stopLocation = transform(stopValue, from: minTemp...maxTemp, to: 0...1)
gradientStops.append(Gradient.Stop(color: color.opacity(opacity), location: stopLocation))
}
return gradientStops
}
// Map inputRange to outputRange
func transform<T: FloatingPoint>(_ input: T, from inputRange: ClosedRange<T>, to outputRange: ClosedRange<T>) -> T {
// need to determine what that value would be in (to.low, to.high)
// difference in output range / difference in input range = slope
let slope = (outputRange.upperBound - outputRange.lowerBound) / (inputRange.upperBound - inputRange.lowerBound)
// slope * normalized input + output lower
let output = slope * (input - inputRange.lowerBound) + outputRange.lowerBound
return output
}

View file

@ -73,6 +73,77 @@ extension MetricsColumnList {
}
}),
// Various Lux
MetricsTableColumn(
id: "lux",
keyPath: \.lux,
name: "Lux",
abbreviatedName: "Lux",
minWidth: 30, maxWidth: 50,
visible: false,
tableBody: { _, lux in
lux.map {
Text("\($0.formatted(.number.grouping(.never).precision(.fractionLength(1))))")
} ?? Text(Constants.nilValueIndicator)
}),
MetricsTableColumn(
id: "whiteLux",
keyPath: \.whiteLux,
name: "White Lux",
abbreviatedName: "White",
minWidth: 30, maxWidth: 50,
visible: false,
tableBody: { _, lux in
lux.map {
Text("\($0.formatted(.number.grouping(.never).precision(.fractionLength(1))))")
} ?? Text(Constants.nilValueIndicator)
}),
MetricsTableColumn(
id: "uvLux",
keyPath: \.uvLux,
name: "UV Lux",
abbreviatedName: "UV",
minWidth: 30, maxWidth: 50,
visible: false,
tableBody: { _, lux in
lux.map {
Text("\($0.formatted(.number.grouping(.never).precision(.fractionLength(1))))")
} ?? Text(Constants.nilValueIndicator)
}),
MetricsTableColumn(
id: "irLux",
keyPath: \.irLux,
name: "IR Lux",
abbreviatedName: "IR",
minWidth: 30, maxWidth: 50,
visible: false,
tableBody: { _, lux in
lux.map {
Text("\($0.formatted(.number.grouping(.never).precision(.fractionLength(1))))")
} ?? Text(Constants.nilValueIndicator)
}),
// Radiation
MetricsTableColumn(
id: "radiation",
keyPath: \.radiation,
name: "Radiation",
abbreviatedName: "☢️",
minWidth: 30, maxWidth: 50,
visible: false,
tableBody: { _, radiation in
radiation.map {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
Text(verbatim: "\($0.formatted(.number.grouping(.never).precision(.fractionLength(1)))) µR/h")
} else {
Text("\($0.formatted(.number.grouping(.never).precision(.fractionLength(1))))")
}
} ?? Text(Constants.nilValueIndicator)
}),
// Wind Direction Series Configuration
MetricsTableColumn(
id: "windDirection",
@ -127,6 +198,126 @@ extension MetricsColumnList {
} ?? Text(verbatim: Constants.nilValueIndicator)
}),
// Rainfall 1-hour
MetricsTableColumn(
id: "rainfall1H",
keyPath: \.rainfall1H,
name: "Rainfall (1H)",
abbreviatedName: "Rain 1H",
minWidth: 30, maxWidth: 60,
visible: false,
tableBody: { _, rainfall in
rainfall.map {
let rain = Measurement(
value: Double($0), unit: UnitLength.millimeters)
return Text(
rain.formatted(
.measurement(
width: .abbreviated,
numberFormatStyle: .number.grouping(.never)
.precision(
.fractionLength(0))))
)
} ?? Text(Constants.nilValueIndicator)
}),
// Rainfall 24-hour
MetricsTableColumn(
id: "rainfall24H",
keyPath: \.rainfall24H,
name: "Rainfall (24H)",
abbreviatedName: "Rain 24H",
minWidth: 30, maxWidth: 60,
visible: false,
tableBody: { _, rainfall in
rainfall.map {
let rain = Measurement(
value: Double($0), unit: UnitLength.millimeters)
return Text(
rain.formatted(
.measurement(
width: .abbreviated,
numberFormatStyle: .number.grouping(.never)
.precision(
.fractionLength(0))))
)
} ?? Text(Constants.nilValueIndicator)
}),
// Weight
MetricsTableColumn(
id: "weight",
keyPath: \.weight,
name: "Weight",
abbreviatedName: "kg",
minWidth: 30, maxWidth: 60,
visible: false,
tableBody: { _, weight in
weight.map {
let weight = Measurement(
value: Double($0), unit: UnitMass.kilograms)
return Text(
weight.formatted(
.measurement(
width: .abbreviated,
numberFormatStyle: .number.grouping(.never)
.precision(
.fractionLength(0))))
)
} ?? Text(Constants.nilValueIndicator)
}),
// Distance sensor, often used for water level
MetricsTableColumn(
id: "distance",
keyPath: \.distance,
name: "Distance",
abbreviatedName: "Dist",
minWidth: 30, maxWidth: 50,
visible: false,
tableBody: { _, distance in
distance.map {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
Text(verbatim: "\($0.formatted(.number.grouping(.never).precision(.fractionLength(1)))) mm")
} else {
Text("\($0.formatted(.number.grouping(.never).precision(.fractionLength(1))))")
}
} ?? Text(Constants.nilValueIndicator)
}),
// Soil Temperature
MetricsTableColumn(
id: "soilTemperature",
keyPath: \.soilTemperature,
name: "Soil Temperature",
abbreviatedName: "Soil Temp",
minWidth: 30, maxWidth: 50,
visible: false,
tableBody: { _, soilTemperature in
soilTemperature.map {
Text($0.formattedTemperature())
} ?? Text(verbatim: Constants.nilValueIndicator)
}),
// Soil Moisture
MetricsTableColumn(
id: "soilMoisture",
keyPath: \.soilMoisture,
name: "Soil Moisture",
abbreviatedName: "Moist",
minWidth: 30, maxWidth: 50,
visible: false,
tableBody: { _, moisture in
moisture.map {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
Text("\($0.formatted(.number.grouping(.never).precision(.fractionLength(0))))%")
} else {
Text("\($0.formatted(.number.grouping(.never).precision(.fractionLength(0))))")
}
} ?? Text(Constants.nilValueIndicator)
}),
// Timestamp Series Configuration -- for use in table only
MetricsTableColumn(
id: "time",

View file

@ -0,0 +1,522 @@
//
// EnvironmentDefaultSeries.swift
// Meshtastic
//
// Created by Jake Bordens on 12/11/24.
//
import Charts
import Foundation
import SwiftUI
// This is the default configuration used by the EnvironmentMetricsLog view for the chart
extension MetricsSeriesList {
static var environmentDefaultChartSeries: MetricsSeriesList {
MetricsSeriesList([
// Temperature Series Configuration
MetricsChartSeries(
id: "temperature",
keyPath: \.temperature,
name: "Temperature",
abbreviatedName: "Temp",
minumumYAxisSpan: 50.0,
conversion: { t in t.map { Float($0.localeTemperature()) } },
foregroundStyle: { chartRange in
let locale = NSLocale.current as NSLocale
let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey"))
let format: UnitTemperature = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? .fahrenheit : .celsius
let lowerBound = chartRange.map { Double($0.lowerBound) } ?? 0.0
let upperBound = chartRange.map { Double($0.upperBound) } ?? 100.0
let stops: [Gradient.Stop] = generateStops(minTemp: lowerBound, maxTemp: upperBound, tempUnit: format, opacity: 1.0)
return LinearGradient(stops: stops, startPoint: .bottom, endPoint: .top)
},
chartBody: { series, chartRange, time, temperature in
if let temperature {
AreaMark(
x: .value("Time", time),
yStart: .value(series.abbreviatedName, chartRange?.lowerBound.doubleValue ?? 0.0),
yEnd: .value(
series.abbreviatedName, temperature.localeTemperature())
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.alignsMarkStylesWithPlotArea()
.accessibilityHidden(true)
.opacity(0.6)
LineMark(
x: .value("Time", time),
y: .value(
series.abbreviatedName, temperature.localeTemperature())
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Relative Humidity Series Configuration
MetricsChartSeries(
id: "relativeHumidity",
keyPath: \.relativeHumidity,
name: "Relative Humidity",
abbreviatedName: "Hum",
initialYAxisRange: 0.0...100.0,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.purple.darker(componentDelta: 0.2)), .purple],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, humidity in
if let humidity {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, humidity)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Barometric Pressure Series Configuration
MetricsChartSeries(
id: "barometricPressure",
keyPath: \.barometricPressure,
name: "Barometric Pressure",
abbreviatedName: "Bar",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.green.darker(componentDelta: 0.3)), .green],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, pressure in
if let pressure {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, pressure)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Indoor Air Quality Series Configuration
MetricsChartSeries(
id: "iaq",
keyPath: \.iaq,
name: "Indoor Air Quality",
abbreviatedName: "IAQ",
visible: false,
foregroundStyle: { _ in .gray },
chartBody: { series, _, time, iaq in
if let iaq {
let iaqEnum = Iaq.getIaq(for: Int(iaq))
PointMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, Float(iaq))
)
.symbol(Circle())
.foregroundStyle(iaqEnum.color)
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, Float(iaq))
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Lux
MetricsChartSeries(
id: "lux",
keyPath: \.lux,
name: "Lux",
abbreviatedName: "Lux",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.cyan.lighter(componentDelta: 0.3)), .cyan],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, lux in
if let lux {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, lux)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// White Lux
MetricsChartSeries(
id: "whiteLux",
keyPath: \.whiteLux,
name: "White Lux",
abbreviatedName: "White",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.cyan.lighter(componentDelta: 0.5)), Color(UIColor.cyan.lighter(componentDelta: 0.2))],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, lux in
if let lux {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, lux)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// UV Lux
MetricsChartSeries(
id: "uvLux",
keyPath: \.uvLux,
name: "UV Lux",
abbreviatedName: "UV",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.systemIndigo.lighter(componentDelta: 0.4)), Color(UIColor.systemIndigo.lighter(componentDelta: 0.2))],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, lux in
if let lux {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, lux)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// IR Lux
MetricsChartSeries(
id: "irLux",
keyPath: \.irLux,
name: "IR Lux",
abbreviatedName: "IR",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.red.darker(componentDelta: 0.5)), .red],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, lux in
if let lux {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, lux)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Radiation
MetricsChartSeries(
id: "radiation",
keyPath: \.radiation,
name: "Radiation",
abbreviatedName: "☢️",
minumumYAxisSpan: 20.0,
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.orange.darker(componentDelta: 0.4)), .orange],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, radiation in
if let radiation {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, radiation)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Combined Wind Speed and Direction Series Configuration -- For use in Chart only
MetricsChartSeries(
id: "windSpeedAndDirection",
keyPath: \.windSpeedAndDirection,
name: "Wind Speed/Direction",
abbreviatedName: "Speed/Dir",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.yellow.darker(componentDelta: 0.3)), Color(UIColor.yellow.darker(componentDelta: 0.1))],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, wsad in
if let wsad {
// debug data: var wsad = WindSpeedAndDirection(windSpeed:Float.random(in:0...25), windDirection: Int32.random(in:0..<3)*90 )
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, wsad.windSpeed)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
PointMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, wsad.windSpeed)
)
.symbol {
if let wd = wsad.windDirection {
Image(systemName: "location.north.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(Color.white, Color(UIColor.yellow.darker(componentDelta: 0.3)))
.rotationEffect(
.degrees(Double(wd)))
}
}.foregroundStyle(.yellow)
}
}),
// Rainfaill 1-hour
MetricsChartSeries(
id: "rainfall1H",
keyPath: \.rainfall1H,
name: "Rainfall 1H",
abbreviatedName: "Rain 1H",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.systemBlue.darker(componentDelta: 0.5)), .blue],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, rainfall in
if let rainfall {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, rainfall)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Rainfaill 24-hour
MetricsChartSeries(
id: "rainfall24H",
keyPath: \.rainfall24H,
name: "Rainfall 24H",
abbreviatedName: "Rain 24H",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.systemBlue.darker(componentDelta: 0.5)), .cyan],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, rainfall in
if let rainfall {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, rainfall)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Weight
MetricsChartSeries(
id: "weight",
keyPath: \.weight,
name: "Weight",
abbreviatedName: "kg",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.systemPink.darker(componentDelta: 0.5)), .pink],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, weight in
if let weight {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, weight)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Distance
MetricsChartSeries(
id: "distance",
keyPath: \.distance,
name: "Distance",
abbreviatedName: "Dist",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.systemTeal.darker(componentDelta: 0.7)), Color(UIColor.systemTeal)],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, distance in
if let distance {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, distance)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Soil Temperature
MetricsChartSeries(
id: "soilTemperature",
keyPath: \.soilTemperature,
name: "Soil Temperature",
abbreviatedName: "Soil Temp",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.brown.darker(componentDelta: 0.4)), .brown],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, soilTemp in
if let soilTemp {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, soilTemp)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
// Soil Temperature
MetricsChartSeries(
id: "soilMoisture",
keyPath: \.soilMoisture,
name: "Soil Moisture",
abbreviatedName: "Moist",
visible: false,
foregroundStyle: { _ in
.linearGradient(
colors: [Color(UIColor.blue.darker(componentDelta: 0.4)), .brown],
startPoint: .bottom, endPoint: .top
)
},
chartBody: { series, _, time, soilMoisture in
if let soilMoisture {
LineMark(
x: .value("Time", time),
y: .value(series.abbreviatedName, soilMoisture)
)
.interpolationMethod(.catmullRom)
.foregroundStyle(by: .value("Series", series.abbreviatedName))
.lineStyle(StrokeStyle(lineWidth: 4))
.alignsMarkStylesWithPlotArea()
}
}),
])
}
}
// Extension to combine windspeed and direction into one attribute for rendering
// for rendering on the chart.
@objc class WindSpeedAndDirection: NSObject, Plottable, Comparable {
let windSpeed: Float
let windDirection: Int32?
init(windSpeed: Float, windDirection: Int32?) {
self.windSpeed = windSpeed
self.windDirection = windDirection
}
// Plottable Conformance
required init?(primitivePlottable: Float) { nil }
var primitivePlottable: Float { windSpeed }
static func < (lhs: WindSpeedAndDirection, rhs: WindSpeedAndDirection) -> Bool {
lhs.windSpeed < rhs.windSpeed
}
}
@objc extension TelemetryEntity {
var windSpeedAndDirection: WindSpeedAndDirection? {
guard let windSpeed = self.windSpeed else { return nil }
return WindSpeedAndDirection(windSpeed: windSpeed, windDirection: self.windDirection)
}
}
// From: https://github.com/meshtastic/Meshtastic-Apple/pull/1013/commits/bc932567c742c8fa9fd30752237b10cb762c5ef3
// Set up gradient stops relative to the scale of the temperature chart
func generateStops(minTemp: Double, maxTemp: Double, tempUnit: UnitTemperature, opacity: Double) -> [Gradient.Stop] {
var gradientStops = [Gradient.Stop]()
let stopTargets: [(Double, Color)] = [
((tempUnit == .celsius ? 0 : 32), .blue),
((tempUnit == .celsius ? 20 : 68), .yellow),
((tempUnit == .celsius ? 30 : 86), .orange),
((tempUnit == .celsius ? 55 : 125), .red)
]
for (stopValue, color) in stopTargets {
let stopLocation = transform(stopValue, from: minTemp...maxTemp, to: 0...1)
gradientStops.append(Gradient.Stop(color: color.opacity(opacity), location: stopLocation))
}
return gradientStops
}
// Map inputRange to outputRange
func transform<T: FloatingPoint>(_ input: T, from inputRange: ClosedRange<T>, to outputRange: ClosedRange<T>) -> T {
// need to determine what that value would be in (to.low, to.high)
// difference in output range / difference in input range = slope
let slope = (outputRange.upperBound - outputRange.lowerBound) / (inputRange.upperBound - inputRange.lowerBound)
// slope * normalized input + output lower
let output = slope * (input - inputRange.lowerBound) + outputRange.lowerBound
return output
}

View file

@ -163,7 +163,7 @@ struct NodeDetail: View {
}
}
if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 {
if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
HStack {
Label {
Text("First heard")
@ -184,7 +184,7 @@ struct NodeDetail: View {
}
}
if let lastHeard = node.lastHeard, lastHeard.timeIntervalSince1970 > 0 {
if let lastHeard = node.lastHeard, lastHeard.timeIntervalSince1970 > 0 && lastHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
HStack {
Label {
Text("Last heard")
@ -195,8 +195,10 @@ struct NodeDetail: View {
Spacer()
if dateFormatRelative, let text = Self.relativeFormatter.string(for: lastHeard) {
Text(text)
.textSelection(.enabled)
if lastHeard.formatted() != "unknown.age".localized {
Text(text)
.textSelection(.enabled)
}
} else {
Text(lastHeard.formatted())
.textSelection(.enabled)
@ -212,7 +214,7 @@ struct NodeDetail: View {
// to use with WeatherKit, or has actual data in the most recent EnvironmentMetrics entity
// that will be rendered in this section.
if node.hasPositions && UserDefaults.environmentEnableWeatherKit
|| node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed"]) {
|| node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "distance", "soilTemperature", "soilMoisture"]) {
Section("Environment") {
if !node.hasEnvironmentMetrics {
LocalWeatherConditions(location: node.latestPosition?.nodeLocation)
@ -245,6 +247,44 @@ struct NodeDetail: View {
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)
}

View file

@ -22,12 +22,12 @@ struct NodeInfoItem: View {
if user.hwModel != "UNSET" {
VStack(alignment: .center) {
Spacer()
Image(systemName: currentDevice?.activelySupported ?? false ? "checkmark.seal.fill" : "x.circle")
Image(systemName: currentDevice?.activelySupported ?? false ? "checkmark.seal.fill" : "seal.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 75, height: 75)
.foregroundStyle(currentDevice?.activelySupported ?? false ? .green : .red)
Text( currentDevice?.activelySupported ?? false ? "Supported" : "Unsupported")
Text( currentDevice?.activelySupported ?? false ? "Full Support" : "Community Support")
.foregroundStyle(.gray)
.font(.callout)
}

View file

@ -64,7 +64,7 @@ struct NodeListItem: View {
let (image, color) = userKeyStatus
IconAndText(systemName: image,
imageColor: color,
text: node.user?.longName ?? "unknown".localized,
text: node.user?.longName?.addingVariationSelectors ?? "unknown".localized,
textColor: .primary)
if node.favorite {
Spacer()
@ -77,10 +77,10 @@ struct NodeListItem: View {
imageColor: .green,
text: "connected".localized)
}
if node.lastHeard?.timeIntervalSince1970 ?? 0 > 0 {
if node.lastHeard?.timeIntervalSince1970 ?? 0 > 0 && node.lastHeard! < Calendar.current.date(byAdding: .year, value: 1, to: Date())!{
IconAndText(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill",
imageColor: node.isOnline ? .green : .orange,
text: node.lastHeard?.formatted() ?? "unknown")
text: node.lastHeard?.formatted() ?? "unknown.age".localized)
}
let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0))
IconAndText(systemName: role?.systemName ?? "figure",

View file

@ -115,7 +115,7 @@ struct MeshMap: View {
editingWaypoint!.longitudeI = Int32((newWaypointCoord?.longitude ?? 0) * 1e7)
editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480)
editingWaypoint!.id = 0
Logger.services.debug("Long press occured at Lat: \(coordinate.latitude) Long: \(coordinate.longitude)")
Logger.services.debug("Long press occured at Lat: \(coordinate.latitude, privacy: .public) Long: \(coordinate.longitude, privacy: .public)")
default: return
}
})

View file

@ -237,7 +237,7 @@ struct NodeList: View {
if deleteNode != nil {
let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(bleManager.connectedPeripheral?.num ?? -1))
if !success {
Logger.data.error("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized)")
Logger.data.error("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized, privacy: .public)")
}
}
}
@ -264,7 +264,7 @@ struct NodeList: View {
columnVisibility: columnVisibility
)
.edgesIgnoringSafeArea([.leading, .trailing])
.navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
.navigationBarTitle(String(node.user?.longName?.addingVariationSelectors ?? "unknown".localized), displayMode: .inline)
.navigationBarItems(
trailing: ZStack {
if UIDevice.current.userInterfaceIdiom != .phone {

View file

@ -176,7 +176,7 @@ struct PaxCounterLog: View {
) {
Button("paxcounter.delete", role: .destructive) {
if clearPax(destNum: node.num, context: context) {
Logger.services.info("Cleared Pax Counter for \(node.num)")
Logger.services.info("Cleared Pax Counter for \(node.num, privacy: .public)")
} else {
Logger.services.error("Clear Pax Counter Log Failed")
}
@ -216,7 +216,7 @@ struct PaxCounterLog: View {
self.isExporting = false
Logger.services.info("PAX Counter log download succeeded")
case .failure(let error):
Logger.services.error("PAX Counter log download failed: \(error.localizedDescription)")
Logger.services.error("PAX Counter log download failed: \(error.localizedDescription, privacy: .public)")
}
}
)

View file

@ -163,7 +163,7 @@ struct PositionLog: View {
Logger.services.info("Position log download succeeded.")
self.isExporting = false
case .failure(let error):
Logger.services.error("Position log download failed: \(error.localizedDescription)")
Logger.services.error("Position log download failed: \(error.localizedDescription, privacy: .public)")
}
}
)

View file

@ -243,7 +243,7 @@ struct PowerMetricsLog: View {
) {
Button("Delete Power metrics?", role: .destructive) {
if clearTelemetry(destNum: node.num, metricsType: 2, context: context) {
Logger.data.notice("Cleared Power Metrics for \(node.num)")
Logger.data.notice("Cleared Power Metrics for \(node.num, privacy: .public)")
} else {
Logger.data.error("Clear Power Metrics Log Failed")
}
@ -272,7 +272,7 @@ struct PowerMetricsLog: View {
ContentUnavailableView("No Power Metrics", systemImage: "slash.circle")
}
}
.navigationTitle("Power Metrics Log}")
.navigationTitle("Power Metrics Log")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {
@ -289,7 +289,7 @@ struct PowerMetricsLog: View {
self.isExporting = false
Logger.services.info("Power metrics log download succeeded.")
case .failure(let error):
Logger.services.error("Power metrics log download failed: \(error.localizedDescription)")
Logger.services.error("Power metrics log download failed: \(error.localizedDescription, privacy: .public)")
}
}
)

View file

@ -64,7 +64,7 @@ struct TraceRouteLog: View {
do {
try context.save()
} catch let error as NSError {
Logger.data.error("\(error.localizedDescription)")
Logger.data.error("\(error.localizedDescription, privacy: .public)")
}
} label: {
Label("delete", systemImage: "trash")

View file

@ -25,34 +25,6 @@ struct AppData: View {
GPSStatus()
}
Divider()
Button(action: {
let container = NSPersistentContainer(name: "Meshtastic")
guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
Logger.data.error("nil File path for back")
return
}
do {
try container.copyPersistentStores(to: url.appendingPathComponent("backup").appendingPathComponent("\(UserDefaults.preferredPeripheralNum)"), overwriting: true)
loadFiles()
Logger.data.notice("🗂️ Made a core data backup to backup/\(UserDefaults.preferredPeripheralNum)")
} catch {
Logger.data.error("🗂️ Core data backup copy error: \(error, privacy: .public)")
}
}) {
Label {
Text("Backup Database")
.font(idiom == .phone ? .callout : .title)
} icon: {
Image(systemName: "cylinder.split.1x2")
.symbolRenderingMode(.hierarchical)
.font(idiom == .phone ? .callout : .title)
.frame(width: 35)
}
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
Divider()
}
List(files, id: \.self) { file in
@ -62,20 +34,6 @@ struct AppData: View {
Label {
Text("Node Core Data Backup \(file.pathComponents[(idiom == .phone || idiom == .pad) ? 9 : 10])/\(file.lastPathComponent) - \(file.creationDate?.formatted() ?? "") - \(file.fileSizeString)")
.swipeActions {
Button(role: .none) {
bleManager.disconnectPeripheral(reconnect: false)
let container = NSPersistentContainer(name: "Meshtastic")
do {
try container.restorePersistentStore(from: file.absoluteURL)
UserDefaults.preferredPeripheralId = ""
UserDefaults.preferredPeripheralNum = Int(file.pathComponents[(idiom == .phone || idiom == .pad) ? 9 : 10]) ?? 0
Logger.data.notice("🗂️ Restored a core data backup to backup/\(UserDefaults.preferredPeripheralNum, privacy: .public)")
} catch {
Logger.data.error("🗂️ Core data restore copy error: \(error, privacy: .public)")
}
} label: {
Label("restore", systemImage: "arrow.counterclockwise")
}
Button(role: .destructive) {
do {
try FileManager.default.removeItem(at: file)

View file

@ -178,7 +178,7 @@ struct AppLog: View {
self.isExporting = false
Logger.services.info("Application log download succeeded.")
case .failure(let error):
Logger.services.error("Application log download failed: \(error.localizedDescription)")
Logger.services.error("Application log download failed: \(error.localizedDescription, privacy: .public)")
}
}
)

View file

@ -186,11 +186,11 @@ struct Channels: View {
if channel.role != Channel.Role.disabled {
do {
try context.save()
Logger.data.info("💾 Saved Channel: \(channel.settings.name)")
Logger.data.info("💾 Saved Channel: \(channel.settings.name, privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Unresolved Core Data error in the channel editor. Error: \(nsError)")
Logger.data.error("Unresolved Core Data error in the channel editor. Error: \(nsError, privacy: .public)")
}
} else {
let objects = selectedChannel?.allPrivateMessages ?? []
@ -203,11 +203,11 @@ struct Channels: View {
context.delete(selectedChannel!)
do {
try context.save()
Logger.data.info("💾 Deleted Channel: \(channel.settings.name)")
Logger.data.info("💾 Deleted Channel: \(channel.settings.name, privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Unresolved Core Data error in the channel editor. Error: \(nsError)")
Logger.data.error("Unresolved Core Data error in the channel editor. Error: \(nsError, privacy: .public)")
}
}
let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!)

View file

@ -21,6 +21,7 @@ struct DeviceConfig: View {
@State private var isPresentingFactoryResetConfirm = false
@State var hasChanges = false
@State var deviceRole = 0
@State private var pendingDeviceRole = 0
@State var buzzerGPIO = 0
@State var buttonGPIO = 0
@State var rebroadcastMode = 0
@ -29,6 +30,10 @@ struct DeviceConfig: View {
@State var ledHeartbeatEnabled = true
@State var tripleClickAsAdHocPing = true
@State var tzdef = ""
@State private var showRouterWarning = false
@State private var confirmWarning = false
var body: some View {
VStack {
@ -39,9 +44,37 @@ struct DeviceConfig: View {
VStack(alignment: .leading) {
Picker("Device Role", selection: $deviceRole ) {
ForEach(DeviceRoles.allCases) { dr in
Text(dr.name)
Text(dr.name).tag(dr.rawValue as Int)
}
}
.onChange(of: deviceRole) { oldValue, newValue in
if !confirmWarning {
if [2, 11].contains(newValue) {
pendingDeviceRole = newValue
deviceRole = oldValue // Reset selection until confirmed
showRouterWarning = true
}
} else {
confirmWarning = false
}
}
.confirmationDialog(
"Are you sure?",
isPresented: $showRouterWarning,
titleVisibility: .visible
) {
Button("Confirm") {
deviceRole = pendingDeviceRole
pendingDeviceRole = 0
confirmWarning = true
}
Button("Cancel", role: .cancel) {
pendingDeviceRole = 0
}
} message: {
Text("The Router roles are designed for high vantage locations like mountaintops and towers. This node needs to be able to have a good direct connection to most of the nodes on the network or else this will significantly hurt the network.")
}
Text(DeviceRoles(rawValue: deviceRole)?.description ?? "")
.foregroundColor(.gray)
.font(.callout)

View file

@ -249,7 +249,6 @@ 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)
}
} else {

View file

@ -230,31 +230,88 @@ struct MQTTConfig: View {
}
.scrollDismissesKeyboard(.interactively)
.disabled(self.bleManager.connectedPeripheral == nil || node?.mqttConfig == nil)
}
SaveConfigButton(node: node, hasChanges: $hasChanges) {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
if connectedNode != nil {
var mqtt = ModuleConfig.MQTTConfig()
mqtt.enabled = self.enabled
mqtt.proxyToClientEnabled = self.proxyToClientEnabled
mqtt.address = self.address
mqtt.username = self.username
mqtt.password = self.password
mqtt.root = self.root
mqtt.encryptionEnabled = self.encryptionEnabled
mqtt.jsonEnabled = self.jsonEnabled
mqtt.tlsEnabled = self.tlsEnabled
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)
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
hasChanges = false
goBack()
SaveConfigButton(node: node, hasChanges: $hasChanges) {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
if connectedNode != nil {
var mqtt = ModuleConfig.MQTTConfig()
mqtt.enabled = self.enabled
mqtt.proxyToClientEnabled = self.proxyToClientEnabled
mqtt.address = self.address
mqtt.username = self.username
mqtt.password = self.password
mqtt.root = self.root
mqtt.encryptionEnabled = self.encryptionEnabled
mqtt.jsonEnabled = self.jsonEnabled
mqtt.tlsEnabled = self.tlsEnabled
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)
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
hasChanges = false
goBack()
}
}
}.onChange(of: enabled) { _, newEnabled in
if newEnabled != node?.mqttConfig?.enabled { hasChanges = true }
}
.onChange(of: proxyToClientEnabled) { _, newProxyToClientEnabled in
if newProxyToClientEnabled {
jsonEnabled = false
tlsEnabled = false
}
if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true }
}
.onChange(of: address) { _, newAddress in
if newAddress != node?.mqttConfig?.address ?? "" { hasChanges = true }
}
.onChange(of: username) { _, newUsername in
if newUsername != node?.mqttConfig?.username ?? "" { hasChanges = true }
}
.onChange(of: password) { _, newPassword in
if newPassword != node?.mqttConfig?.password ?? "" { hasChanges = true }
}
.onChange(of: root) { _, newRoot in
if newRoot != node?.mqttConfig?.root ?? "" { hasChanges = true }
}
.onChange(of: selectedTopic) { _, newSelectedTopic in
root = newSelectedTopic
}
.onChange(of: encryptionEnabled) { _, newEncryptionEnabled in
if newEncryptionEnabled != node?.mqttConfig?.encryptionEnabled { hasChanges = true }
}
.onChange(of: jsonEnabled) { _, newJsonEnabled in
if newJsonEnabled {
proxyToClientEnabled = false
}
if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true }
}
.onChange(of: tlsEnabled) { _, newTlsEnabled in
if address.lowercased() == "mqtt.meshtastic.org" {
tlsEnabled = false
} else {
if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true }
}
}
.onChange(of: mqttConnected) { _, newMqttConnected in
if newMqttConnected == false {
if bleManager.mqttProxyConnected {
bleManager.mqttManager.disconnect()
}
} else {
if !bleManager.mqttProxyConnected && node != nil {
bleManager.mqttManager.connectFromConfigSettings(node: node!)
}
}
}
.onChange(of: mapReportingEnabled) { _, newMapReportingEnabled in
if newMapReportingEnabled != node?.mqttConfig?.mapReportingEnabled { hasChanges = true }
}
.onChange(of: mapPublishIntervalSecs) { _, newMapPublishIntervalSecs in
if newMapPublishIntervalSecs != node?.mqttConfig?.mapPublishIntervalSecs ?? -1 { hasChanges = true }
}
}
.navigationTitle("mqtt.config")
@ -267,64 +324,6 @@ struct MQTTConfig: View {
)
}
)
.onChange(of: enabled) { _, newEnabled in
if newEnabled != node?.mqttConfig?.enabled { hasChanges = true }
}
.onChange(of: proxyToClientEnabled) { _, newProxyToClientEnabled in
if newProxyToClientEnabled {
jsonEnabled = false
tlsEnabled = false
}
if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true }
}
.onChange(of: address) { newAddress in
if newAddress != node?.mqttConfig?.address ?? "" { hasChanges = true }
}
.onChange(of: username) { newUsername in
if newUsername != node?.mqttConfig?.username ?? "" { hasChanges = true }
}
.onChange(of: password) { newPassword in
if newPassword != node?.mqttConfig?.password ?? "" { hasChanges = true }
}
.onChange(of: root) { _, newRoot in
if newRoot != node?.mqttConfig?.root ?? "" { hasChanges = true }
}
.onChange(of: selectedTopic) { _, newSelectedTopic in
root = newSelectedTopic
}
.onChange(of: encryptionEnabled) { _, newEncryptionEnabled in
if newEncryptionEnabled != node?.mqttConfig?.encryptionEnabled { hasChanges = true }
}
.onChange(of: jsonEnabled) { _, newJsonEnabled in
if newJsonEnabled {
proxyToClientEnabled = false
}
if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true }
}
.onChange(of: tlsEnabled) { _, newTlsEnabled in
if address.lowercased() == "mqtt.meshtastic.org" {
tlsEnabled = false
} else {
if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true }
}
}
.onChange(of: mqttConnected) { _, newMqttConnected in
if newMqttConnected == false {
if bleManager.mqttProxyConnected {
bleManager.mqttManager.disconnect()
}
} else {
if !bleManager.mqttProxyConnected && node != nil {
bleManager.mqttManager.connectFromConfigSettings(node: node!)
}
}
}
.onChange(of: mapReportingEnabled) { _, newMapReportingEnabled in
if newMapReportingEnabled != node?.mqttConfig?.mapReportingEnabled { hasChanges = true }
}
.onChange(of: mapPublishIntervalSecs) { _, newMapPublishIntervalSecs in
if newMapPublishIntervalSecs != node?.mqttConfig?.mapPublishIntervalSecs ?? -1 { hasChanges = true }
}
.onFirstAppear {
// Need to request a MqttModuleConfig from the remote node before allowing changes
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
@ -348,6 +347,7 @@ struct MQTTConfig: View {
}
}
}
func setMqttValues() {
nearbyTopics = []
@ -357,7 +357,7 @@ struct MQTTConfig: View {
defaultTopic = "msh/" + (region?.topic ?? "UNSET")
geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in
if let error {
Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription)")
Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription, privacy: .public)")
return
}

View file

@ -18,8 +18,8 @@ struct StoreForwardConfig: View {
@State var hasChanges: Bool = false
/// Enable the Store and Forward Module
@State var enabled = false
/// Is a S&F Router
@State var isRouter = false
/// Is a S&F Server
@State var isServer = false
/// Send a Heartbeat
@State var heartbeat: Bool = false
/// Number of Records
@ -35,43 +35,19 @@ struct StoreForwardConfig: View {
ConfigHeader(title: "Store & Forward", config: \.storeForwardConfig, node: node, onAppear: setStoreAndForwardValues)
Section(header: Text("options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
Text("Enables the store and forward module. Store and forward must be enabled on both client and router devices.")
Text("Enables the store and forward module.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.listRowSeparator(.visible)
if enabled {
HStack {
Picker(selection: $isRouter, label: Text("Role")) {
Text("Client")
.tag(false)
Text("Router")
.tag(true)
}
.pickerStyle(SegmentedPickerStyle())
.padding(.top, 5)
.padding(.bottom, 5)
}
VStack {
if isRouter {
Text("Store and forward router devices require a ESP32 device with PSRAM.")
.foregroundColor(.gray)
.font(.callout)
} else {
Text("Store and forward clients can request history from routers on the network.")
.foregroundColor(.gray)
.font(.callout)
}
}
}
}
if isRouter {
Section(header: Text("Router Options")) {
if enabled {
Section(header: Text("Settings")) {
Toggle(isOn: $heartbeat) {
Label("storeforward.heartbeat", systemImage: "waveform.path.ecg")
Text("Send a heartbeat to advertise the server's presence.")
}
Picker("Number of records", selection: $records) {
Text("unset").tag(0)
@ -81,7 +57,7 @@ struct StoreForwardConfig: View {
Text("100").tag(100)
}
.pickerStyle(DefaultPickerStyle())
Picker("History Return Max", selection: $historyReturnMax ) {
Picker("History Return Max", selection: $historyReturnMax) {
Text("unset").tag(0)
Text("25").tag(25)
Text("50").tag(50)
@ -89,7 +65,7 @@ struct StoreForwardConfig: View {
Text("100").tag(100)
}
.pickerStyle(DefaultPickerStyle())
Picker("History Return Window", selection: $historyReturnWindow ) {
Picker("History Return Window", selection: $historyReturnWindow) {
Text("unset").tag(0)
Text("One Minute").tag(60)
Text("Five Minutes").tag(300)
@ -101,6 +77,20 @@ struct StoreForwardConfig: View {
}
.pickerStyle(DefaultPickerStyle())
}
Section(header: Text("Server Option")) {
Toggle(isOn: $isServer) {
Label("Server", systemImage: "server.rack")
Text("Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.listRowSeparator(.visible)
if isServer {
Text("Store and forward servers require an ESP32 device with PSRAM or Linux Native.")
.foregroundColor(.gray)
.font(.callout)
}
}
}
}
.scrollDismissesKeyboard(.interactively)
@ -110,18 +100,19 @@ struct StoreForwardConfig: View {
SaveConfigButton(node: node, hasChanges: $hasChanges) {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
if connectedNode != nil {
/// Let the user set isRouter for the connected node, for nodes on the mesh set isRouter based
/// Let the user set isServer for the connected node, for nodes on the mesh set isServer based
/// on receipt of a primary heartbeat
if connectedNode?.num ?? 0 == node?.num ?? -1 {
connectedNode?.storeForwardConfig?.isRouter = isRouter
connectedNode?.storeForwardConfig?.isRouter = isServer
do {
try context.save()
} catch {
Logger.mesh.error("Failed to save isRouter: \(error.localizedDescription)")
Logger.mesh.error("Failed to save isServer: \(error.localizedDescription, privacy: .public)")
}
}
var sfc = ModuleConfig.StoreForwardConfig()
sfc.isServer = isServer
sfc.enabled = self.enabled
sfc.heartbeat = self.heartbeat
sfc.records = UInt32(self.records)
@ -171,8 +162,8 @@ struct StoreForwardConfig: View {
.onChange(of: enabled) { oldEnabled, newEnabled in
if oldEnabled != newEnabled && newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true }
}
.onChange(of: isRouter) { oldIsRouter, newIsRouter in
if oldIsRouter != newIsRouter && newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true }
.onChange(of: isServer) { oldIsServer, newIsServer in
if oldIsServer != newIsServer && newIsServer != node!.storeForwardConfig!.isRouter { hasChanges = true }
}
.onChange(of: heartbeat) { oldHeartbeat, newHeartbeat in
if oldHeartbeat != newHeartbeat && newHeartbeat != node?.storeForwardConfig?.heartbeat ?? true { hasChanges = true }
@ -187,9 +178,10 @@ struct StoreForwardConfig: View {
if oldHistoryReturnWindow != newHistoryReturnWindow && newHistoryReturnWindow != node!.storeForwardConfig?.historyReturnWindow ?? -1 { hasChanges = true }
}
}
func setStoreAndForwardValues() {
self.enabled = (node?.storeForwardConfig?.enabled ?? false)
self.isRouter = (node?.storeForwardConfig?.isRouter ?? false)
self.isServer = (node?.storeForwardConfig?.isRouter ?? false)
self.heartbeat = (node?.storeForwardConfig?.heartbeat ?? true)
self.records = Int(node?.storeForwardConfig?.records ?? 50)
self.historyReturnMax = Int(node?.storeForwardConfig?.historyReturnMax ?? 100)

View file

@ -24,66 +24,78 @@ struct NetworkConfig: View {
@State var ntpServer = ""
@State var ethEnabled = false
@State var ethMode = 0
@State var udpEnabled = false
var body: some View {
VStack {
Form {
ConfigHeader(title: "Network", config: \.networkConfig, node: node, onAppear: setNetworkValues)
if node != nil && node?.metadata?.hasWifi ?? false {
Section(header: Text("WiFi Options")) {
if let node {
if node.metadata?.hasWifi ?? false {
Section(header: Text("WiFi Options")) {
Toggle(isOn: $wifiEnabled) {
Label("enabled", systemImage: "wifi")
Text("Enabling WiFi will disable the bluetooth connection to the app.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $wifiEnabled) {
Label("enabled", systemImage: "wifi")
Text("Enabling WiFi will disable the bluetooth connection to the app.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
HStack {
Label("ssid", systemImage: "network")
TextField("ssid", text: $wifiSsid)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: wifiSsid) {
var totalBytes = wifiSsid.utf8.count
// Only mess with the value if it is too big
while totalBytes > 32 {
wifiSsid = String(wifiSsid.dropLast())
totalBytes = wifiSsid.utf8.count
HStack {
Label("ssid", systemImage: "network")
TextField("ssid", text: $wifiSsid)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: wifiSsid) {
var totalBytes = wifiSsid.utf8.count
// Only mess with the value if it is too big
while totalBytes > 32 {
wifiSsid = String(wifiSsid.dropLast())
totalBytes = wifiSsid.utf8.count
}
hasChanges = true
}
hasChanges = true
}
.foregroundColor(.gray)
}
.keyboardType(.default)
HStack {
Label("password", systemImage: "wallet.pass")
TextField("password", text: $wifiPsk)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: wifiPsk) {
var totalBytes = wifiPsk.utf8.count
// Only mess with the value if it is too big
while totalBytes > 63 {
wifiPsk = String(wifiPsk.dropLast())
totalBytes = wifiPsk.utf8.count
.foregroundColor(.gray)
}
.keyboardType(.default)
HStack {
Label("password", systemImage: "wallet.pass")
TextField("password", text: $wifiPsk)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: wifiPsk) {
var totalBytes = wifiPsk.utf8.count
// Only mess with the value if it is too big
while totalBytes > 63 {
wifiPsk = String(wifiPsk.dropLast())
totalBytes = wifiPsk.utf8.count
}
hasChanges = true
}
hasChanges = true
}
.foregroundColor(.gray)
.foregroundColor(.gray)
}
.keyboardType(.default)
}
.keyboardType(.default)
}
}
if node != nil && node?.metadata?.hasEthernet ?? false {
Section(header: Text("Ethernet Options")) {
Toggle(isOn: $ethEnabled) {
Label("enabled", systemImage: "network")
Text("Enabling Ethernet will disable the bluetooth connection to the app.")
if node.metadata?.hasEthernet ?? false {
Section(header: Text("Ethernet Options")) {
Toggle(isOn: $ethEnabled) {
Label("enabled", systemImage: "network")
Text("Enabling Ethernet will disable the bluetooth connection to the app.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
if node.metadata?.hasEthernet ?? false || node.metadata?.hasWifi ?? false {
Section(header: Text("UDP Broadcast")) {
Toggle(isOn: $udpEnabled) {
Label("enabled", systemImage: "point.3.connected.trianglepath.dotted")
Text("Enable broadcasting packets via UDP over the local network.")
}
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
}
@ -98,6 +110,7 @@ struct NetworkConfig: View {
network.wifiSsid = self.wifiSsid
network.wifiPsk = self.wifiPsk
network.ethEnabled = self.ethEnabled
network.enabledProtocols = self.udpEnabled ? UInt32(Config.NetworkConfig.ProtocolFlags.udpBroadcast.rawValue) : UInt32(Config.NetworkConfig.ProtocolFlags.noBroadcast.rawValue)
// network.addressMode = Config.NetworkConfig.AddressMode.dhcp
let adminMessageId = bleManager.saveNetworkConfig(config: network, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
@ -166,14 +179,30 @@ struct NetworkConfig: View {
}
.onChange(of: ethEnabled) { _, newEthEnabled in
if newEthEnabled != node?.networkConfig?.ethEnabled { hasChanges = true }
}.onChange(of: udpEnabled) {_, newUdpEnabled in
if let netConfig = node?.networkConfig {
let newValue: UInt32
if newUdpEnabled {
newValue = UInt32(netConfig.enabledProtocols) | UInt32(Config.NetworkConfig.ProtocolFlags.udpBroadcast.rawValue)
} else {
newValue = UInt32(netConfig.enabledProtocols) & ~UInt32(Config.NetworkConfig.ProtocolFlags.udpBroadcast.rawValue)
}
if netConfig.enabledProtocols != Int32(newValue) {
netConfig.enabledProtocols = Int32(newValue)
hasChanges = true
}
}
}
}
func setNetworkValues() {
self.wifiEnabled = node?.networkConfig?.wifiEnabled ?? false
self.wifiSsid = node?.networkConfig?.wifiSsid ?? ""
self.wifiPsk = node?.networkConfig?.wifiPsk ?? ""
self.wifiMode = Int(node?.networkConfig?.wifiMode ?? 0)
self.ethEnabled = node?.networkConfig?.ethEnabled ?? false
let enabledProtocols = UInt32(node?.networkConfig?.enabledProtocols ?? Int32(Config.NetworkConfig.ProtocolFlags.noBroadcast.rawValue))
self.udpEnabled = enabledProtocols & UInt32(Config.NetworkConfig.ProtocolFlags.udpBroadcast.rawValue) != 0
self.hasChanges = false
}
}

View file

@ -530,7 +530,7 @@ struct PositionConfig: View {
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving Position Config Entity \(nsError)")
Logger.data.error("Error Saving Position Config Entity \(nsError, privacy: .public)")
}
}
@ -550,7 +550,7 @@ struct PositionConfig: View {
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving Position Config Entity \(nsError)")
Logger.data.error("Error Saving Position Config Entity \(nsError, privacy: .public)")
}
}
}

View file

@ -22,12 +22,11 @@ struct SecurityConfig: View {
@State var hasChanges = false
@State var publicKey = ""
@State var hasValidPublicKey: Bool = false
@State var privateKey = ""
@State var hasValidPrivateKey: Bool = false
@State var adminKey = ""
@State var adminKey2 = ""
@State var adminKey3 = ""
@State var adminKey: String = ""
@State var adminKey2: String = ""
@State var adminKey3: String = ""
@State var hasValidAdminKey: Bool = true
@State var hasValidAdminKey2: Bool = true
@State var hasValidAdminKey3: Bool = true
@ -45,11 +44,14 @@ struct SecurityConfig: View {
Section(header: Text("Admin & Direct Message Keys")) {
VStack(alignment: .leading) {
Label("Public Key", systemImage: "key")
SecureInput("Public Key", text: $publicKey, isValid: $hasValidPublicKey)
.background(
RoundedRectangle(cornerRadius: 10.0)
.stroke(hasValidPublicKey ? Color.clear : Color.red, lineWidth: 2.0)
)
Text(publicKey)
.font(idiom == .phone ? .caption : .callout)
.allowsTightening(true)
.monospaced()
.keyboardType(.alphabet)
.foregroundStyle(.tertiary)
.disableAutocorrection(true)
.textSelection(.enabled)
Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
@ -144,15 +146,6 @@ struct SecurityConfig: View {
.onChange(of: adminChannelEnabled) { _, newAdminChannelEnabled in
if newAdminChannelEnabled != node?.securityConfig?.adminChannelEnabled { hasChanges = true }
}
.onChange(of: publicKey) {
let tempKey = Data(base64Encoded: publicKey) ?? Data()
if tempKey.count == 32 {
hasValidPublicKey = true
} else {
hasValidPublicKey = false
}
hasChanges = true
}
.onChange(of: privateKey) {
let tempKey = Data(base64Encoded: privateKey) ?? Data()
if tempKey.count == 32 {
@ -222,7 +215,7 @@ struct SecurityConfig: View {
SaveConfigButton(node: node, hasChanges: $hasChanges) {
if !hasValidPublicKey || !hasValidPrivateKey || !hasValidAdminKey {
if !hasValidPrivateKey || !hasValidAdminKey || !hasValidAdminKey2 || !hasValidAdminKey3 {
return
}
@ -259,9 +252,9 @@ struct SecurityConfig: View {
func setSecurityValues() {
self.publicKey = node?.securityConfig?.publicKey?.base64EncodedString() ?? ""
self.privateKey = node?.securityConfig?.privateKey?.base64EncodedString() ?? ""
self.adminKey = node?.securityConfig?.adminKey?.base64EncodedString() ?? ""
self.adminKey2 = node?.securityConfig?.adminKey2?.base64EncodedString() ?? ""
self.adminKey3 = node?.securityConfig?.adminKey3?.base64EncodedString() ?? ""
self.adminKey = node?.securityConfig?.adminKey?.base64EncodedString(options: .lineLength64Characters) ?? ""
self.adminKey2 = node?.securityConfig?.adminKey2?.base64EncodedString(options: .lineLength64Characters) ?? ""
self.adminKey3 = node?.securityConfig?.adminKey3?.base64EncodedString(options: .lineLength64Characters) ?? ""
self.isManaged = node?.securityConfig?.isManaged ?? false
self.serialEnabled = node?.securityConfig?.serialEnabled ?? false
self.debugLogApiEnabled = node?.securityConfig?.debugLogApiEnabled ?? false

View file

@ -138,7 +138,7 @@ struct Firmware: View {
.font(.callout)
.padding(.bottom)
} else {
Text("OTA Updates are not supported on the this NRF Device.")
Text("OTA Updates are not supported on this NRF Device.")
.font(.title3)
Link("Drag & Drop Firmware Update", destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/nrf52/drag-n-drop")!)
.font(.callout)

View file

@ -62,7 +62,7 @@ class Api: ObservableObject {
completion(deviceHardware)
}
} catch {
Logger.services.error("JSON decode failure: \(error.localizedDescription)")
Logger.services.error("JSON decode failure: \(error.localizedDescription, privacy: .public)")
}
return
}
@ -82,7 +82,7 @@ class Api: ObservableObject {
completion(firmwareReleases)
}
} catch {
Logger.services.error("JSON decode failure: \(error.localizedDescription)")
Logger.services.error("JSON decode failure: \(error.localizedDescription, privacy: .public)")
}
return
}

View file

@ -188,7 +188,7 @@ struct RouteRecorder: View {
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving RouteEntity from the Route Recorder \(nsError)")
Logger.data.error("Error Saving RouteEntity from the Route Recorder \(nsError, privacy: .public)")
}
} label: {
Label("start", systemImage: "play")
@ -246,7 +246,7 @@ struct RouteRecorder: View {
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving RouteEntity from the Route Recorder \(nsError)")
Logger.data.error("Error Saving RouteEntity from the Route Recorder \(nsError, privacy: .public)")
}
isShowingDetails = false
} label: {
@ -298,11 +298,10 @@ struct RouteRecorder: View {
do {
try context.save()
Logger.data.info("💾 Saved a new route location")
// logger.info("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving LocationEntity from the Route Recorder \(nsError)")
Logger.data.error("Error Saving LocationEntity from the Route Recorder \(nsError, privacy: .public)")
}
}
}

View file

@ -64,7 +64,7 @@ struct Routes: View {
var latIndex = -1
var longIndex = -1
for index in headers!.indices {
Logger.services.debug("\(index): \( headers![index])")
Logger.services.debug("\(index, privacy: .public): \( headers![index], privacy: .public)")
if headers![index].trimmingCharacters(in: .whitespaces) == "Latitude" {
latIndex = index
} else if headers![index].trimmingCharacters(in: .whitespaces) == "Longitude" {
@ -94,7 +94,7 @@ struct Routes: View {
do {
try context.save()
} catch let error as NSError {
Logger.services.error("\(error.localizedDescription)")
Logger.services.error("\(error.localizedDescription, privacy: .public)")
isShowingBadFileAlert = true
}
} else {
@ -103,11 +103,11 @@ struct Routes: View {
} catch {
// TODO: deal with errors
Logger.services.error("\(error.localizedDescription)")
Logger.services.error("\(error.localizedDescription, privacy: .public)")
}
} catch {
Logger.services.error("CSV Import Error: \(error.localizedDescription)")
Logger.services.error("CSV Import Error: \(error.localizedDescription, privacy: .public)")
}
}
List(routes, id: \.self, selection: $selectedRoute) { route in
@ -151,7 +151,7 @@ struct Routes: View {
do {
try context.save()
} catch let error as NSError {
Logger.data.error("\(error.localizedDescription)")
Logger.data.error("\(error.localizedDescription, privacy: .public)")
}
} label: {
Label("delete", systemImage: "trash")
@ -227,7 +227,7 @@ struct Routes: View {
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving RouteEntity from the Route Editor \(nsError)")
Logger.data.error("Error Saving RouteEntity from the Route Editor \(nsError, privacy: .public)")
}
}
.buttonStyle(.bordered)
@ -300,7 +300,7 @@ struct Routes: View {
self.isExporting = false
Logger.services.info("Route log download succeeded.")
case .failure(let error):
Logger.services.error("Route log download failed: \(error.localizedDescription).")
Logger.services.error("Route log download failed: \(error.localizedDescription, privacy: .public).")
}
}
)

View file

@ -8,6 +8,7 @@
import SwiftUI
import OSLog
import TipKit
import MeshtasticProtobufs
struct Settings: View {
@Environment(\.managedObjectContext) var context
@ -25,10 +26,21 @@ struct Settings: View {
@State private var selectedNode: Int = 0
@State private var preferredNodeNum: Int = 0
@State private var moduleOverride: Bool = false
@ObservedObject
var router: Router
// MARK: Helper
private func isModuleSupported(_ module: ExcludedModules) -> Bool {
return moduleOverride || Int(nodes.first(where: { $0.num == preferredNodeNum })?.metadata?.excludedModules ?? Int32.zero) & module.rawValue == 0
}
private func isAnySupported(_ modules: [ExcludedModules]) -> Bool {
return modules.map(isModuleSupported).contains(true)
}
// MARK: Views
var radioConfigurationSection: some View {
@ -153,57 +165,68 @@ struct Settings: View {
}
var moduleConfigurationSection: some View {
Section("module.configuration") {
NavigationLink(value: SettingsNavigationState.ambientLighting) {
Label {
Text("Ambient Lighting")
} icon: {
Image(systemName: "light.max")
Section {
if isModuleSupported(.ambientlightingConfig) {
NavigationLink(value: SettingsNavigationState.ambientLighting) {
Label {
Text("Ambient Lighting")
} icon: {
Image(systemName: "light.max")
}
}
}
NavigationLink(value: SettingsNavigationState.cannedMessages) {
Label {
Text("Canned Messages")
} icon: {
Image(systemName: "list.bullet.rectangle.fill")
if isModuleSupported(.cannedmsgConfig) {
NavigationLink(value: SettingsNavigationState.cannedMessages) {
Label {
Text("Canned Messages")
} icon: {
Image(systemName: "list.bullet.rectangle.fill")
}
}
}
NavigationLink(value: SettingsNavigationState.detectionSensor) {
Label {
Text("detection.sensor")
} icon: {
Image(systemName: "sensor")
if isModuleSupported(.detectionsensorConfig) {
NavigationLink(value: SettingsNavigationState.detectionSensor) {
Label {
Text("detection.sensor")
} icon: {
Image(systemName: "sensor")
}
}
}
NavigationLink(value: SettingsNavigationState.externalNotification) {
Label {
Text("external.notification")
} icon: {
Image(systemName: "megaphone")
if isModuleSupported(.extnotifConfig) {
NavigationLink(value: SettingsNavigationState.externalNotification) {
Label {
Text("external.notification")
} icon: {
Image(systemName: "megaphone")
}
}
}
NavigationLink(value: SettingsNavigationState.mqtt) {
Label {
Text("mqtt")
} icon: {
Image(systemName: "dot.radiowaves.up.forward")
if isModuleSupported(.mqttConfig) {
NavigationLink(value: SettingsNavigationState.mqtt) {
Label {
Text("mqtt")
} icon: {
Image(systemName: "dot.radiowaves.up.forward")
}
}
}
NavigationLink(value: SettingsNavigationState.rangeTest) {
Label {
Text("range.test")
} icon: {
Image(systemName: "point.3.connected.trianglepath.dotted")
if isModuleSupported(.rangetestConfig) {
NavigationLink(value: SettingsNavigationState.rangeTest) {
Label {
Text("range.test")
} icon: {
Image(systemName: "point.3.connected.trianglepath.dotted")
}
}
}
if let node = nodes.first(where: { $0.num == preferredNodeNum }),
node.metadata?.hasWifi ?? false {
if isModuleSupported(.paxcounterConfig) {
NavigationLink(value: SettingsNavigationState.paxCounter) {
Label {
Text("config.module.paxcounter.settings")
@ -213,37 +236,61 @@ struct Settings: View {
}
}
NavigationLink(value: SettingsNavigationState.ringtone) {
Label {
Text("ringtone")
} icon: {
Image(systemName: "music.note.list")
if isModuleSupported(.audioConfig) {
NavigationLink(value: SettingsNavigationState.ringtone) {
Label {
Text("ringtone")
} icon: {
Image(systemName: "music.note.list")
}
}
}
NavigationLink(value: SettingsNavigationState.serial) {
Label {
Text("serial")
} icon: {
Image(systemName: "terminal")
if isModuleSupported(.serialConfig) {
NavigationLink(value: SettingsNavigationState.serial) {
Label {
Text("serial")
} icon: {
Image(systemName: "terminal")
}
}
}
NavigationLink(value: SettingsNavigationState.storeAndForward) {
Label {
Text("Store & Forward")
} icon: {
Image(systemName: "envelope.arrow.triangle.branch")
if isModuleSupported(.storeforwardConfig) {
NavigationLink(value: SettingsNavigationState.storeAndForward) {
Label {
Text("Store & Forward")
} icon: {
Image(systemName: "envelope.arrow.triangle.branch")
}
}
}
NavigationLink(value: SettingsNavigationState.telemetry) {
Label {
Text("telemetry")
} icon: {
Image(systemName: "chart.xyaxis.line")
if isModuleSupported(.telemetryConfig) {
NavigationLink(value: SettingsNavigationState.telemetry) {
Label {
Text("telemetry")
} icon: {
Image(systemName: "chart.xyaxis.line")
}
}
}
// Update this list with the modules that are shown above. If all are not supported
// Then show a message.
if !isAnySupported([.ambientlightingConfig, .cannedmsgConfig,
.detectionsensorConfig, .extnotifConfig,
.mqttConfig, .rangetestConfig, .paxcounterConfig,
.audioConfig, .serialConfig, .storeforwardConfig,
.telemetryConfig]) {
Text("This node does not support any configurable modules.")
}
} header: {
Text("module.configuration")
} footer: {
if moduleOverride {
Text("Currently showing modules that may not be supported by this node.")
}
}
}
@ -341,7 +388,7 @@ struct Settings: View {
/// Connected Node
if node.num == bleManager.connectedPeripheral?.num ?? 0 {
Label {
Text("BLE: \(node.user?.longName ?? "unknown".localized)")
Text("BLE: \(node.user?.longName?.addingVariationSelectors ?? "unknown".localized)")
} icon: {
Image(systemName: "antenna.radiowaves.left.and.right")
}
@ -363,14 +410,14 @@ struct Settings: View {
.tag(Int(node.num))
} else if UserDefaults.enableAdministration && node.user?.pkiEncrypted ?? false {
Label {
Text("Request PKI Admin: \(node.user?.longName ?? "unknown".localized)")
Text("Request PKI Admin: \(node.user?.longName?.addingVariationSelectors ?? "unknown".localized)")
} icon: {
Image(systemName: "rectangle.and.hand.point.up.left")
}
.tag(Int(node.num))
} else if !UserDefaults.enableAdministration {
Label {
Text("Request Legacy Admin: \(node.user?.longName ?? "unknown".localized)")
Text("Request Legacy Admin: \(node.user?.longName?.addingVariationSelectors ?? "unknown".localized)")
} icon: {
Image(systemName: "rectangle.and.hand.point.up.left")
}
@ -395,7 +442,7 @@ struct Settings: View {
TipView(AdminChannelTip(), arrowEdge: .top)
} else {
if bleManager.connectedPeripheral != nil {
Text("Connected Node \(node?.user?.longName ?? "unknown".localized)")
Text("Connected Node \(node?.user?.longName?.addingVariationSelectors ?? "unknown".localized)")
}
}
}
@ -497,7 +544,10 @@ struct Settings: View {
}
.navigationTitle("settings")
.navigationBarItems(
leading: MeshtasticLogo()
leading: MeshtasticLogo().onLongPressGesture(minimumDuration: 1.0) {
self.moduleOverride.toggle()
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
}
)
}
}

View file

@ -50,12 +50,14 @@ struct UserConfig: View {
TextField("Long Name", text: $longName)
.onChange(of: longName) {
var totalBytes = longName.utf8.count
var newValue = longName.withoutVariationSelectors
var totalBytes = newValue.utf8.count
// Only mess with the value if it is too big
while totalBytes > (isLicensed ? 6 : 36) {
longName = String(longName.dropLast())
totalBytes = longName.utf8.count
newValue = String(newValue.dropLast())
totalBytes = newValue.utf8.count
}
longName = newValue
}
}
.keyboardType(.default)

View file

@ -1,5 +1,6 @@
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: meshtastic/admin.proto
@ -24,7 +25,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP
/// This message is handled by the Admin module and is responsible for all settings/channel read/write operations.
/// This message is used to do settings operations to both remote AND local nodes.
/// (Prior to 1.2 these operations were done via special ToRadio operations)
public struct AdminMessage {
public struct AdminMessage: @unchecked Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -261,6 +262,36 @@ public struct AdminMessage {
set {payloadVariant = .setScale(newValue)}
}
///
/// Backup the node's preferences
public var backupPreferences: AdminMessage.BackupLocation {
get {
if case .backupPreferences(let v)? = payloadVariant {return v}
return .flash
}
set {payloadVariant = .backupPreferences(newValue)}
}
///
/// Restore the node's preferences
public var restorePreferences: AdminMessage.BackupLocation {
get {
if case .restorePreferences(let v)? = payloadVariant {return v}
return .flash
}
set {payloadVariant = .restorePreferences(newValue)}
}
///
/// Remove backups of the node's preferences
public var removeBackupPreferences: AdminMessage.BackupLocation {
get {
if case .removeBackupPreferences(let v)? = payloadVariant {return v}
return .flash
}
set {payloadVariant = .removeBackupPreferences(newValue)}
}
///
/// Set the owner for this node
public var setOwner: User {
@ -533,7 +564,7 @@ public struct AdminMessage {
///
/// TODO: REPLACE
public enum OneOf_PayloadVariant: Equatable {
public enum OneOf_PayloadVariant: Equatable, Sendable {
///
/// Send the specified channel in the response to this message
/// NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present)
@ -603,6 +634,15 @@ public struct AdminMessage {
/// Set zero and offset for scale chips
case setScale(UInt32)
///
/// Backup the node's preferences
case backupPreferences(AdminMessage.BackupLocation)
///
/// Restore the node's preferences
case restorePreferences(AdminMessage.BackupLocation)
///
/// Remove backups of the node's preferences
case removeBackupPreferences(AdminMessage.BackupLocation)
///
/// Set the owner for this node
case setOwner(User)
///
@ -689,213 +729,11 @@ public struct AdminMessage {
/// Tell the node to reset the nodedb.
case nodedbReset(Int32)
#if !swift(>=4.1)
public static func ==(lhs: AdminMessage.OneOf_PayloadVariant, rhs: AdminMessage.OneOf_PayloadVariant) -> Bool {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch (lhs, rhs) {
case (.getChannelRequest, .getChannelRequest): return {
guard case .getChannelRequest(let l) = lhs, case .getChannelRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getChannelResponse, .getChannelResponse): return {
guard case .getChannelResponse(let l) = lhs, case .getChannelResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getOwnerRequest, .getOwnerRequest): return {
guard case .getOwnerRequest(let l) = lhs, case .getOwnerRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getOwnerResponse, .getOwnerResponse): return {
guard case .getOwnerResponse(let l) = lhs, case .getOwnerResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getConfigRequest, .getConfigRequest): return {
guard case .getConfigRequest(let l) = lhs, case .getConfigRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getConfigResponse, .getConfigResponse): return {
guard case .getConfigResponse(let l) = lhs, case .getConfigResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getModuleConfigRequest, .getModuleConfigRequest): return {
guard case .getModuleConfigRequest(let l) = lhs, case .getModuleConfigRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getModuleConfigResponse, .getModuleConfigResponse): return {
guard case .getModuleConfigResponse(let l) = lhs, case .getModuleConfigResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getCannedMessageModuleMessagesRequest, .getCannedMessageModuleMessagesRequest): return {
guard case .getCannedMessageModuleMessagesRequest(let l) = lhs, case .getCannedMessageModuleMessagesRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getCannedMessageModuleMessagesResponse, .getCannedMessageModuleMessagesResponse): return {
guard case .getCannedMessageModuleMessagesResponse(let l) = lhs, case .getCannedMessageModuleMessagesResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getDeviceMetadataRequest, .getDeviceMetadataRequest): return {
guard case .getDeviceMetadataRequest(let l) = lhs, case .getDeviceMetadataRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getDeviceMetadataResponse, .getDeviceMetadataResponse): return {
guard case .getDeviceMetadataResponse(let l) = lhs, case .getDeviceMetadataResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getRingtoneRequest, .getRingtoneRequest): return {
guard case .getRingtoneRequest(let l) = lhs, case .getRingtoneRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getRingtoneResponse, .getRingtoneResponse): return {
guard case .getRingtoneResponse(let l) = lhs, case .getRingtoneResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getDeviceConnectionStatusRequest, .getDeviceConnectionStatusRequest): return {
guard case .getDeviceConnectionStatusRequest(let l) = lhs, case .getDeviceConnectionStatusRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getDeviceConnectionStatusResponse, .getDeviceConnectionStatusResponse): return {
guard case .getDeviceConnectionStatusResponse(let l) = lhs, case .getDeviceConnectionStatusResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setHamMode, .setHamMode): return {
guard case .setHamMode(let l) = lhs, case .setHamMode(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getNodeRemoteHardwarePinsRequest, .getNodeRemoteHardwarePinsRequest): return {
guard case .getNodeRemoteHardwarePinsRequest(let l) = lhs, case .getNodeRemoteHardwarePinsRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getNodeRemoteHardwarePinsResponse, .getNodeRemoteHardwarePinsResponse): return {
guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.enterDfuModeRequest, .enterDfuModeRequest): return {
guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.deleteFileRequest, .deleteFileRequest): return {
guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setScale, .setScale): return {
guard case .setScale(let l) = lhs, case .setScale(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setOwner, .setOwner): return {
guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setChannel, .setChannel): return {
guard case .setChannel(let l) = lhs, case .setChannel(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setConfig, .setConfig): return {
guard case .setConfig(let l) = lhs, case .setConfig(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setModuleConfig, .setModuleConfig): return {
guard case .setModuleConfig(let l) = lhs, case .setModuleConfig(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setCannedMessageModuleMessages, .setCannedMessageModuleMessages): return {
guard case .setCannedMessageModuleMessages(let l) = lhs, case .setCannedMessageModuleMessages(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setRingtoneMessage, .setRingtoneMessage): return {
guard case .setRingtoneMessage(let l) = lhs, case .setRingtoneMessage(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.removeByNodenum, .removeByNodenum): return {
guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setFavoriteNode, .setFavoriteNode): return {
guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.removeFavoriteNode, .removeFavoriteNode): return {
guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setFixedPosition, .setFixedPosition): return {
guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.removeFixedPosition, .removeFixedPosition): return {
guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setTimeOnly, .setTimeOnly): return {
guard case .setTimeOnly(let l) = lhs, case .setTimeOnly(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getUiConfigRequest, .getUiConfigRequest): return {
guard case .getUiConfigRequest(let l) = lhs, case .getUiConfigRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getUiConfigResponse, .getUiConfigResponse): return {
guard case .getUiConfigResponse(let l) = lhs, case .getUiConfigResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.storeUiConfig, .storeUiConfig): return {
guard case .storeUiConfig(let l) = lhs, case .storeUiConfig(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setIgnoredNode, .setIgnoredNode): return {
guard case .setIgnoredNode(let l) = lhs, case .setIgnoredNode(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.removeIgnoredNode, .removeIgnoredNode): return {
guard case .removeIgnoredNode(let l) = lhs, case .removeIgnoredNode(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.beginEditSettings, .beginEditSettings): return {
guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.commitEditSettings, .commitEditSettings): return {
guard case .commitEditSettings(let l) = lhs, case .commitEditSettings(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.factoryResetDevice, .factoryResetDevice): return {
guard case .factoryResetDevice(let l) = lhs, case .factoryResetDevice(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.rebootOtaSeconds, .rebootOtaSeconds): return {
guard case .rebootOtaSeconds(let l) = lhs, case .rebootOtaSeconds(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.exitSimulator, .exitSimulator): return {
guard case .exitSimulator(let l) = lhs, case .exitSimulator(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.rebootSeconds, .rebootSeconds): return {
guard case .rebootSeconds(let l) = lhs, case .rebootSeconds(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.shutdownSeconds, .shutdownSeconds): return {
guard case .shutdownSeconds(let l) = lhs, case .shutdownSeconds(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.factoryResetConfig, .factoryResetConfig): return {
guard case .factoryResetConfig(let l) = lhs, case .factoryResetConfig(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.nodedbReset, .nodedbReset): return {
guard case .nodedbReset(let l) = lhs, case .nodedbReset(let r) = rhs else { preconditionFailure() }
return l == r
}()
default: return false
}
}
#endif
}
///
/// TODO: REPLACE
public enum ConfigType: SwiftProtobuf.Enum {
public enum ConfigType: SwiftProtobuf.Enum, Swift.CaseIterable {
public typealias RawValue = Int
///
@ -929,6 +767,9 @@ public struct AdminMessage {
///
/// TODO: REPLACE
case securityConfig // = 7
///
/// Session key config
case sessionkeyConfig // = 8
///
@ -972,11 +813,25 @@ public struct AdminMessage {
}
}
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [AdminMessage.ConfigType] = [
.deviceConfig,
.positionConfig,
.powerConfig,
.networkConfig,
.displayConfig,
.loraConfig,
.bluetoothConfig,
.securityConfig,
.sessionkeyConfig,
.deviceuiConfig,
]
}
///
/// TODO: REPLACE
public enum ModuleConfigType: SwiftProtobuf.Enum {
public enum ModuleConfigType: SwiftProtobuf.Enum, Swift.CaseIterable {
public typealias RawValue = Int
///
@ -1074,53 +929,71 @@ public struct AdminMessage {
}
}
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [AdminMessage.ModuleConfigType] = [
.mqttConfig,
.serialConfig,
.extnotifConfig,
.storeforwardConfig,
.rangetestConfig,
.telemetryConfig,
.cannedmsgConfig,
.audioConfig,
.remotehardwareConfig,
.neighborinfoConfig,
.ambientlightingConfig,
.detectionsensorConfig,
.paxcounterConfig,
]
}
public enum BackupLocation: SwiftProtobuf.Enum, Swift.CaseIterable {
public typealias RawValue = Int
///
/// Backup to the internal flash
case flash // = 0
///
/// Backup to the SD card
case sd // = 1
case UNRECOGNIZED(Int)
public init() {
self = .flash
}
public init?(rawValue: Int) {
switch rawValue {
case 0: self = .flash
case 1: self = .sd
default: self = .UNRECOGNIZED(rawValue)
}
}
public var rawValue: Int {
switch self {
case .flash: return 0
case .sd: return 1
case .UNRECOGNIZED(let i): return i
}
}
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [AdminMessage.BackupLocation] = [
.flash,
.sd,
]
}
public init() {}
}
#if swift(>=4.2)
extension AdminMessage.ConfigType: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [AdminMessage.ConfigType] = [
.deviceConfig,
.positionConfig,
.powerConfig,
.networkConfig,
.displayConfig,
.loraConfig,
.bluetoothConfig,
.securityConfig,
.sessionkeyConfig,
.deviceuiConfig,
]
}
extension AdminMessage.ModuleConfigType: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [AdminMessage.ModuleConfigType] = [
.mqttConfig,
.serialConfig,
.extnotifConfig,
.storeforwardConfig,
.rangetestConfig,
.telemetryConfig,
.cannedmsgConfig,
.audioConfig,
.remotehardwareConfig,
.neighborinfoConfig,
.ambientlightingConfig,
.detectionsensorConfig,
.paxcounterConfig,
]
}
#endif // swift(>=4.2)
///
/// Parameters for setting up Meshtastic for ameteur radio usage
public struct HamParameters {
public struct HamParameters: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -1150,7 +1023,7 @@ public struct HamParameters {
///
/// Response envelope for node_remote_hardware_pins
public struct NodeRemoteHardwarePinsResponse {
public struct NodeRemoteHardwarePinsResponse: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -1164,15 +1037,6 @@ public struct NodeRemoteHardwarePinsResponse {
public init() {}
}
#if swift(>=5.5) && canImport(_Concurrency)
extension AdminMessage: @unchecked Sendable {}
extension AdminMessage.OneOf_PayloadVariant: @unchecked Sendable {}
extension AdminMessage.ConfigType: @unchecked Sendable {}
extension AdminMessage.ModuleConfigType: @unchecked Sendable {}
extension HamParameters: @unchecked Sendable {}
extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "meshtastic"
@ -1203,6 +1067,9 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
21: .standard(proto: "enter_dfu_mode_request"),
22: .standard(proto: "delete_file_request"),
23: .standard(proto: "set_scale"),
24: .standard(proto: "backup_preferences"),
25: .standard(proto: "restore_preferences"),
26: .standard(proto: "remove_backup_preferences"),
32: .standard(proto: "set_owner"),
33: .standard(proto: "set_channel"),
34: .standard(proto: "set_config"),
@ -1453,6 +1320,30 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
self.payloadVariant = .setScale(v)
}
}()
case 24: try {
var v: AdminMessage.BackupLocation?
try decoder.decodeSingularEnumField(value: &v)
if let v = v {
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
self.payloadVariant = .backupPreferences(v)
}
}()
case 25: try {
var v: AdminMessage.BackupLocation?
try decoder.decodeSingularEnumField(value: &v)
if let v = v {
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
self.payloadVariant = .restorePreferences(v)
}
}()
case 26: try {
var v: AdminMessage.BackupLocation?
try decoder.decodeSingularEnumField(value: &v)
if let v = v {
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
self.payloadVariant = .removeBackupPreferences(v)
}
}()
case 32: try {
var v: User?
var hadOneofValue = false
@ -1796,6 +1687,18 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .setScale(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 23)
}()
case .backupPreferences?: try {
guard case .backupPreferences(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularEnumField(value: v, fieldNumber: 24)
}()
case .restorePreferences?: try {
guard case .restorePreferences(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularEnumField(value: v, fieldNumber: 25)
}()
case .removeBackupPreferences?: try {
guard case .removeBackupPreferences(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularEnumField(value: v, fieldNumber: 26)
}()
case .setOwner?: try {
guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 32)
@ -1949,6 +1852,13 @@ extension AdminMessage.ModuleConfigType: SwiftProtobuf._ProtoNameProviding {
]
}
extension AdminMessage.BackupLocation: SwiftProtobuf._ProtoNameProviding {
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "FLASH"),
1: .same(proto: "SD"),
]
}
extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".HamParameters"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
@ -1980,7 +1890,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa
if self.txPower != 0 {
try visitor.visitSingularInt32Field(value: self.txPower, fieldNumber: 2)
}
if self.frequency != 0 {
if self.frequency.bitPattern != 0 {
try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3)
}
if !self.shortName.isEmpty {

View file

@ -1,5 +1,6 @@
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: meshtastic/apponly.proto
@ -7,7 +8,6 @@
// For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/
import Foundation
import SwiftProtobuf
// If the compiler emits an error on this type, it is because this file
@ -26,7 +26,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP
/// any SECONDARY channels.
/// No DISABLED channels are included.
/// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL
public struct ChannelSet {
public struct ChannelSet: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -53,10 +53,6 @@ public struct ChannelSet {
fileprivate var _loraConfig: Config.LoRaConfig? = nil
}
#if swift(>=5.5) && canImport(_Concurrency)
extension ChannelSet: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "meshtastic"

View file

@ -1,5 +1,6 @@
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: meshtastic/atak.proto
@ -20,7 +21,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP
typealias Version = _2
}
public enum Team: SwiftProtobuf.Enum {
public enum Team: SwiftProtobuf.Enum, Swift.CaseIterable {
public typealias RawValue = Int
///
@ -130,11 +131,6 @@ public enum Team: SwiftProtobuf.Enum {
}
}
}
#if swift(>=4.2)
extension Team: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [Team] = [
.unspecifedColor,
@ -153,13 +149,12 @@ extension Team: CaseIterable {
.darkGreen,
.brown,
]
}
#endif // swift(>=4.2)
}
///
/// Role of the group member
public enum MemberRole: SwiftProtobuf.Enum {
public enum MemberRole: SwiftProtobuf.Enum, Swift.CaseIterable {
public typealias RawValue = Int
///
@ -233,11 +228,6 @@ public enum MemberRole: SwiftProtobuf.Enum {
}
}
}
#if swift(>=4.2)
extension MemberRole: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [MemberRole] = [
.unspecifed,
@ -250,13 +240,12 @@ extension MemberRole: CaseIterable {
.rto,
.k9,
]
}
#endif // swift(>=4.2)
}
///
/// Packets for the official ATAK Plugin
public struct TAKPacket {
public struct TAKPacket: @unchecked Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -337,7 +326,7 @@ public struct TAKPacket {
///
/// The payload of the packet
public enum OneOf_PayloadVariant: Equatable {
public enum OneOf_PayloadVariant: Equatable, @unchecked Sendable {
///
/// TAK position report
case pli(PLI)
@ -349,28 +338,6 @@ public struct TAKPacket {
/// May be compressed / truncated by the sender (EUD)
case detail(Data)
#if !swift(>=4.1)
public static func ==(lhs: TAKPacket.OneOf_PayloadVariant, rhs: TAKPacket.OneOf_PayloadVariant) -> Bool {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch (lhs, rhs) {
case (.pli, .pli): return {
guard case .pli(let l) = lhs, case .pli(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.chat, .chat): return {
guard case .chat(let l) = lhs, case .chat(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.detail, .detail): return {
guard case .detail(let l) = lhs, case .detail(let r) = rhs else { preconditionFailure() }
return l == r
}()
default: return false
}
}
#endif
}
public init() {}
@ -382,7 +349,7 @@ public struct TAKPacket {
///
/// ATAK GeoChat message
public struct GeoChat {
public struct GeoChat: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -424,7 +391,7 @@ public struct GeoChat {
///
/// ATAK Group
/// <__group role='Team Member' name='Cyan'/>
public struct Group {
public struct Group: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -446,7 +413,7 @@ public struct Group {
///
/// ATAK EUD Status
/// <status battery='100' />
public struct Status {
public struct Status: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -463,7 +430,7 @@ public struct Status {
///
/// ATAK Contact
/// <contact endpoint='0.0.0.0:4242:tcp' phone='+12345678' callsign='FALKE'/>
public struct Contact {
public struct Contact: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -483,7 +450,7 @@ public struct Contact {
///
/// Position Location Information from ATAK
public struct PLI {
public struct PLI: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -515,18 +482,6 @@ public struct PLI {
public init() {}
}
#if swift(>=5.5) && canImport(_Concurrency)
extension Team: @unchecked Sendable {}
extension MemberRole: @unchecked Sendable {}
extension TAKPacket: @unchecked Sendable {}
extension TAKPacket.OneOf_PayloadVariant: @unchecked Sendable {}
extension GeoChat: @unchecked Sendable {}
extension Group: @unchecked Sendable {}
extension Status: @unchecked Sendable {}
extension Contact: @unchecked Sendable {}
extension PLI: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "meshtastic"

View file

@ -1,5 +1,6 @@
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: meshtastic/cannedmessages.proto
@ -7,7 +8,6 @@
// For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/
import Foundation
import SwiftProtobuf
// If the compiler emits an error on this type, it is because this file
@ -22,7 +22,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP
///
/// Canned message module configuration.
public struct CannedMessageModuleConfig {
public struct CannedMessageModuleConfig: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -36,10 +36,6 @@ public struct CannedMessageModuleConfig {
public init() {}
}
#if swift(>=5.5) && canImport(_Concurrency)
extension CannedMessageModuleConfig: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "meshtastic"

View file

@ -1,5 +1,6 @@
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: meshtastic/channel.proto
@ -36,13 +37,15 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP
/// FIXME: Add description of multi-channel support and how primary vs secondary channels are used.
/// FIXME: explain how apps use channels for security.
/// explain how remote settings and remote gpio are managed as an example
public struct ChannelSettings {
public struct ChannelSettings: @unchecked Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
///
/// Deprecated in favor of LoraConfig.channel_num
///
/// NOTE: This field was marked as deprecated in the .proto file.
public var channelNum: UInt32 = 0
///
@ -111,7 +114,7 @@ public struct ChannelSettings {
///
/// This message is specifically for modules to store per-channel configuration data.
public struct ModuleSettings {
public struct ModuleSettings: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -132,7 +135,7 @@ public struct ModuleSettings {
///
/// A pair of a channel number, mode and the (sharable) settings for that channel
public struct Channel {
public struct Channel: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
@ -170,7 +173,7 @@ public struct Channel {
/// cross band routing as needed.
/// If a device has only a single radio (the common case) only one channel can be PRIMARY at a time
/// (but any number of SECONDARY channels can't be sent received on that common frequency)
public enum Role: SwiftProtobuf.Enum {
public enum Role: SwiftProtobuf.Enum, Swift.CaseIterable {
public typealias RawValue = Int
///
@ -209,6 +212,13 @@ public struct Channel {
}
}
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [Channel.Role] = [
.disabled,
.primary,
.secondary,
]
}
public init() {}
@ -216,26 +226,6 @@ public struct Channel {
fileprivate var _settings: ChannelSettings? = nil
}
#if swift(>=4.2)
extension Channel.Role: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [Channel.Role] = [
.disabled,
.primary,
.secondary,
]
}
#endif // swift(>=4.2)
#if swift(>=5.5) && canImport(_Concurrency)
extension ChannelSettings: @unchecked Sendable {}
extension ModuleSettings: @unchecked Sendable {}
extension Channel: @unchecked Sendable {}
extension Channel.Role: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "meshtastic"

Some files were not shown because too many files have changed in this diff Show more