From 126cdfbdb3557ecbda975c88cc153293a8bc827a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 6 Dec 2023 12:32:17 -0800 Subject: [PATCH] Settings rework - new async location handler --- Meshtastic.xcodeproj/project.pbxproj | 12 +- Meshtastic/Helpers/BLEManager.swift | 83 ++-- Meshtastic/Helpers/LocationsHandler.swift | 96 +++++ .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 4 +- .../contents | 378 ++++++++++++++++++ Meshtastic/MeshtasticAppDelegate.swift | 14 +- .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 2 +- .../Nodes/Helpers/Map/PositionPopover.swift | 6 +- .../Config/Module/CannedMessagesConfig.swift | 1 - .../Config/Module/DetectionSensorConfig.swift | 269 ++++++------- .../Module/ExternalNotificationConfig.swift | 229 +++++------ .../Settings/Config/Module/MQTTConfig.swift | 327 +++++++-------- .../Settings/Config/Module/SerialConfig.swift | 2 - .../Settings/Config/Module/StoreForward.swift | 120 +++--- .../Config/Module/TelemetryConfig.swift | 1 - .../Settings/Config/PositionConfig.swift | 16 - Meshtastic/Views/Settings/RouteRecorder.swift | 160 ++++++++ Meshtastic/Views/Settings/Routes.swift | 5 +- Meshtastic/Views/Settings/Settings.swift | 11 + 20 files changed, 1193 insertions(+), 545 deletions(-) create mode 100644 Meshtastic/Helpers/LocationsHandler.swift create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents create mode 100644 Meshtastic/Views/Settings/RouteRecorder.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 1ac11fbe..e7b9c1b7 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -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 = ""; }; DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = ""; }; DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; + DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV21.xcdatamodel; sourceTree = ""; }; + DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsHandler.swift; sourceTree = ""; }; DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareChannels.swift; sourceTree = ""; }; DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModel.xcdatamodel; sourceTree = ""; }; DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryGauge.swift; sourceTree = ""; }; @@ -403,6 +407,7 @@ DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = ""; }; DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; + DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRecorder.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = ""; }; DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForward.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 531d4206..524aa226 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -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!") - // } } diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift new file mode 100644 index 00000000..9c5626c6 --- /dev/null +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -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 + } +} diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index fbcba258..12116f95 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV20.xcdatamodel + MeshtasticDataModelV21.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents index fc942300..88f217f5 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -116,10 +116,12 @@ + + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents new file mode 100644 index 00000000..fc942300 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 0918f9ea..df9fe7ac 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -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) { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 48ff4ba1..850c4e7e 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -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 { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index d92d4b35..ea7a550c 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -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) diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index f79c90b6..42a83ed3 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -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.") diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 868aaf85..64e98f2a 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -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) + } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 3b99f7f0..0c27ee33 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -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: { diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 1dd1f6cb..001ef1f7 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -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: { diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 7e716166..4386b3ca 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -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.") diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForward.swift b/Meshtastic/Views/Settings/Config/Module/StoreForward.swift index d1b22798..cec01a74 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForward.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForward.swift @@ -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: { diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 560331e2..f3cdb5ef 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -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 { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index a0205ae2..746926dd 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -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) diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift new file mode 100644 index 00000000..df5ca511 --- /dev/null +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -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) + } + } + } + } +} diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index 1c3d52c7..5039f724 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -23,8 +23,7 @@ struct Routes: View { var routes: FetchedResults 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] ?? [] diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 16423c60..f8daa9e8 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -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 }