diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ca60afdd6..2438475af 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -194,6 +194,9 @@ }, "256 bit" : { + }, + "A Trace Route was sent, no response has been received." : { + }, "about" : { "localizations" : { @@ -17579,6 +17582,9 @@ }, "Rotary 1" : { + }, + "Route Back: %@" : { + }, "Route Lines" : { @@ -20156,7 +20162,7 @@ "Store and forward clients can request history from routers on the network." : { }, - "Store and forward router devices must also be in the router or router client device role and requires a ESP32 device with PSRAM." : { + "Store and forward router devices require a ESP32 device with PSRAM." : { }, "storeforward" : { @@ -21859,14 +21865,27 @@ "Trace Route Log" : { }, - "Trace route received directly by %@" : { - + "Trace route received directly by %@ with a SNR of %@ dB" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Trace route received directly by %1$@ with a SNR of %2$@ dB" + } + } + } }, "Trace Route Sent" : { }, "Trace route sent to %@" : { + }, + "Trace route to %@ was not sent." : { + + }, + "Trace Route was rate limited. You can send a trace route a maximum of once every thirty seconds." : { + }, "Traffic" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 2035a6fc7..20a5586da 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -100,6 +100,7 @@ DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; }; DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; }; DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; + DD6D5A332CA1178300ED3032 /* TraceRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D5A322CA1178300ED3032 /* TraceRoute.swift */; }; DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65712C6AB8EC0053C113 /* SecureInput.swift */; }; DD6F65742C6CB80A0053C113 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65732C6CB80A0053C113 /* View.swift */; }; DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65752C6EA5490053C113 /* AckErrors.swift */; }; @@ -365,6 +366,8 @@ DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = ""; }; + DD6D5A322CA1178300ED3032 /* TraceRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRoute.swift; sourceTree = ""; }; + DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 45.xcdatamodel"; sourceTree = ""; }; DD6F65712C6AB8EC0053C113 /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = ""; }; DD6F65732C6CB80A0053C113 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; DD6F65752C6EA5490053C113 /* AckErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AckErrors.swift; sourceTree = ""; }; @@ -747,6 +750,14 @@ path = Module; sourceTree = ""; }; + DD6D5A312CA1176A00ED3032 /* Layouts */ = { + isa = PBXGroup; + children = ( + DD6D5A322CA1178300ED3032 /* TraceRoute.swift */, + ); + path = Layouts; + sourceTree = ""; + }; DD6F65772C6EAB860053C113 /* Help */ = { isa = PBXGroup; children = ( @@ -901,6 +912,7 @@ DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( + DD6D5A312CA1176A00ED3032 /* Layouts */, C9483F6B2773016700998F6B /* MapKitMap */, DDC2E18D26CE25CB0042C5E4 /* Helpers */, DD47E3D726F2F21A00029299 /* Bluetooth */, @@ -1433,6 +1445,7 @@ DD6F65742C6CB80A0053C113 /* View.swift in Sources */, DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, + DD6D5A332CA1178300ED3032 /* TraceRoute.swift in Sources */, DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */, BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */, DD73FD1128750779000852D6 /* PositionLog.swift in Sources */, @@ -1686,7 +1699,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.7; + MARKETING_VERSION = 2.5.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1720,7 +1733,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.7; + MARKETING_VERSION = 2.5.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1752,7 +1765,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.7; + MARKETING_VERSION = 2.5.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1785,7 +1798,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.7; + MARKETING_VERSION = 2.5.8; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1897,6 +1910,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */, DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */, DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */, DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */, @@ -1942,7 +1956,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */; + currentVersion = DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index c1bd5beca..7585fb1ec 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -23,7 +23,7 @@ extension NodeInfoEntity { } var hasPositions: Bool { - return positions?.count ?? 0 > 0 + return self.positions?.count ?? 0 > 0 } var hasDeviceMetrics: Bool { diff --git a/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift b/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift index 4e7cdb60a..804aacf88 100644 --- a/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift @@ -10,36 +10,6 @@ import CoreLocation import MapKit import SwiftUI -extension TraceRouteEntity { - - var latitude: Double? { - - let d = Double(latitudeI) - if d == 0 { - return 0 - } - return d / 1e7 - } - - var longitude: Double? { - - let d = Double(longitudeI) - if d == 0 { - return 0 - } - return d / 1e7 - } - - var coordinate: CLLocationCoordinate2D? { - if latitudeI != 0 && longitudeI != 0 { - let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) - return coord - } else { - return nil - } - } -} - extension TraceRouteHopEntity { var latitude: Double? { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 894401c78..865f8d234 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -468,15 +468,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRoute.id = Int64(meshPacket.id) traceRoute.time = Date() traceRoute.node = receivingNode - // Grab the most recent postion, within the last hour - if connectedNode?.positions?.count ?? 0 > 0, let mostRecent = connectedNode?.positions?.lastObject as? PositionEntity { - if mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { - traceRoute.altitude = mostRecent.altitude - traceRoute.latitudeI = mostRecent.latitudeI - traceRoute.longitudeI = mostRecent.longitudeI - traceRoute.hasPositions = true - } - } do { try context.save() Logger.data.info("πŸ’Ύ Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "unknown".localized), privacy: .public)") @@ -601,7 +592,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return } do { - let logRecord = try LogRecord(serializedData: characteristic.value!) + let logRecord = try LogRecord(serializedBytes: characteristic.value!) var message = logRecord.source.isEmpty ? logRecord.message : "[\(logRecord.source)] \(logRecord.message)" switch logRecord.level { case .debug: @@ -622,14 +613,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Ignore fail to parse as LogRecord } - case LEGACY_LOGRADIO_UUID: - if characteristic.value == nil || characteristic.value!.isEmpty { - return - } - if let log = String(data: characteristic.value!, encoding: .utf8) { - handleRadioLog(radioLog: log) - } - case FROMRADIO_UUID: if characteristic.value == nil || characteristic.value!.isEmpty { @@ -638,7 +621,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var decodedInfo = FromRadio() do { - decodedInfo = try FromRadio(serializedData: characteristic.value!) + decodedInfo = try FromRadio(serializedBytes: characteristic.value!) } catch { Logger.services.error("πŸ’₯ \(error.localizedDescription, privacy: .public) \(characteristic.value!, privacy: .public)") @@ -653,6 +636,21 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate ) mqttManager.mqttClientProxy?.publish(message) } else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) { + if decodedInfo.clientNotification.hasReplyID { + /// Set Sent bool on TraceRouteEntity to false if we got rate limited + if decodedInfo.clientNotification.message.starts(with: "TraceRoute") { + let traceRoute = getTraceRoute(id: Int64(decodedInfo.clientNotification.replyID), context: context) + traceRoute?.sent = false + do { + try context.save() + Logger.data.info("πŸ’Ύ [TraceRouteEntity] Trace Route Rate Limited") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("πŸ’₯ [TraceRouteEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } + } let manager = LocalNotificationManager() manager.notifications = [ Notification( @@ -695,8 +693,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate disconnectPeripheral(reconnect: false) try container.restorePersistentStore(from: databasePath) UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0) - connectTo(peripheral: peripheral) + context.refreshAllObjects() Logger.data.notice("πŸ—‚οΈ Restored Core data for /\(UserDefaults.preferredPeripheralNum, privacy: .public)") + connectTo(peripheral: peripheral) } catch { Logger.data.error("πŸ—‚οΈ Restore Core data copy error: \(error, privacy: .public)") } @@ -836,36 +835,72 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // MeshLogger.log("πŸ•ΈοΈ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("πŸ•ΈοΈ MESH PACKET received for Audio App UNHANDLED UNHANDLED") case .tracerouteApp: - if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { + if let routingMessage = try? RouteDiscovery(serializedBytes: decodedInfo.packet.decoded.payload) { let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context) traceRoute?.response = true - traceRoute?.route = routingMessage.route if routingMessage.route.count == 0 { - let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(decodedInfo.packet.from)) + let snr = routingMessage.snrBack.count > 0 ? routingMessage.snrBack[0] / 4 : 0 + traceRoute?.snr = Float(snr) + let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(snr)) MeshLogger.log("πŸͺ§ \(logString)") - } else { - var routeString = "You --> " var hopNodes: [TraceRouteHopEntity] = [] - for node in routingMessage.route { + let connectedHop = TraceRouteHopEntity(context: context) + connectedHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized + connectedHop.time = Date() + if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + connectedHop.altitude = mostRecent.altitude + connectedHop.latitudeI = mostRecent.latitudeI + connectedHop.longitudeI = mostRecent.longitudeI + traceRoute?.hasPositions = true + } + hopNodes.append(connectedHop) + var routeString = "You --> " + for (index, node) in routingMessage.route.enumerated() { + var hopNode = getNodeInfo(id: Int64(node), context: context) + if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { + hopNode = createNodeInfo(num: Int64(node), context: context) + } + let traceRouteHop = TraceRouteHopEntity(context: context) + if routingMessage.snrTowards.count >= index + 1 { + traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) + } + if let hn = hopNode, hn.hasPositions { + if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + traceRouteHop.altitude = mostRecent.altitude + traceRouteHop.latitudeI = mostRecent.latitudeI + traceRouteHop.longitudeI = mostRecent.longitudeI + traceRoute?.hasPositions = true + } + } + traceRouteHop.num = hopNode?.num ?? 0 + if hopNode != nil { + if decodedInfo.packet.rxTime > 0 { + hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) + } + hopNodes.append(traceRouteHop) + } + routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " + } + var routeBackString = traceRoute?.node?.user?.longName ?? "unknown".localized + for (index, node) in routingMessage.routeBack.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) traceRouteHop.time = Date() - if hopNode?.hasPositions ?? false { - traceRoute?.hasPositions = true - if let mostRecent = hopNode?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { + traceRouteHop.back = true + if routingMessage.snrBack.count >= index + 1 { + traceRouteHop.snr = Float(routingMessage.snrBack[index] / 4) + } + if let hn = hopNode, hn.hasPositions { + if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI - traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized - } else { - traceRoute?.hasPositions = false + traceRoute?.hasPositions = true } - } else { - traceRoute?.hasPositions = false } traceRouteHop.num = hopNode?.num ?? 0 if hopNode != nil { @@ -874,10 +909,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } hopNodes.append(traceRouteHop) } - routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") --> " + routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } - routeString += traceRoute?.node?.user?.longName ?? "unknown".localized traceRoute?.routeText = routeString + traceRoute?.routeBackText = routeBackString traceRoute?.hops = NSOrderedSet(array: hopNodes) do { try context.save() @@ -892,7 +927,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } case .neighborinfoApp: - if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) { + if let neighborInfo = try? NeighborInfo(serializedBytes: decodedInfo.packet.decoded.payload) { // MeshLogger.log("πŸ•ΈοΈ MESH PACKET received for Neighbor Info App UNHANDLED") MeshLogger.log("πŸ•ΈοΈ MESH PACKET received for Neighbor Info App UNHANDLED \(neighborInfo)") } @@ -915,7 +950,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate lastConnectionError = "" isSubscribed = true Logger.mesh.info("🀜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID)") - sendTime() + if sendTime() { + } peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) // Config conplete returns so we don't read the characteristic again diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 414343665..a215667ba 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -84,15 +84,15 @@ import OSLog if smartPostion { let age = -location.timestamp.timeIntervalSinceNow if age > 10 { - Logger.services.warning("πŸ“ [App] Bad Location \(self.count, privacy: .public): Too Old \(age, privacy: .public) seconds ago \(location, privacy: .private)") + Logger.services.info("πŸ“ [App] Smart Position - Bad Location: Too Old \(age, privacy: .public) seconds ago \(location, privacy: .private)") return false } if location.horizontalAccuracy < 0 { - Logger.services.warning("πŸ“ [App] Bad Location \(self.count, privacy: .public): Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)") + Logger.services.info("πŸ“ [App] Smart Position - Bad Location: Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)") return false } if location.horizontalAccuracy > 5 { - Logger.services.warning("πŸ“ [App] Bad Location \(self.count, privacy: .public): Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)") + Logger.services.info("πŸ“ [App] Smart Position - Bad Location: Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)") return false } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 75d2b85d9..7d0c6a6b1 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -876,7 +876,7 @@ func textMessageAppPacket( if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) /// Set the public key for the message - if newMessage.fromUser?.pkiEncrypted ?? false { + if newMessage.fromUser?.pkiEncrypted ?? false && packet.pkiEncrypted { newMessage.pkiEncrypted = true newMessage.publicKey = packet.publicKey } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 8a88003bd..4269da215 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 44.xcdatamodel + MeshtasticDataModelV 45.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents index 98de03479..ae7c08e44 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents @@ -428,11 +428,14 @@ + + + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents new file mode 100644 index 000000000..2a5e4a561 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -0,0 +1,476 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index cbad8df92..eaf5db0b6 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -391,6 +391,22 @@ "activelySupported": true, "displayName": "Heltec Vision Master E290" }, + { + "hwModel": 69, + "hwModelSlug": "HELTEC_MESH_NODE_T114", + "platformioTarget": "heltec-mesh-node-t114", + "architecture": "nrf52840", + "activelySupported": false, + "displayName": "Heltec Mesh Node T114" + }, + { + "hwModel": 70, + "hwModelSlug": "SENSECAP_INDICATOR", + "platformioTarget": "seeed-sensecap-indicator", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "SenseCAP Indicator" + }, { "hwModel": 71, "hwModelSlug": "TRACKER_T1000_E", diff --git a/Meshtastic/Views/Layouts/TraceRoute.swift b/Meshtastic/Views/Layouts/TraceRoute.swift new file mode 100644 index 000000000..bb79f7474 --- /dev/null +++ b/Meshtastic/Views/Layouts/TraceRoute.swift @@ -0,0 +1,68 @@ +// +// TraceRoute.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 9/22/24. +// +import SwiftUI + +struct Rotation: LayoutValueKey { + static let defaultValue: Binding? = nil +} + +struct TraceRouteComponent: View { + var animation: Animation? + @ViewBuilder let content: () -> V + @State private var rotation: Angle = .zero + + var body: some View { + content() + .rotationEffect(rotation) + .layoutValue(key: Rotation.self, value: $rotation.animation(animation)) + } +} + +struct TraceRoute: Layout { + var animatableData: AnimatablePair { + get { + AnimatablePair(rotation.radians, radius) + } + set { + rotation = Angle.radians(newValue.first) + radius = newValue.second + } + } + + var radius: CGFloat + var rotation: Angle + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let maxSize = subviews.map { $0.sizeThatFits(proposal) }.reduce(CGSize.zero) { + return CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height)) + } + return CGSize(width: (maxSize.width / 2 + radius) * 2, + height: (maxSize.height / 2 + radius) * 2) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + let angleStep = (Angle.degrees(360).radians / Double(subviews.count)) + + for (index, subview) in subviews.enumerated() { + let angle = angleStep * CGFloat(index) + rotation.radians + + var point = CGPoint(x: 0, y: -radius).applying(CGAffineTransform(rotationAngle: angle)) + point.x += bounds.midX + point.y += bounds.midY + + subview.place(at: point, anchor: .center, proposal: .unspecified) + + DispatchQueue.main.async { + if index % 2 == 0 { + subview[Rotation.self]?.wrappedValue = .zero + } else { + subview[Rotation.self]?.wrappedValue = .radians(angle) + } + } + } + } +} diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 8166b15f4..398a12b02 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -6,14 +6,15 @@ // import SwiftUI +import CoreData +import OSLog import MapKit - struct TraceRouteLog: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @State private var isPresentingClearLogConfirm: Bool = false @State var isExporting = false @State var exportString = "" @@ -24,117 +25,188 @@ struct TraceRouteLog: View { @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic let distanceFormatter = MKDistanceFormatter() + /// State for the circle of routes + var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast + @State private var indexes: Int = 0 + @State var angle: Angle = .zero + @State var animation: Animation? var body: some View { HStack(alignment: .top) { VStack { VStack { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in - Label { - Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : "No Response")") + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : (route.sent ? "No Response" : "Not Sent"))") + .font(.callout) } icon: { Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash") .symbolRenderingMode(.hierarchical) } + .swipeActions { + Button(role: .destructive) { + context.delete(route) + do { + try context.save() + } catch let error as NSError { + Logger.data.error("\(error.localizedDescription)") + } + } label: { + Label("delete", systemImage: "trash") + } + } } .listStyle(.plain) } - .frame(minHeight: 200, maxHeight: 230) - VStack { + .frame(minHeight: CGFloat(node.traceRoutes?.count ?? 0 * 40), maxHeight: 250) + Divider() + ScrollView { if selectedRoute != nil { if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { - Label { Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") } icon: { - Image(systemName: "signpost.right.and.left") + Image(systemName: "signpost.right") .symbolRenderingMode(.hierarchical) } - .font(.title2) + .font(.title3) + Label { + Text("Route Back: \(selectedRoute?.routeBackText ?? "unknown".localized)") + } icon: { + Image(systemName: "signpost.left") + .symbolRenderingMode(.hierarchical) + } + .font(.title3) } else if selectedRoute?.response ?? false { Label { - Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized) with a SNR of \(String(format: "%.2f", selectedRoute?.node?.snr ?? 0.0)) dB") } icon: { Image(systemName: "signpost.right.and.left") .symbolRenderingMode(.hierarchical) } - .font(.title2) - } - if selectedRoute?.response ?? false { - if selectedRoute?.hasPositions ?? false { - Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.green)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } + .font(.title3) + } else if !(selectedRoute?.sent ?? true) { + Label { + VStack { + Text("Trace route to \(selectedRoute?.node?.user?.longName ?? "unknown".localized) was not sent.") + .font(idiom == .phone ? .body : .largeTitle) + .fontWeight(.semibold) + Text("Trace Route was rate limited. You can send a trace route a maximum of once every thirty seconds.") + .font(idiom == .phone ? .caption : .body) + .foregroundStyle(.secondary) + .padding() } - .annotationTitles(.automatic) - // Direct Trace Route - if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { - if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { - let traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] - Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.black)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } - } - let dashed = StrokeStyle( - lineWidth: 2, - lineCap: .round, lineJoin: .round, dash: [7, 10] - ) - MapPolyline(coordinates: traceRouteCoords) - .stroke(.blue, style: dashed) - } - } else if selectedRoute?.hops?.count ?? 0 == 0 { - + } icon: { + Image(systemName: "square.and.arrow.up.trianglebadge.exclamationmark") + .symbolRenderingMode(.hierarchical) + } + } else { + Label { + VStack { + Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + .font(idiom == .phone ? .body : .largeTitle) + .fontWeight(.semibold) + Text("A Trace Route was sent, no response has been received.") + .font(idiom == .phone ? .caption : .body) + .foregroundStyle(.secondary) + .padding() + } + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + } + if selectedRoute?.hops?.count ?? 0 >= 3 { + HStack(alignment: .center) { + GeometryReader { geometry in + let size = ((geometry.size.width >= geometry.size.height ? geometry.size.height : geometry.size.width) / 2) - (idiom == .phone ? 45 : 85) + Spacer() + TraceRoute(radius: size < 600 ? size : 600, rotation: angle) { + contents() } + .padding(.leading, idiom == .phone ? 0 : 20) + Spacer() } - .frame(maxWidth: .infinity, maxHeight: .infinity) + .scaledToFit() } - VStack { - /// Distance - if selectedRoute?.node?.positions?.count ?? 0 > 0, - selectedRoute?.coordinate != nil, - let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { - - let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) - - if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { - let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) - Label { - Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))") - .foregroundColor(.primary) - } icon: { - Image(systemName: "lines.measurement.horizontal") - .symbolRenderingMode(.hierarchical) - } + .onAppear { + // Set the view rotation animation after the view appeared, + // to avoid animating initial rotation + DispatchQueue.main.async { + indexes = (selectedRoute?.hops?.array.count ?? 0) * 2 + animation = .easeInOut(duration: 1.0) + withAnimation(.easeInOut(duration: 2.0)) { + angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90)) } } } - } else { - VStack { - Label { - Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") - } icon: { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) + .onTapGesture { + withAnimation(.easeInOut(duration: 2.0)) { + angle = (angle == .degrees(-90) ? .degrees(90) : .degrees(-90)) } - .font(.title3) - Spacer() } } + if selectedRoute?.hasPositions ?? false { +// Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { +// Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { +// ZStack { +// Circle() +// .fill(Color(.green)) +// .strokeBorder(.white, lineWidth: 3) +// .frame(width: 15, height: 15) +// } +// } +// .annotationTitles(.automatic) +// // Direct Trace Route +// if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { +// if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { +// let traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] +// Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { +// ZStack { +// Circle() +// .fill(Color(.black)) +// .strokeBorder(.white, lineWidth: 3) +// .frame(width: 15, height: 15) +// } +// } +// let dashed = StrokeStyle( +// lineWidth: 2, +// lineCap: .round, lineJoin: .round, dash: [7, 10] +// ) +// MapPolyline(coordinates: traceRouteCoords) +// .stroke(.blue, style: dashed) +// } +// } +// } +// .frame(maxWidth: .infinity, minHeight: 250) +// if selectedRoute?.response ?? false { +// VStack { +// /// Distance +// if selectedRoute?.node?.positions?.count ?? 0 > 0, +// selectedRoute?.coordinate != nil, +// let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { +// let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) +// if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { +// let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) +// Label { +// Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))") +// .foregroundColor(.primary) +// } icon: { +// Image(systemName: "lines.measurement.horizontal") +// .symbolRenderingMode(.hierarchical) +// } +// } +// } +// } +// } + Spacer() + .padding(.bottom, 125) + } } else { ContentUnavailableView("Select a Trace Route", systemImage: "signpost.right.and.left") } } - Spacer() + .edgesIgnoringSafeArea(.bottom) } .navigationTitle("Trace Route Log") } @@ -143,4 +215,69 @@ struct TraceRouteLog: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) } + @ViewBuilder func contents(animation: Animation? = nil) -> some View { + ForEach(0.. [TraceRouteHopEntity] { + /// static let context = PersistenceController.preview.container.viewContext + var array = [TraceRouteHopEntity]() + let trh1 = TraceRouteHopEntity(context: context) + trh1.num = 366311664 + trh1.snr = 12.5 + let trh2 = TraceRouteHopEntity(context: context) + trh2.num = 3662955168 + trh2.snr = -115.00 + let trh3 = TraceRouteHopEntity(context: context) + trh3.num = 3663982804 + trh3.snr = 17.5 + let trh4 = TraceRouteHopEntity(context: context) + trh4.num = 4202719792 + trh4.snr = 7.0 + let trh5 = TraceRouteHopEntity(context: context) + trh5.num = 603700594 + trh5.snr = 8.9 + let trh6 = TraceRouteHopEntity(context: context) + trh6.num = 836212501 + trh6.snr = -24.0 + let trh7 = TraceRouteHopEntity(context: context) + trh7.num = 3663116644 + trh7.snr = -6.0 + let trh8 = TraceRouteHopEntity(context: context) + trh8.num = 8362955168 + trh8.snr = 7.5 + array.append(trh1) + array.append(trh2) + array.append(trh3) + array.append(trh4) + array.append(trh5) + array.append(trh6) + array.append(trh7) + array.append(trh8) + return array } diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index 8fff8979c..aeee96015 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -56,7 +56,7 @@ struct StoreForwardConfig: View { } VStack { if isRouter { - Text("Store and forward router devices must also be in the router or router client device role and requires a ESP32 device with PSRAM.") + Text("Store and forward router devices require a ESP32 device with PSRAM.") .foregroundColor(.gray) .font(.callout) } else { diff --git a/Meshtastic/Views/Settings/Logs/LogDetail.swift b/Meshtastic/Views/Settings/Logs/LogDetail.swift index 8006127e3..ca5d17d69 100644 --- a/Meshtastic/Views/Settings/Logs/LogDetail.swift +++ b/Meshtastic/Views/Settings/Logs/LogDetail.swift @@ -37,11 +37,13 @@ struct LogDetail: View { List { /// Time Label { - Text("log.time".localized + ":") - .font(idiom == .phone ? .caption : .title) - .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) - Text(log.date.formatted(dateFormatStyle)) - .font(idiom == .phone ? .caption : .title) + HStack { + Text("log.time".localized + ":") + .font(idiom == .phone ? .caption : .title) + .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) + Text(log.date.formatted(dateFormatStyle)) + .font(idiom == .phone ? .caption : .title) + } } icon: { Image(systemName: "timer") .symbolRenderingMode(.hierarchical) @@ -53,11 +55,13 @@ struct LogDetail: View { .listSectionSeparator(.visible, edges: .bottom) /// Subsystem Label { - Text("log.subsystem".localized + ":") - .font(idiom == .phone ? .caption : .title) - .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) - Text(log.subsystem) - .font(idiom == .phone ? .caption : .title) + HStack { + Text("log.subsystem".localized + ":") + .font(idiom == .phone ? .caption : .title) + .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) + Text(log.subsystem) + .font(idiom == .phone ? .caption : .title) + } } icon: { Image(systemName: "gear") .symbolRenderingMode(.hierarchical) @@ -68,11 +72,13 @@ struct LogDetail: View { .listRowSeparator(.visible) /// Process Label { - Text("log.process".localized + ":") - .font(idiom == .phone ? .caption : .title) - .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) - Text(log.process) - .font(idiom == .phone ? .caption : .title) + HStack { + Text("log.process".localized + ":") + .font(idiom == .phone ? .caption : .title) + .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) + Text(log.process) + .font(idiom == .phone ? .caption : .title) + } } icon: { Image(systemName: "tag") .symbolRenderingMode(.hierarchical) @@ -83,12 +89,13 @@ struct LogDetail: View { .listRowSeparator(.visible) /// Level Label { - Text("log.level".localized + ":") - .font(idiom == .phone ? .caption : .title) - .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) - Text(log.level.description) - .font(idiom == .phone ? .caption : .title) - .foregroundStyle(log.level.color) + HStack { + Text("log.level".localized + ":") + .font(idiom == .phone ? .caption : .title) + .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) + Text(log.level.description) + .font(idiom == .phone ? .caption : .title) + .foregroundStyle(log.level.color) } } icon: { Image(systemName: "stairs") .symbolRenderingMode(.hierarchical) @@ -99,11 +106,13 @@ struct LogDetail: View { .listRowSeparator(.visible) /// Category Label { - Text("log.category".localized + ":") - .font(idiom == .phone ? .caption : .title) - .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) - Text(log.category) - .font(idiom == .phone ? .caption : .title) + HStack { + Text("log.category".localized + ":") + .font(idiom == .phone ? .caption : .title) + .frame(width: idiom == .phone ? 115 : 190, alignment: .trailing) + Text(log.category) + .font(idiom == .phone ? .caption : .title) + } } icon: { Image(systemName: "square.grid.2x2") .symbolRenderingMode(.hierarchical)