diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 37750d4d..d80208dd 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -723,9 +723,6 @@ }, "Altitude is Mean Sea Level" : { - }, - "Altitude: %@" : { - }, "Always point north" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 8e478691..cca267e7 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -56,6 +56,8 @@ DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1933752B0835D500771CD5 /* PositionAltitudeChart.swift */; }; DD1933782B084F4200771CD5 /* Measurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1933772B084F4200771CD5 /* Measurement.swift */; }; DD1B8F402B35E2F10022AABC /* GPSStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */; }; + DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0EA2C601795008C0C70 /* CLLocation.swift */; }; + DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -287,6 +289,9 @@ DD1933752B0835D500771CD5 /* PositionAltitudeChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAltitudeChart.swift; sourceTree = ""; }; DD1933772B084F4200771CD5 /* Measurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurement.swift; sourceTree = ""; }; DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPSStatus.swift; sourceTree = ""; }; + DD1BD0EA2C601795008C0C70 /* CLLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocation.swift; sourceTree = ""; }; + DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatters.swift; sourceTree = ""; }; + DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -591,6 +596,14 @@ path = CoreData; sourceTree = ""; }; + DD1BD0EC2C603C5B008C0C70 /* Measurement */ = { + isa = PBXGroup; + children = ( + DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */, + ); + path = Measurement; + sourceTree = ""; + }; DD47E3CA26F0E50300029299 /* Nodes */ = { isa = PBXGroup; children = ( @@ -801,6 +814,7 @@ DDC2E15626CE248E0042C5E4 /* Meshtastic */ = { isa = PBXGroup; children = ( + DD1BD0EC2C603C5B008C0C70 /* Measurement */, 25F5D5BC2C3F6D7B008036E3 /* Router */, DD7709392AA1ABA1007A8BF0 /* Tips */, DD90860A26F645B700DC5189 /* Meshtastic.entitlements */, @@ -962,6 +976,7 @@ DD007BB12AA59B9A00F5FA12 /* CoreData */, DDFFA7462B3A7F3C004730DB /* Bundle.swift */, DDDB444529F8A96500EE2349 /* Character.swift */, + DD1BD0EA2C601795008C0C70 /* CLLocation.swift */, DDDB444929F8AA3A00EE2349 /* CLLocationCoordinate2D.swift */, DDDB444B29F8AAA600EE2349 /* Color.swift */, 25C49D8F2C471AEA0024FBD1 /* Constants.swift */, @@ -1269,6 +1284,7 @@ DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */, DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */, DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */, + DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, @@ -1304,6 +1320,7 @@ DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */, DDB6ABD928B0A4BA00384BA1 /* BluetoothModes.swift in Sources */, + DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */, DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */, DD2553592855B52700E55709 /* PositionConfig.swift in Sources */, DD97E96828EFE9A00056DDA4 /* About.swift in Sources */, @@ -1812,6 +1829,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */, DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */, DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */, DD3D17DC2C3D7B1400561584 /* MeshtasticDataModelV 39.xcdatamodel */, @@ -1854,7 +1872,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */; + currentVersion = DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Extensions/CLLocation.swift b/Meshtastic/Extensions/CLLocation.swift new file mode 100644 index 00000000..c4a81849 --- /dev/null +++ b/Meshtastic/Extensions/CLLocation.swift @@ -0,0 +1,28 @@ +// +// CLLocation.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 8/4/24. +// +import Foundation +import MapKit + +func degreesToRadians(degrees: Double) -> Double { return degrees * .pi / 180.0 } +func radiansToDegrees(radians: Double) -> Double { return radians * 180.0 / .pi } + +func getBearingBetweenTwoPoints(point1: CLLocation, point2: CLLocation) -> Double { + + let lat1 = degreesToRadians(degrees: point1.coordinate.latitude) + let lon1 = degreesToRadians(degrees: point1.coordinate.longitude) + + let lat2 = degreesToRadians(degrees: point2.coordinate.latitude) + let lon2 = degreesToRadians(degrees: point2.coordinate.longitude) + + let dLon = lon2 - lon1 + + let y = sin(dLon) * cos(lat2) + let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon) + let radiansBearing = atan2(y, x) + + return radiansToDegrees(radians: radiansBearing) +} diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index e88a2bee..66c915df 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -63,7 +63,7 @@ public func createNodeInfo(num: Int64, context: NSManagedObjectContext) -> NodeI newNode.num = Int64(num) let newUser = UserEntity(context: context) newUser.num = Int64(num) - let userId = String(format: "%2X", num) + let userId = num.toHex() newUser.userId = "!\(userId)" let last4 = String(userId.suffix(4)) newUser.longName = "Meshtastic \(last4)" diff --git a/Meshtastic/Extensions/Int.swift b/Meshtastic/Extensions/Int.swift index c9087d7f..3f2cb358 100644 --- a/Meshtastic/Extensions/Int.swift +++ b/Meshtastic/Extensions/Int.swift @@ -18,12 +18,12 @@ extension Int { extension UInt32 { func toHex() -> String { - return String(format: "!%2X", self) + return String(format: "!%2X", self).lowercased() } } extension Int64 { func toHex() -> String { - return String(format: "!%2X", self) + return String(format: "!%2X", self).lowercased() } } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 0fbd680b..ca8441be 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -158,7 +158,7 @@ extension UserDefaults { @UserDefault(.firmwareVersion, defaultValue: "0.0.0") static var firmwareVersion: String - + @UserDefault(.environmentEnableWeatherKit, defaultValue: true) static var environmentEnableWeatherKit: Bool diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 93879e02..475235de 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -902,7 +902,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(connectedPeripheral.num)) do { - let fetchedNodeInfo = try context.fetch(fetchNodeInfoRequest) ?? [] + let fetchedNodeInfo = try context.fetch(fetchNodeInfoRequest) if fetchedNodeInfo.count == 1 { // Subscribe to Mqtt Client Proxy if enabled if fetchedNodeInfo[0].mqttConfig != nil && fetchedNodeInfo[0].mqttConfig?.enabled ?? false && fetchedNodeInfo[0].mqttConfig?.proxyToClientEnabled ?? false { @@ -1132,7 +1132,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return success } - @MainActor + @MainActor public func getPositionFromPhoneGPS(destNum: Int64) -> Position? { var positionPacket = Position() if #available(iOS 17.0, macOS 14.0, *) { @@ -1265,7 +1265,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum)) Logger.services.debug("📍 \(logString)") return true @@ -1523,7 +1522,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let fetchMyInfoRequest = MyInfoEntity.fetchRequest() fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) ?? [] + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) if fetchedMyInfo.count == 1 { i = Int32(fetchedMyInfo[0].channels?.count ?? -1) myInfo = fetchedMyInfo[0] diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index bb3687b5..d2987e46 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -287,6 +287,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newUser.longName = nodeInfo.user.longName newUser.shortName = nodeInfo.user.shortName newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() + newUser.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) + newUser.hwDisplayName = dh?.displayName + } + } newUser.isLicensed = nodeInfo.user.isLicensed newUser.role = Int32(nodeInfo.user.role.rawValue) newNode.user = newUser @@ -354,6 +361,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() + fetchedNode[0].user!.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId }) + fetchedNode[0].user!.hwDisplayName = dh?.displayName + } + } } else { if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum { diff --git a/Meshtastic/Measurement/CustomFormatters.swift b/Meshtastic/Measurement/CustomFormatters.swift new file mode 100644 index 00000000..e14c96fd --- /dev/null +++ b/Meshtastic/Measurement/CustomFormatters.swift @@ -0,0 +1,18 @@ +// +// CustomFormatters.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 8/4/24. +// + +import Foundation + +/// Custom altitude formatter that always returns the provided unit +/// Needs to be used in conjunction with logic that checks for metric and displays the right value. +public var altitudeFormatter: MeasurementFormatter { + let formatter = MeasurementFormatter() + formatter.unitOptions = .providedUnit + formatter.unitStyle = .long + formatter.numberFormatter.maximumFractionDigits = 1 + return formatter +} diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 9deb5562..3ddf90f8 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 41.xcdatamodel + MeshtasticDataModelV 42.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents new file mode 100644 index 00000000..6b2eaa00 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index ff691440..4d7dc4c1 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -93,7 +93,7 @@ struct MeshtasticAppleApp: App { if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false - if ((self.incomingUrl?.absoluteString.lowercased().contains("?")) != nil) { + if self.incomingUrl?.absoluteString.lowercased().contains("?") != nil { guard let cs = components.last!.components(separatedBy: "?").first else { return } diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 96808c73..93ba7f16 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -103,39 +103,6 @@ extension NSPersistentContainer { case invalidSource(String) } - /// Restore a persistent store for a URL `backupURL`. - /// **Be very careful with this**. To restore a persistent store, the current persistent store must be removed from the container. When that happens, **all currently loaded Core Data objects** will become invalid. Using them after restoring will cause your app to crash. When calling this method you **must** ensure that you do not continue to use any previously fetched managed objects or existing fetched results controllers. **If this method does not throw, that does not mean your app is safe.** You need to take extra steps to prevent crashes. The details vary depending on the nature of your app. - /// - Parameter backupURL: A file URL containing backup copies of all currently loaded persistent stores. - /// - Throws: `CopyPersistentStoreError` in various situations. - /// - Returns: Nothing. If no errors are thrown, the restore is complete. -// func restorePersistentStore(from backupURL: URL) throws -> Void { -// guard backupURL.isFileURL else { -// throw CopyPersistentStoreErrors.invalidSource("Backup URL must be a file URL") -// } -// -// for persistentStoreDescription in persistentStoreDescriptions { -// guard let loadedStoreURL = persistentStoreDescription.url else { -// continue -// } -// guard FileManager.default.fileExists(atPath: backupURL.path) else { -// throw CopyPersistentStoreErrors.invalidSource("Missing backup store for \(backupURL)") -// } -// do { -// let storeOptions = persistentStoreDescription.options -// let configurationName = persistentStoreDescription.configuration -// let storeType = persistentStoreDescription.type -// -// // Replace the current store with the backup copy. This has a side effect of removing the current store from the Core Data stack. -// // When restoring, it's necessary to use the current persistent store coordinator. -// try persistentStoreCoordinator.replacePersistentStore(at: loadedStoreURL, destinationOptions: storeOptions, withPersistentStoreFrom: backupURL, sourceOptions: storeOptions, ofType: storeType) -// // Add the persistent store at the same location we've been using, because it was removed in the previous step. -// try persistentStoreCoordinator.addPersistentStore(ofType: storeType, configurationName: configurationName, at: loadedStoreURL, options: storeOptions) -// } catch { -// throw CopyPersistentStoreErrors.copyStoreError("Could not restore: \(error.localizedDescription)") -// } -// } -// } -// /// Restore backup persistent stores located in the directory referenced by `backupURL`. /// /// **Be very careful with this**. To restore a persistent store, the current persistent store must be removed from the container. When that happens, **all currently loaded Core Data objects** will become invalid. Using them after restoring will cause your app to crash. When calling this method you **must** ensure that you do not continue to use any previously fetched managed objects or existing fetched results controllers. **If this method does not throw, that does not mean your app is safe.** You need to take extra steps to prevent crashes. The details vary depending on the nature of your app. diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 3fac0312..c407f414 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -177,6 +177,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.shortName = newUserMessage.shortName newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() + newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) + newUser.hwDisplayName = dh?.displayName + } + } newNode.user = newUser if UserDefaults.newNodeNotifications { @@ -257,6 +264,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() + fetchedNode[0].user!.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) + fetchedNode[0].user!.hwDisplayName = dh?.displayName + } + } } } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index ad538bd0..09eb2e6c 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -5,7 +5,7 @@ "platformioTarget": "tlora-v2", "architecture": "esp32", "activelySupported": false, - "displayName": "T-LoRa V2" + "displayName": "LILYGO T-LoRa V2" }, { "hwModel": 2, @@ -13,7 +13,7 @@ "platformioTarget": "tlora-v1", "architecture": "esp32", "activelySupported": false, - "displayName": "T-LoRa V1" + "displayName": "LILYGO T-LoRa V1" }, { "hwModel": 3, @@ -21,7 +21,7 @@ "platformioTarget": "tlora-v2-1-1_6", "architecture": "esp32", "activelySupported": true, - "displayName": "T-LoRa V2.1-1.6" + "displayName": "LILYGO T-LoRa V2.1-1.6" }, { "hwModel": 4, @@ -29,7 +29,7 @@ "platformioTarget": "tbeam", "architecture": "esp32", "activelySupported": true, - "displayName": "T-Beam" + "displayName": "LILYGO T-Beam" }, { "hwModel": 5, @@ -45,7 +45,7 @@ "platformioTarget": "tbeam0_7", "architecture": "esp32", "activelySupported": false, - "displayName": "T-Beam V0.7" + "displayName": "LILYGO T-Beam V0.7" }, { "hwModel": 7, @@ -53,7 +53,7 @@ "platformioTarget": "t-echo", "architecture": "nrf52840", "activelySupported": true, - "displayName": "T-Echo" + "displayName": "LILYGO T-Echo" }, { "hwModel": 8, @@ -61,7 +61,7 @@ "platformioTarget": "tlora-v1_3", "architecture": "esp32", "activelySupported": false, - "displayName": "T-LoRa V1.1-1.3" + "displayName": "LILYGO T-LoRa V1.1-1.3" }, { "hwModel": 9, @@ -69,7 +69,7 @@ "platformioTarget": "rak4631", "architecture": "nrf52840", "activelySupported": true, - "displayName": "RAK4631" + "displayName": "RAK WisBlock 4631" }, { "hwModel": 10, @@ -93,7 +93,7 @@ "platformioTarget": "tbeam-s3-core", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-Beam S3 Core" + "displayName": "LILYGO T-Beam S3 Core" }, { "hwModel": 13, @@ -101,7 +101,7 @@ "platformioTarget": "rak11200", "architecture": "esp32", "activelySupported": false, - "displayName": "RAK11200" + "displayName": "RAK WisBlock 11200" }, { "hwModel": 14, @@ -117,7 +117,7 @@ "platformioTarget": "tlora-v2-1-1_8", "architecture": "esp32", "activelySupported": true, - "displayName": "T-LoRa V2.1-1.8" + "displayName": "LILYGO T-LoRa V2.1-1.8" }, { "hwModel": 16, @@ -125,7 +125,7 @@ "platformioTarget": "tlora-t3s3-v1", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-LoRa T3-S3" + "displayName": "LILYGO T-LoRa T3-S3" }, { "hwModel": 17, @@ -143,6 +143,14 @@ "activelySupported": true, "displayName": "Nano G2 Ultra" }, + { + "hwModel": 21, + "hwModelSlug": "WIO_WM1110", + "platformioTarget": "wio-tracker-wm1110", + "architecture": "nrf52840", + "activelySupported": true, + "displayName": "Seeed Wio WM1110 Tracker" + }, { "hwModel": 25, "hwModelSlug": "STATION_G1", @@ -157,7 +165,7 @@ "platformioTarget": "rak11310", "architecture": "rp2040", "activelySupported": true, - "displayName": "RAK11310" + "displayName": "RAK WisBlock 11310" }, { "hwModel": 29, @@ -165,7 +173,7 @@ "platformioTarget": "canaryone", "architecture": "nrf52840", "activelySupported": true, - "displayName": "CanaryOne" + "displayName": "Canary One" }, { "hwModel": 30, @@ -277,7 +285,7 @@ "platformioTarget": "t-deck", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-Deck" + "displayName": "LILYGO T-Deck" }, { "hwModel": 51, @@ -285,7 +293,7 @@ "platformioTarget": "t-watch-s3", "architecture": "esp32-s3", "activelySupported": true, - "displayName": "T-Watch S3" + "displayName": "LILYGO T-Watch S3" }, { "hwModel": 52, @@ -360,11 +368,35 @@ "displayName": "RadioMaster 900 Bandit Nano" }, { - "hwModel": 21, - "hwModelSlug": "WIO_WM1110", - "platformioTarget": "wio-tracker-wm1110", + "hwModel": 66, + "hwModelSlug": "HELTEC_VISION_MASTER_T190", + "platformioTarget": "heltec-vision-master-T190", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "Heltec Vision Master T190" + }, + { + "hwModel": 67, + "hwModelSlug": "HELTEC_VISION_MASTER_E213", + "platformioTarget": "heltec-vision-master-e213", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "Heltec Vision Master E213" + }, + { + "hwModel": 68, + "hwModelSlug": "HELTEC_VISION_MASTER_E290", + "platformioTarget": "heltec-vision-master-e290", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "Heltec Vision Master E290" + }, + { + "hwModel": 71, + "hwModelSlug": "TRACKER_T1000_E", + "platformioTarget": "tracker-t1000-e", "architecture": "nrf52840", "activelySupported": true, - "displayName": "Seeed Wio WM1110 Tracker" + "displayName": "Seeed Card Tracker T1000-E" } ] diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift index 829fd837..c207d084 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -14,36 +14,34 @@ struct LoRaSignalStrengthMeter: View { var compact: Bool var body: some View { - if snr != 0.0 && rssi != 0 { - let signalStrength = getLoRaSignalStrength(snr: snr, rssi: rssi, preset: preset) - let gradient = Gradient(colors: [.red, .orange, .yellow, .green]) - if !compact { - VStack { - LoRaSignalStrengthIndicator(signalStrength: signalStrength) - Text("Signal \(signalStrength.description)").font(.footnote) - Text("SNR \(String(format: "%.2f", snr))dB") - .foregroundColor(getSnrColor(snr: snr, preset: ModemPresets.longFast)) - .font(.caption2) - Text("RSSI \(rssi)dB") - .foregroundColor(getRssiColor(rssi: rssi)) - .font(.caption2) - } - } else { - VStack { - Gauge(value: Double(signalStrength.rawValue), in: 0...3) { - } currentValueLabel: { - Image(systemName: "dot.radiowaves.left.and.right") - .font(.callout) - .frame(width: 30) - Text("Signal \(signalStrength.description)") - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.gray) - .fixedSize() - } - .gaugeStyle(.accessoryLinear) - .tint(gradient) - .font(.caption) + let signalStrength = getLoRaSignalStrength(snr: snr, rssi: rssi, preset: preset) + let gradient = Gradient(colors: [.red, .orange, .yellow, .green]) + if !compact { + VStack { + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", snr))dB") + .foregroundColor(getSnrColor(snr: snr, preset: ModemPresets.longFast)) + .font(.caption2) + Text("RSSI \(rssi)dB") + .foregroundColor(getRssiColor(rssi: rssi)) + .font(.caption2) + } + } else { + VStack { + Gauge(value: Double(signalStrength.rawValue), in: 0...3) { + } currentValueLabel: { + Image(systemName: "dot.radiowaves.left.and.right") + .font(.callout) + .frame(width: 30) + Text("Signal \(signalStrength.description)") + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .foregroundColor(.gray) + .fixedSize() } + .gaugeStyle(.accessoryLinear) + .tint(gradient) + .font(.caption) } } } diff --git a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift index bf196f6d..b7629326 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift @@ -110,7 +110,6 @@ struct MapViewSwiftUI: UIViewRepresentable { overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) - case .satellite: mapView.mapType = .satellite case .hybrid: diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 5c7214fe..13eb837d 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -237,7 +237,7 @@ struct UserList: View { private func searchUserList() { /// Case Insensitive Search Text Predicates - let searchPredicates = ["userId", "numString", "hwModel", "longName", "shortName"].map { property in + let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) } /// Create a compound predicate using each text search preicate as an OR diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 6514ead8..ce0eb182 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -14,7 +14,7 @@ struct UserMessageList: View { @EnvironmentObject var appState: AppState @EnvironmentObject var bleManager: BLEManager @Environment(\.managedObjectContext) var context - + // Keyboard State @FocusState var messageFieldFocused: Bool // View State Items diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 3ffce6cb..63e17424 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -108,6 +108,57 @@ struct DeviceMetricsLog: View { .chartLegend(position: .automatic, alignment: .bottom) } else { // Fallback on earlier versions + Chart { + ForEach(chartData, id: \.self) { point in + Plot { + LineMark( + x: .value("x", point.time!), + y: .value("y", point.batteryLevel) + ) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") + .foregroundStyle(batteryChartColor) + .interpolationMethod(.linear) + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.channelUtilization) + ) + .symbolSize(25) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") + .foregroundStyle(channelUtilizationChartColor) + RuleMark(y: .value("Network Status Orange", 25)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) + .foregroundStyle(.orange) + RuleMark(y: .value("Network Status Red", 50)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) + .foregroundStyle(.red) + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.airUtilTx) + ) + .symbolSize(25) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") + .foregroundStyle(airtimeChartColor) + } + } + .chartXAxis(content: { + AxisMarks(position: .top) + }) + .chartXAxis(.automatic) + .chartYScale(domain: 0...100) + .chartForegroundStyleScale([ + idiom == .phone ? "Battery" : "Battery Level": batteryChartColor, + "Channel Utilization": channelUtilizationChartColor, + "Airtime": airtimeChartColor + ]) + .chartLegend(position: .automatic, alignment: .bottom) } } .frame(minHeight: 240) diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift index 71c3a317..ecf16f13 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift @@ -3,16 +3,12 @@ import OSLog import SwiftUI struct DeleteNodeButton: View { + var bleManager: BLEManager - var context: NSManagedObjectContext - var connectedNode: NodeInfoEntity - var node: NodeInfoEntity - @Environment(\.dismiss) private var dismiss - @State private var isPresentingAlert = false var body: some View { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index faa49760..b3b9c18e 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -10,6 +10,7 @@ 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 @@ -74,8 +75,17 @@ struct PositionPopover: View { .padding(.bottom, 5) /// Altitude Label { - Text("Altitude: \(distanceFormatter.string(fromDistance: Double(position.altitude)))") - .foregroundColor(.primary) + let formatter = MeasurementFormatter() + let distanceInMeters = Measurement(value: Double(position.altitude), unit: UnitLength.meters) + let distanceInFeet = distanceInMeters.converted(to: UnitLength.feet) + if Locale.current.measurementSystem == .metric { + Text(altitudeFormatter.string(from: distanceInMeters)) + .foregroundColor(.primary) + } else { + Text(altitudeFormatter.string(from: distanceInFeet)) + .foregroundColor(.primary) + } + } icon: { Image(systemName: "mountain.2.fill") .symbolRenderingMode(.hierarchical) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 9bc5781a..68769280 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -43,7 +43,6 @@ struct NodeDetail: View { Section("Hardware") { NodeInfoItem(node: node) } - Section("Node") { HStack { Label { @@ -65,7 +64,7 @@ struct NodeDetail: View { .symbolRenderingMode(.multicolor) } Spacer() - Text(node.user?.userId ?? "?") + Text(node.num.toHex()) .textSelection(.enabled) } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 1355ce57..69235f11 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -11,64 +11,64 @@ import MapKit struct NodeInfoItem: View { - @ObservedObject - var node: NodeInfoEntity + @ObservedObject var node: NodeInfoEntity var modemPreset: ModemPresets = ModemPresets( rawValue: UserDefaults.modemPreset ) ?? ModemPresets.longFast var body: some View { - HStack { - Spacer() - CircleText( - text: node.user?.shortName ?? "?", - color: Color(UIColor(hex: UInt32(node.num))), - circleSize: 65 - ) - if let user = node.user { - VStack(alignment: .center) { - if user.hwModel != "UNSET" { - Image(user.hardwareImage ?? "UNSET") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 65, height: 65) - .cornerRadius(5) - Text(String(node.user!.hwModel ?? "unset".localized)) - .font(.caption2) - .frame(maxWidth: 80) - } else { - Image(systemName: "person.crop.circle.badge.questionmark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 65, height: 65) - .cornerRadius(5) - Text(String("incomplete".localized)) - .font(.caption) - .frame(maxWidth: 80) + ViewThatFits(in: .horizontal) { + VStack { + if let user = node.user { + HStack(alignment: .center) { + if user.hwModel != "UNSET" { + Image(user.hardwareImage ?? "UNSET") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 65, height: 65) + .cornerRadius(5) + Text(String(node.user?.hwDisplayName ?? (node.user?.hwModel ?? "unset".localized))) + .font(.callout) + } else { + Image(systemName: "person.crop.circle.badge.questionmark") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 65, height: 65) + .cornerRadius(5) + Text(String("incomplete".localized)) + .font(.callout) + } } } - } - - if node.snr != 0 && !node.viaMqtt { - VStack(alignment: .center) { - let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: modemPreset) - LoRaSignalStrengthIndicator(signalStrength: signalStrength) - Text("Signal \(signalStrength.description)").font(.footnote) - Text("SNR \(String(format: "%.2f", node.snr))dB") - .foregroundColor(getSnrColor(snr: node.snr, preset: modemPreset)) - .font(.caption2) - Text("RSSI \(node.rssi)dB") - .foregroundColor(getRssiColor(rssi: node.rssi)) - .font(.caption) + HStack(alignment: .center) { + Spacer() + CircleText( + text: node.user?.shortName ?? "?", + color: Color(UIColor(hex: UInt32(node.num))), + circleSize: 75 + ) + if node.snr != 0 && !node.viaMqtt { + Spacer() + VStack { + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: modemPreset) + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", node.snr))dB") + .foregroundColor(getSnrColor(snr: node.snr, preset: modemPreset)) + .font(.caption) + Text("RSSI \(node.rssi)dB") + .foregroundColor(getRssiColor(rssi: node.rssi)) + .font(.caption) + } + } + if node.telemetries?.count ?? 0 > 0 { + Spacer() + BatteryGauge(node: node) + } + Spacer() } - .frame(minWidth: 110, maxWidth: 175) } - - if node.telemetries?.count ?? 0 > 0 { - BatteryGauge(node: node) - } - Spacer() } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index a68a63cd..ba7dee6e 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -21,7 +21,6 @@ struct NodeListItem: View { LazyVStack(alignment: .leading) { HStack { VStack(alignment: .leading) { - CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 70) .padding(.trailing, 5) BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor) @@ -99,6 +98,17 @@ struct NodeListItem: View { DistanceText(meters: metersAway) .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.gray) + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: CLLocation(latitude: lastPostion.coordinate.latitude, longitude: lastPostion.coordinate.longitude)) + let headingDegrees = Angle.degrees(trueBearing) + Image(systemName: "location.north") + .font(.callout) + .symbolRenderingMode(.multicolor) + .clipShape(Circle()) + .rotationEffect(headingDegrees) + let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) + Text("\(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))") + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .foregroundColor(.gray) } } } else { @@ -114,6 +124,17 @@ struct NodeListItem: View { DistanceText(meters: metersAway) .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.secondary) + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: CLLocation(latitude: lastPostion.coordinate.latitude, longitude: lastPostion.coordinate.longitude)) + let headingDegrees = Angle.degrees(trueBearing) + Image(systemName: "location.north") + .font(.callout) + .symbolRenderingMode(.multicolor) + .clipShape(Circle()) + .rotationEffect(headingDegrees) + let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) + Text("\(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))") + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .foregroundColor(.gray) } } } @@ -164,7 +185,7 @@ struct NodeListItem: View { .frame(width: 30) } if node.hasEnvironmentMetrics { - Image(systemName: "cloud.sun.rain.fill") + Image(systemName: "cloud.sun.rain") .symbolRenderingMode(.hierarchical) .font(.callout) .frame(width: 30) diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 71cbfaf2..2dc8d105 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -111,7 +111,7 @@ struct MeshMap: View { } .onChange(of: router.navigationState) { guard case .map(let selectedNodeNum) = router.navigationState else { return } - //TODO: handle deep link for waypoints + // TODO: handle deep link for waypoints } .onChange(of: (selectedMapLayer)) { newMapLayer in switch selectedMapLayer { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 3773593b..c442b315 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -31,13 +31,13 @@ struct NodeList: View { @State private var hopsAway: Double = -1.0 @State private var roleFilter = false @State private var deviceRoles: Set = [] - @State private var isPresentingTraceRouteSentAlert = false @State private var isPresentingPositionSentAlert = false @State private var isPresentingPositionFailedAlert = false @State private var isPresentingDeleteNodeAlert = false @State private var deleteNodeId: Int64 = 0 + var boolFilters: [Bool] {[ isOnline, isFavorite, @@ -344,7 +344,7 @@ struct NodeList: View { private func searchNodeList() async { /// Case Insensitive Search Text Predicates - let searchPredicates = ["user.userId", "user.numString", "user.hwModel", "user.longName", "user.shortName"].map { property in + let searchPredicates = ["user.userId", "user.numString", "user.hwModel", "user.hwDisplayName", "user.longName", "user.shortName"].map { property in return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) } /// Create a compound predicate using each text search preicate as an OR diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 9556298a..345a4299 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -63,7 +63,6 @@ struct TraceRouteLog: View { } .font(.title2) } - if selectedRoute?.response ?? false { if selectedRoute?.hasPositions ?? false { Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index faae073e..880dabf2 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -243,13 +243,11 @@ struct Settings: View { var loggingSection: some View { Section(header: Text("logging")) { - if #available (iOS 17.0, *) { - NavigationLink(value: SettingsNavigationState.debugLogs) { - Label { - Text("Logs") - } icon: { - Image(systemName: "scroll") - } + NavigationLink(value: SettingsNavigationState.debugLogs) { + Label { + Text("Logs") + } icon: { + Image(systemName: "scroll") } } } @@ -401,7 +399,9 @@ struct Settings: View { radioConfigurationSection deviceConfigurationSection moduleConfigurationSection - loggingSection + if #available (iOS 17.0, *) { + loggingSection + } #if DEBUG developersSection #endif