mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Settings rework - new async location handler
This commit is contained in:
parent
fb66e3b250
commit
126cdfbdb3
20 changed files with 1193 additions and 545 deletions
|
|
@ -29,6 +29,7 @@
|
|||
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; };
|
||||
DD2DC2C029BCD8AB003B383C /* HardwareModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */; };
|
||||
DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; };
|
||||
DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */; };
|
||||
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; };
|
||||
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */; };
|
||||
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */; };
|
||||
|
|
@ -172,6 +173,7 @@
|
|||
DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; };
|
||||
DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; };
|
||||
DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; };
|
||||
DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */; };
|
||||
DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; };
|
||||
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; };
|
||||
DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; };
|
||||
|
|
@ -240,6 +242,8 @@
|
|||
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = "<group>"; };
|
||||
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||
DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV21.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsHandler.swift; sourceTree = "<group>"; };
|
||||
DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareChannels.swift; sourceTree = "<group>"; };
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModel.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryGauge.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -403,6 +407,7 @@
|
|||
DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = "<group>"; };
|
||||
DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRecorder.swift; sourceTree = "<group>"; };
|
||||
DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForward.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -531,6 +536,7 @@
|
|||
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */,
|
||||
DD4A911D2708C65400501B7E /* AppSettings.swift */,
|
||||
DDAB580C2B0DAA9E00147258 /* Routes.swift */,
|
||||
DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */,
|
||||
DDA0B6B1294CDC55001356EC /* Channels.swift */,
|
||||
DDD6EEAE29BC024700383354 /* Firmware.swift */,
|
||||
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */,
|
||||
|
|
@ -836,6 +842,7 @@
|
|||
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */,
|
||||
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */,
|
||||
DDDB443C29F6592F00EE2349 /* NetworkManager.swift */,
|
||||
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1149,6 +1156,7 @@
|
|||
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,
|
||||
DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */,
|
||||
DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */,
|
||||
DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */,
|
||||
DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */,
|
||||
DDDB26482AACD6D1003AFCB7 /* NodeMapMapkit.swift in Sources */,
|
||||
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
|
||||
|
|
@ -1163,6 +1171,7 @@
|
|||
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */,
|
||||
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */,
|
||||
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */,
|
||||
DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */,
|
||||
DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */,
|
||||
DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */,
|
||||
DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */,
|
||||
|
|
@ -1764,6 +1773,7 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */,
|
||||
DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */,
|
||||
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */,
|
||||
DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */,
|
||||
|
|
@ -1785,7 +1795,7 @@
|
|||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */;
|
||||
currentVersion = DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */;
|
||||
name = Meshtastic.xcdatamodeld;
|
||||
path = Meshtastic/Meshtastic.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
var context: NSManagedObjectContext?
|
||||
//var userSettings: UserSettings?
|
||||
private var centralManager: CBCentralManager!
|
||||
private let restoreKey = "Meshtastic.BLE.Manager"
|
||||
@Published var peripherals: [Peripheral] = []
|
||||
@Published var connectedPeripheral: Peripheral!
|
||||
@Published var lastConnectionError: String
|
||||
|
|
@ -874,25 +873,48 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool {
|
||||
var success = false
|
||||
let fromNodeNum = connectedPeripheral.num
|
||||
if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 {
|
||||
return false
|
||||
}
|
||||
var positionPacket = Position()
|
||||
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
|
||||
let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date()
|
||||
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0)
|
||||
positionPacket.satsInView = UInt32(LocationHelper.satsInView)
|
||||
let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0
|
||||
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
|
||||
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
|
||||
}
|
||||
let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0
|
||||
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
if fromNodeNum <= 0 {
|
||||
return false
|
||||
}
|
||||
positionPacket.latitudeI = Int32(LocationsHandler.shared.lastLocation.coordinate.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(LocationsHandler.shared.lastLocation.coordinate.longitude * 1e7)
|
||||
let timestamp = LocationsHandler.shared.lastLocation.timestamp
|
||||
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.altitude = Int32(LocationsHandler.shared.lastLocation.altitude)
|
||||
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
|
||||
let currentSpeed = LocationsHandler.shared.lastLocation.speed
|
||||
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
|
||||
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
|
||||
}
|
||||
let currentHeading = LocationsHandler.shared.lastLocation.course
|
||||
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
} else {
|
||||
if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 {
|
||||
return false
|
||||
}
|
||||
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
|
||||
let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date()
|
||||
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0)
|
||||
positionPacket.satsInView = UInt32(LocationHelper.satsInView)
|
||||
let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0
|
||||
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
|
||||
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
|
||||
}
|
||||
let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0
|
||||
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
}
|
||||
|
||||
var meshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(destNum)
|
||||
meshPacket.from = UInt32(fromNodeNum)
|
||||
|
|
@ -2305,29 +2327,4 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
let visibleDuration = Calendar.current.date(byAdding: .second, value: -5, to: today)!
|
||||
self.peripherals.removeAll(where: { $0.lastUpdate < visibleDuration})
|
||||
}
|
||||
|
||||
// func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
|
||||
//
|
||||
// guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if peripherals.count > 0 {
|
||||
//
|
||||
// for peripheral in peripherals {
|
||||
// print(peripheral)
|
||||
// switch peripheral.state {
|
||||
// case .connecting: // I've only seen this happen when
|
||||
// // re-launching attached to Xcode.
|
||||
// print("Xcode Restore")
|
||||
//
|
||||
// case .connected:
|
||||
// connectTo(peripheral: peripheral)
|
||||
// print("Restore BLE State")
|
||||
// default: break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// print("willRestoreState Hit!")
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
96
Meshtastic/Helpers/LocationsHandler.swift
Normal file
96
Meshtastic/Helpers/LocationsHandler.swift
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// LocationsHandler.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 12/4/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
|
||||
// Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`.
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
@MainActor class LocationsHandler: ObservableObject {
|
||||
|
||||
static let shared = LocationsHandler() // Create a single, shared instance of the object.
|
||||
private let manager: CLLocationManager
|
||||
private var background: CLBackgroundActivitySession?
|
||||
|
||||
@Published var lastLocation = CLLocation()
|
||||
@Published var isStationary = false
|
||||
@Published var count = 0
|
||||
|
||||
@Published
|
||||
var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") {
|
||||
didSet { UserDefaults.standard.set(updatesStarted, forKey: "liveUpdatesStarted") }
|
||||
}
|
||||
|
||||
@Published
|
||||
var backgroundActivity: Bool = UserDefaults.standard.bool(forKey: "BGActivitySessionStarted") {
|
||||
didSet {
|
||||
backgroundActivity ? self.background = CLBackgroundActivitySession() : self.background?.invalidate()
|
||||
UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted")
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`.
|
||||
}
|
||||
|
||||
func startLocationUpdates() {
|
||||
if self.manager.authorizationStatus == .notDetermined {
|
||||
self.manager.requestWhenInUseAuthorization()
|
||||
}
|
||||
print("Starting location updates")
|
||||
Task() {
|
||||
do {
|
||||
self.updatesStarted = true
|
||||
let updates = CLLocationUpdate.liveUpdates()
|
||||
for try await update in updates {
|
||||
if !self.updatesStarted { break } // End location updates by breaking out of the loop.
|
||||
if let loc = update.location {
|
||||
self.lastLocation = loc
|
||||
self.isStationary = update.isStationary
|
||||
self.count += 1
|
||||
//print("Location \(self.count): \(self.lastLocation)")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Could not start location updates")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func stopLocationUpdates() {
|
||||
print("Stopping location updates")
|
||||
self.updatesStarted = false
|
||||
}
|
||||
|
||||
static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090)
|
||||
|
||||
static var satsInView: Int {
|
||||
// If we have a position we have a sat
|
||||
var sats = 1
|
||||
if shared.lastLocation.verticalAccuracy > 0 {
|
||||
sats = 4
|
||||
if 0...5 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 12
|
||||
} else if 6...15 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 10
|
||||
} else if 16...30 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 9
|
||||
} else if 31...45 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 7
|
||||
} else if 46...60 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 5
|
||||
}
|
||||
} else if shared.lastLocation.verticalAccuracy < 0 && 60...300 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 3
|
||||
} else if shared.lastLocation.verticalAccuracy < 0 && shared.lastLocation.horizontalAccuracy > 300 {
|
||||
sats = 2
|
||||
}
|
||||
return sats
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV20.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV21.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23C5047e" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23C5055b" 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"/>
|
||||
|
|
@ -116,10 +116,12 @@
|
|||
<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="horizontalAccuracy" optional="YES" attributeType="Double" defaultValueString="0.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"/>
|
||||
<attribute name="verticalAccuracy" optional="YES" attributeType="Double" defaultValueString="0.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">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,378 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23C5047e" 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="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" optional="YES" attributeType="Boolean" 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="psk" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
|
||||
<fetchedProperty name="allPrivateMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="channel == $FETCH_SOURCE.index && toUser == nil AND isEmoji == false"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="detectionTriggeredHigh" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<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="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="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"/>
|
||||
<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="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"/>
|
||||
<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="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="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<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="receivedTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" 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"/>
|
||||
<fetchedProperty name="tapbacks" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="messageId"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<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="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="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"/>
|
||||
<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"/>
|
||||
<fetchedProperty name="allMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="toUser == nil"/>
|
||||
</fetchedProperty>
|
||||
<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="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="detection" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="environment" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" 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="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
|
||||
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
|
||||
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
|
||||
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Nullify" 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="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="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="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="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="num"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</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="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="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="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="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<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="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="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="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"/>
|
||||
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
|
||||
<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="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="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="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="voltage" 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="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hwModel" attributeType="String"/>
|
||||
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" 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="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="shortName" attributeType="String"/>
|
||||
<attribute name="userId" attributeType="String"/>
|
||||
<attribute name="vip" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<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"/>
|
||||
<fetchedProperty name="adminMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND isEmoji == false AND admin = true"/>
|
||||
</fetchedProperty>
|
||||
<fetchedProperty name="allMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10 "/>
|
||||
</fetchedProperty>
|
||||
<fetchedProperty name="detectionSensorMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND portNum = 10"/>
|
||||
</fetchedProperty>
|
||||
</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"/>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
print("🚀 Meshtstic Apple App launched!")
|
||||
// Default User Default Values
|
||||
|
|
@ -16,6 +16,18 @@ class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotification
|
|||
UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory" : true])
|
||||
UserDefaults.standard.register(defaults: ["meshMapShowRouteLines" : true])
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
let locationsHandler = LocationsHandler.shared
|
||||
|
||||
// If location updates were previously active, restart them after the background launch.
|
||||
if locationsHandler.updatesStarted {
|
||||
locationsHandler.startLocationUpdates()
|
||||
}
|
||||
// If a background activity session was previously active, reinstantiate it after the background launch.
|
||||
if locationsHandler.backgroundActivity {
|
||||
locationsHandler.backgroundActivity = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ struct NodeMapSwiftUI: View {
|
|||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
var mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationHelper.DefaultLocation
|
||||
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
})
|
||||
|
||||
if node.hasPositions {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct PositionPopover: View {
|
||||
@ObservedObject var locationsHandler = LocationsHandler.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
|
@ -132,8 +134,8 @@ struct PositionPopover: View {
|
|||
.padding(.bottom, 5)
|
||||
|
||||
/// Distance
|
||||
if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 {
|
||||
let metersAway = position.coordinate.distance(from: LocationHelper.currentLocation)
|
||||
if locationsHandler.lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
|
||||
let metersAway = position.coordinate.distance(from:CLLocationCoordinate2D(latitude: locationsHandler.lastLocation.coordinate.latitude, longitude: locationsHandler.lastLocation.coordinate.longitude))
|
||||
Label {
|
||||
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ struct CannedMessagesConfig: View {
|
|||
@State var messages = ""
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
|
|
|
|||
|
|
@ -41,150 +41,151 @@ struct DetectionSensorConfig: View {
|
|||
@State var monitorPin = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.detectionSensorConfig == nil {
|
||||
Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
if enabled {
|
||||
HStack {
|
||||
Picker(selection: $role, label: Text("Role")) {
|
||||
ForEach(DetectionSensorRole.allCases, id: \.self) { r in
|
||||
Text(r.description)
|
||||
.tag(r)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
if enabled && role == .client {
|
||||
Section(header: Text("Client options")) {
|
||||
Toggle(isOn: $detectionNotificationsEnabled) {
|
||||
Label("Enable Notifications", systemImage: "bell.badge")
|
||||
Text("Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge.")
|
||||
.font(.caption)
|
||||
}
|
||||
.listRowSeparator(.visible)
|
||||
}
|
||||
}
|
||||
if enabled && role == .sensor {
|
||||
Section(header: Text("Sensor options")) {
|
||||
Toggle(isOn: $sendBell) {
|
||||
Label("Send Bell", systemImage: "bell")
|
||||
Text("Send ASCII bell with alert message. Useful for triggering external notification on bell.")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
HStack {
|
||||
Label("Name", systemImage: "signature")
|
||||
TextField("Friendly name", text: $name, axis: .vertical)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: name, perform: { _ in
|
||||
|
||||
let totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 20 {
|
||||
|
||||
let firstNBytes = Data(name.utf8.prefix(20))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
name = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.listRowSeparator(.visible)
|
||||
.offset(y: -10)
|
||||
Picker("GPIO Pin to monitor", selection: $monitorPin) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Toggle(isOn: $detectionTriggeredHigh) {
|
||||
Label("Detection trigger High", systemImage: "dial.high")
|
||||
Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $usePullup) {
|
||||
Label("Uses pullup resistor", systemImage: "arrow.up.to.line")
|
||||
Text(" Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin")
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.detectionSensorConfig == nil {
|
||||
Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
if enabled {
|
||||
HStack {
|
||||
Picker(selection: $role, label: Text("Role")) {
|
||||
ForEach(DetectionSensorRole.allCases, id: \.self) { r in
|
||||
Text(r.description)
|
||||
.tag(r)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(header: Text("update.interval")) {
|
||||
Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
if enabled && role == .client {
|
||||
Section(header: Text("Client options")) {
|
||||
Toggle(isOn: $detectionNotificationsEnabled) {
|
||||
Label("Enable Notifications", systemImage: "bell.badge")
|
||||
Text("Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Mininum time between detection broadcasts. Default is 45 seconds.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.listRowSeparator(.visible)
|
||||
Picker("State Broadcast Interval", selection: $stateBroadcastSecs) {
|
||||
Text("Never").tag(0)
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
if enabled && role == .sensor {
|
||||
Section(header: Text("Sensor options")) {
|
||||
Toggle(isOn: $sendBell) {
|
||||
Label("Send Bell", systemImage: "bell")
|
||||
Text("Send ASCII bell with alert message. Useful for triggering external notification on bell.")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
HStack {
|
||||
Label("Name", systemImage: "signature")
|
||||
TextField("Friendly name", text: $name, axis: .vertical)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: name, perform: { _ in
|
||||
|
||||
let totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 20 {
|
||||
|
||||
let firstNBytes = Data(name.utf8.prefix(20))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
name = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.listRowSeparator(.visible)
|
||||
.offset(y: -10)
|
||||
Picker("GPIO Pin to monitor", selection: $monitorPin) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Toggle(isOn: $detectionTriggeredHigh) {
|
||||
Label("Detection trigger High", systemImage: "dial.high")
|
||||
Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $usePullup) {
|
||||
Label("Uses pullup resistor", systemImage: "arrow.up.to.line")
|
||||
Text(" Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
Section(header: Text("update.interval")) {
|
||||
Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Mininum time between detection broadcasts. Default is 45 seconds.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.listRowSeparator(.visible)
|
||||
Picker("State Broadcast Interval", selection: $stateBroadcastSecs) {
|
||||
Text("Never").tag(0)
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,137 +32,138 @@ struct ExternalNotificationConfig: View {
|
|||
@State var nagTimeout = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.externalNotificationConfig == nil {
|
||||
Text("External notification config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.externalNotificationConfig == nil {
|
||||
Text("External notification config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setExternalNotificationValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setExternalNotificationValues()
|
||||
}
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "megaphone")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertBell) {
|
||||
Label("Alert when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessage) {
|
||||
Label("Alert when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $usePWM) {
|
||||
Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.")
|
||||
.font(.caption)
|
||||
}
|
||||
Section(header: Text("Advanced GPIO Options")) {
|
||||
Section(header: Text("Primary GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $active) {
|
||||
Label("Active", systemImage: "togglepower")
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "megaphone")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.")
|
||||
.font(.caption)
|
||||
Picker("Output pin GPIO", selection: $output) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
Toggle(isOn: $alertBell) {
|
||||
Label("Alert when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("GPIO Output Duration", selection: $outputMilliseconds ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessage) {
|
||||
Label("Alert when receiving a message", systemImage: "message")
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("When using in GPIO mode, keep the output on for this long. ")
|
||||
.font(.caption)
|
||||
Picker("Nag timeout", selection: $nagTimeout ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $usePWM) {
|
||||
Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill")
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("Specifies how long the monitored GPIO should output.")
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
Section(header: Text("Optional GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $alertBellBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertBellVibra) {
|
||||
Label("Alert GPIO vibra motor when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO vibra motor when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
Section(header: Text("Advanced GPIO Options")) {
|
||||
Section(header: Text("Primary GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $active) {
|
||||
Label("Active", systemImage: "togglepower")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("Output pin vibra GPIO", selection: $outputVibra) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.")
|
||||
.font(.caption)
|
||||
Picker("Output pin GPIO", selection: $output) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("GPIO Output Duration", selection: $outputMilliseconds ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("When using in GPIO mode, keep the output on for this long. ")
|
||||
.font(.caption)
|
||||
Picker("Nag timeout", selection: $nagTimeout ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("Specifies how long the monitored GPIO should output.")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
Section(header: Text("Optional GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $alertBellBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertBellVibra) {
|
||||
Label("Alert GPIO vibra motor when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO vibra motor when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("Output pin vibra GPIO", selection: $outputVibra) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.externalNotificationConfig == nil)
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.externalNotificationConfig == nil)
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -25,176 +25,177 @@ struct MQTTConfig: View {
|
|||
@State var root = "msh"
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.mqttConfig == nil {
|
||||
Text("MQTT config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.mqttConfig == nil {
|
||||
Text("MQTT config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setMqttValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setMqttValues()
|
||||
}
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $proxyToClientEnabled) {
|
||||
|
||||
Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If both MQTT and the client proxy are enabled your mobile device will utalize an available network connection to connect to the specified MQTT server.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $encryptionEnabled) {
|
||||
|
||||
Label("Encryption Enabled", systemImage: "lock.icloud")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $jsonEnabled) {
|
||||
|
||||
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("JSON mode is a limited, unencrypted MQTT output.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $tlsEnabled) {
|
||||
|
||||
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Your MQTT Server must support TLS.")
|
||||
.font(.caption2)
|
||||
}
|
||||
Section(header: Text("Custom Server")) {
|
||||
HStack {
|
||||
Label("Address", systemImage: "server.rack")
|
||||
TextField("Server Address", text: $address)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: address, perform: { _ in
|
||||
let totalBytes = address.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
address = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
.keyboardType(.default)
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
|
||||
HStack {
|
||||
Label("mqtt.username", systemImage: "person.text.rectangle")
|
||||
TextField("mqtt.username", text: $username)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: username, perform: { _ in
|
||||
|
||||
let totalBytes = username.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
username = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("password", systemImage: "wallet.pass")
|
||||
TextField("password", text: $password)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: password, perform: { _ in
|
||||
|
||||
let totalBytes = password.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(password.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
password = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("Root Topic", systemImage: "tree")
|
||||
TextField("Root Topic", text: $root)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: root, perform: { _ in
|
||||
let totalBytes = root.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 14 {
|
||||
let firstNBytes = Data(root.utf8.prefix(14))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
root = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.asciiCapable)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disableAutocorrection(true)
|
||||
Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs")
|
||||
.font(.caption2)
|
||||
}
|
||||
Text("You can set uplink and downlink for each channel.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $proxyToClientEnabled) {
|
||||
|
||||
Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If both MQTT and the client proxy are enabled your mobile device will utalize an available network connection to connect to the specified MQTT server.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $encryptionEnabled) {
|
||||
|
||||
Label("Encryption Enabled", systemImage: "lock.icloud")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $jsonEnabled) {
|
||||
|
||||
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("JSON mode is a limited, unencrypted MQTT output.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $tlsEnabled) {
|
||||
|
||||
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Your MQTT Server must support TLS.")
|
||||
.font(.caption2)
|
||||
}
|
||||
Section(header: Text("Custom Server")) {
|
||||
HStack {
|
||||
Label("Address", systemImage: "server.rack")
|
||||
TextField("Server Address", text: $address)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: address, perform: { _ in
|
||||
let totalBytes = address.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
address = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
.keyboardType(.default)
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
|
||||
HStack {
|
||||
Label("mqtt.username", systemImage: "person.text.rectangle")
|
||||
TextField("mqtt.username", text: $username)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: username, perform: { _ in
|
||||
|
||||
let totalBytes = username.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
username = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("password", systemImage: "wallet.pass")
|
||||
TextField("password", text: $password)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: password, perform: { _ in
|
||||
|
||||
let totalBytes = password.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(password.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
password = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("Root Topic", systemImage: "tree")
|
||||
TextField("Root Topic", text: $root)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: root, perform: { _ in
|
||||
let totalBytes = root.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 14 {
|
||||
let firstNBytes = Data(root.utf8.prefix(14))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
root = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.asciiCapable)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disableAutocorrection(true)
|
||||
Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs")
|
||||
.font(.caption2)
|
||||
}
|
||||
Text("You can set uplink and downlink for each channel.")
|
||||
.font(.callout)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.mqttConfig == nil)
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.mqttConfig == nil)
|
||||
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ struct SerialConfig: View {
|
|||
@State var mode = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
|
|
|
|||
|
|
@ -27,72 +27,72 @@ struct StoreForwardConfig: View {
|
|||
@State var historyReturnWindow = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.storeForwardConfig == nil {
|
||||
Text("Store and forward config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.storeForwardConfig == nil {
|
||||
Text("Store and forward config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
|
||||
}
|
||||
Toggle(isOn: $heartbeat) {
|
||||
Label("storeforward.heartbeat", systemImage: "waveform.path.ecg")
|
||||
}
|
||||
Picker("Number of records", selection: $records) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Max", selection: $historyReturnMax ) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Window", selection: $historyReturnWindow ) {
|
||||
Text("unset").tag(0)
|
||||
Text("One Minute").tag(60)
|
||||
Text("Five Minutes").tag(300)
|
||||
Text("Ten Minutes").tag(600)
|
||||
Text("Fifteen Minutes").tag(900)
|
||||
Text("Thirty Minutes").tag(1800)
|
||||
Text("One Hour").tag(3600)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
|
||||
}
|
||||
Toggle(isOn: $heartbeat) {
|
||||
Label("storeforward.heartbeat", systemImage: "waveform.path.ecg")
|
||||
}
|
||||
Picker("Number of records", selection: $records) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Max", selection: $historyReturnMax ) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Window", selection: $historyReturnWindow ) {
|
||||
Text("unset").tag(0)
|
||||
Text("One Minute").tag(60)
|
||||
Text("Five Minutes").tag(300)
|
||||
Text("Ten Minutes").tag(600)
|
||||
Text("Fifteen Minutes").tag(900)
|
||||
Text("Thirty Minutes").tag(1800)
|
||||
Text("One Hour").tag(3600)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.storeForwardConfig == nil)
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.storeForwardConfig == nil)
|
||||
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ struct TelemetryConfig: View {
|
|||
@State var environmentDisplayFahrenheit = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
|
|
|
|||
|
|
@ -39,8 +39,6 @@ struct PositionConfig: View {
|
|||
@State var rxGpio = 0
|
||||
@State var txGpio = 0
|
||||
@State var fixedPosition = false
|
||||
@State var gpsUpdateInterval = 0
|
||||
@State var gpsAttemptTime = 0
|
||||
@State var positionBroadcastSeconds = 0
|
||||
@State var broadcastSmartMinimumDistance = 0
|
||||
@State var broadcastSmartMinimumIntervalSecs = 0
|
||||
|
|
@ -213,12 +211,6 @@ struct PositionConfig: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
if deviceGpsEnabled {
|
||||
|
||||
Picker("Attempt Time", selection: $gpsAttemptTime) {
|
||||
ForEach(GpsAttemptTimes.allCases) { at in
|
||||
Text(at.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("GPS Receive GPIO", selection: $rxGpio) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
|
|
@ -280,7 +272,6 @@ struct PositionConfig: View {
|
|||
pc.positionBroadcastSmartEnabled = smartPositionEnabled
|
||||
pc.gpsEnabled = deviceGpsEnabled
|
||||
pc.fixedPosition = fixedPosition
|
||||
pc.gpsAttemptTime = UInt32(gpsAttemptTime)
|
||||
pc.positionBroadcastSecs = UInt32(positionBroadcastSeconds)
|
||||
pc.broadcastSmartMinimumIntervalSecs = UInt32(broadcastSmartMinimumIntervalSecs)
|
||||
pc.broadcastSmartMinimumDistance = UInt32(broadcastSmartMinimumDistance)
|
||||
|
|
@ -347,11 +338,6 @@ struct PositionConfig: View {
|
|||
if newTxGpio != node!.positionConfig!.txGpio { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: gpsAttemptTime) { newGpsAttemptTime in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newGpsAttemptTime != node!.positionConfig!.gpsAttemptTime { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: smartPositionEnabled) { newSmartPositionEnabled in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newSmartPositionEnabled != node!.positionConfig!.smartPositionEnabled { hasChanges = true }
|
||||
|
|
@ -439,8 +425,6 @@ struct PositionConfig: View {
|
|||
self.rxGpio = Int(node?.positionConfig?.rxGpio ?? 0)
|
||||
self.txGpio = Int(node?.positionConfig?.txGpio ?? 0)
|
||||
self.fixedPosition = node?.positionConfig?.fixedPosition ?? false
|
||||
self.gpsUpdateInterval = Int(node?.positionConfig?.gpsUpdateInterval ?? 30)
|
||||
self.gpsAttemptTime = Int(node?.positionConfig?.gpsAttemptTime ?? 30)
|
||||
self.positionBroadcastSeconds = Int(node?.positionConfig?.positionBroadcastSeconds ?? 900)
|
||||
self.broadcastSmartMinimumIntervalSecs = Int(node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 30)
|
||||
self.broadcastSmartMinimumDistance = Int(node?.positionConfig?.broadcastSmartMinimumDistance ?? 50)
|
||||
|
|
|
|||
160
Meshtastic/Views/Settings/RouteRecorder.swift
Normal file
160
Meshtastic/Views/Settings/RouteRecorder.swift
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
//
|
||||
// Routes.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 11/21/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
import CoreMotion
|
||||
|
||||
struct TimerDisplayObject {
|
||||
var seconds: Int = 0
|
||||
var minutes: Int = 0
|
||||
var hours: Int = 0
|
||||
|
||||
var display: String {
|
||||
if self.seconds == 0 {
|
||||
"\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):00"
|
||||
} else {
|
||||
"\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):\(String(format: "%02d", self.seconds))"
|
||||
}
|
||||
}
|
||||
|
||||
var timeMinuteCalculator: Float { Float(hours*60+seconds/60+minutes) }
|
||||
}
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct RouteRecorder: View {
|
||||
|
||||
@ObservedObject var locationsHandler = LocationsHandler.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
|
||||
@State var isTimerRunning = false
|
||||
@State var isShowingDetails = false
|
||||
@State var timer: Timer?
|
||||
@Namespace var namespace
|
||||
@Namespace var mapscope
|
||||
@State var timeElapsed: TimerDisplayObject = TimerDisplayObject()
|
||||
@State var timerDisplay = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack {
|
||||
VStack {
|
||||
Map(position: $position, scope: mapscope) {
|
||||
UserAnnotation()
|
||||
// ForEach(locations, id: \.id) { location in
|
||||
// Marker(location.name, systemImage: location.icon, coordinate: location.location)
|
||||
// .tint(location.colour)
|
||||
// }
|
||||
}
|
||||
}
|
||||
.mapControls {
|
||||
MapUserLocationButton()
|
||||
MapCompass()
|
||||
MapScaleView()
|
||||
MapPitchToggle()
|
||||
}
|
||||
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
|
||||
.transition(.slide)
|
||||
.mapControlVisibility(.visible)
|
||||
.task {
|
||||
print("this is running")
|
||||
locationsHandler.startLocationUpdates()
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
ZStack {
|
||||
VStack {
|
||||
HStack(spacing: 10) {
|
||||
Spacer()
|
||||
if isTimerRunning {
|
||||
Button {
|
||||
isShowingDetails = true
|
||||
isTimerRunning = false
|
||||
} label: {
|
||||
Image(systemName: "pause.fill")
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.circle)
|
||||
.matchedGeometryEffect(id: "Pause Button", in: namespace)
|
||||
} else {
|
||||
Button {
|
||||
isShowingDetails = true
|
||||
isTimerRunning = true
|
||||
timeElapsed.seconds -= 1
|
||||
} label: {
|
||||
Image(systemName: "play.fill")
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.circle)
|
||||
.matchedGeometryEffect(id: "Play Button", in: namespace)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.onReceive(timerDisplay) { _ in
|
||||
if isTimerRunning {
|
||||
timeElapsed.seconds += 1
|
||||
if timeElapsed.seconds == 60 {
|
||||
timeElapsed.seconds = 0
|
||||
timeElapsed.minutes += 1
|
||||
if timeElapsed.minutes == 60 {
|
||||
timeElapsed.minutes = 0
|
||||
timeElapsed.hours += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $isShowingDetails) {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
HStack {
|
||||
Text(timeElapsed.display)
|
||||
.font(.largeTitle)
|
||||
Text("Time Elapseed")
|
||||
.font(.callout)
|
||||
}
|
||||
.padding()
|
||||
Divider()
|
||||
VStack(alignment: .leading) {
|
||||
let horizontalAccuracy = Measurement(value: locationsHandler.lastLocation.horizontalAccuracy, unit: UnitLength.meters)
|
||||
let verticalAccuracy = Measurement(value: locationsHandler.lastLocation.verticalAccuracy, unit: UnitLength.meters)
|
||||
let altitiude = Measurement(value: locationsHandler.lastLocation.altitude, unit: UnitLength.meters)
|
||||
let speed = Measurement(value: locationsHandler.lastLocation.speed, unit: UnitSpeed.kilometersPerHour)
|
||||
List {
|
||||
Label("Coordinate \(String(format: "%.5f", locationsHandler.lastLocation.coordinate.latitude)), \(String(format: "%.5f", locationsHandler.lastLocation.coordinate.longitude))", systemImage: "mappin")
|
||||
.textSelection(.enabled)
|
||||
Label("Horizontal Accuracy \(horizontalAccuracy.formatted())", systemImage: "scope")
|
||||
if locationsHandler.lastLocation.verticalAccuracy > 0 {
|
||||
Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2")
|
||||
}
|
||||
Label("Vertical Accuracy \(verticalAccuracy.formatted())", systemImage: "lines.measurement.vertical")
|
||||
Label("Satellites Estimate \(LocationHelper.satsInView)", systemImage: "sparkles")
|
||||
Label("\(locationsHandler.isStationary ? "Moving" : "Stationary")", systemImage: locationsHandler.isStationary ? "figure.walk.motion" : "figure.stand")
|
||||
if locationsHandler.lastLocation.speedAccuracy > 0 {
|
||||
Label("Speed \(speed.formatted())", systemImage: "speedometer")
|
||||
}
|
||||
if locationsHandler.lastLocation.courseAccuracy > 0 {
|
||||
Label("Heading \(String(format: "%.2f", locationsHandler.lastLocation.course))°", systemImage: "location.circle")
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.5)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,7 @@ struct Routes: View {
|
|||
|
||||
var routes: FetchedResults<RouteEntity>
|
||||
var body: some View {
|
||||
//NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
Button("Import Route") {
|
||||
importing = true
|
||||
}
|
||||
|
|
@ -152,8 +151,6 @@ struct Routes: View {
|
|||
.listStyle(.plain)
|
||||
}
|
||||
.navigationTitle("Route List")
|
||||
// } detail: {
|
||||
|
||||
VStack {
|
||||
if selectedRoute != nil {
|
||||
let locationArray = selectedRoute?.locations?.array as? [LocationEntity] ?? []
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ struct Settings: View {
|
|||
enum SettingsSidebar {
|
||||
case appSettings
|
||||
case routes
|
||||
case routeRecorder
|
||||
case shareChannels
|
||||
case userConfig
|
||||
case loraConfig
|
||||
|
|
@ -68,6 +69,15 @@ struct Settings: View {
|
|||
Text("routes")
|
||||
}
|
||||
.tag(SettingsSidebar.routes)
|
||||
|
||||
NavigationLink {
|
||||
RouteRecorder()
|
||||
} label: {
|
||||
Image(systemName: "record.circle")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("route.recorder")
|
||||
}
|
||||
.tag(SettingsSidebar.routeRecorder)
|
||||
}
|
||||
|
||||
let node = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
|
|
@ -303,6 +313,7 @@ struct Settings: View {
|
|||
}
|
||||
}
|
||||
.onAppear {
|
||||
selection = SettingsSidebar.about
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue