From bd5191ccd2bacb44379d17bc9bf35e887aecdeb6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 8 Dec 2023 11:41:29 -0800 Subject: [PATCH] Initial trace route log --- Meshtastic.xcodeproj/project.pbxproj | 8 ++ .../CoreData/TraceRouteEntityExtension.swift | 71 ++++++++++++ Meshtastic/Helpers/BLEManager.swift | 78 +++++++++++-- .../contents | 28 ++++- Meshtastic/Persistence/QueryCoreData.swift | 17 +++ .../Views/Nodes/Helpers/NodeDetail.swift | 16 ++- Meshtastic/Views/Nodes/TraceRouteLog.swift | 108 ++++++++++++++++++ 7 files changed, 316 insertions(+), 10 deletions(-) create mode 100644 Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift create mode 100644 Meshtastic/Views/Nodes/TraceRouteLog.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e7b9c1b7..2ad60579 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -173,6 +173,8 @@ 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 */; }; + DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */; }; + DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.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 */; }; @@ -407,6 +409,8 @@ 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 = ""; }; + DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteLog.swift; sourceTree = ""; }; + DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteEntityExtension.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 = ""; }; @@ -485,6 +489,7 @@ DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */, DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */, DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */, + DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */, ); path = CoreData; sourceTree = ""; @@ -515,6 +520,7 @@ DD73FD1028750779000852D6 /* PositionLog.swift */, DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, + DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */, ); path = Nodes; sourceTree = ""; @@ -1152,6 +1158,7 @@ DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */, DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */, DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */, + DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */, DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, @@ -1263,6 +1270,7 @@ DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */, DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */, DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */, + DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift b/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift new file mode 100644 index 00000000..4e7cdb60 --- /dev/null +++ b/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift @@ -0,0 +1,71 @@ +// +// TraceRouteEntityExtension.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 12/7/23. +// + +import CoreData +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? { + + 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 + } + } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 524aa226..c82b4796 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -376,6 +376,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let fromNodeNum = connectedPeripheral.num let routePacket = RouteDiscovery() var meshPacket = MeshPacket() + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. = NSFetchRequest.init(entityName: "NodeInfoEntity") + nodes.predicate = NSPredicate(format: "num IN %@", [destNum, self.connectedPeripheral.num]) + do { + guard let fetchedNodes = try context!.fetch(nodes) as? [NodeInfoEntity] else { + return false + } + let receivingNode = fetchedNodes.first(where: { $0.num == destNum }) + let connectedNode = fetchedNodes.first(where: { $0.num == self.connectedPeripheral.num }) + + 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: .minute, value: -60, to: Date())! { + traceRoute.altitude = mostRecent.altitude + traceRoute.latitudeI = mostRecent.latitudeI + traceRoute.longitudeI = mostRecent.longitudeI + } + } + do { + try context!.save() + print("πŸ’Ύ Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "unknown".localized))") + } catch { + context!.rollback() + let nsError = error as NSError + print("πŸ’₯ Error Updating Core Data BluetoothConfigEntity: \(nsError)") + } + + let logString = String.localizedStringWithFormat("mesh.log.traceroute.sent %@".localized, String(destNum)) + MeshLogger.log("πŸͺ§ \(logString)") + + } catch { + + } } return success } @@ -595,16 +631,44 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate MeshLogger.log("πŸ•ΈοΈ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .tracerouteApp: if let routingMessage = try? RouteDiscovery(serializedData: 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)) MeshLogger.log("πŸͺ§ \(logString)") - } else { - var routeString = "\(decodedInfo.packet.to) --> " - for node in routingMessage.route { - routeString += "\(node) --> " + } else { + var routeString = "You --> " + var hopNodes: [TraceRouteHopEntity] = [] +// for node in routingMessage.route { +// let hopNode = getNodeInfo(id: Int64(node), context: context!) +// let traceRouteHop = TraceRouteHopEntity(context: context!) +// traceRouteHop.time = Date() +// let mostRecent = hopNode?.positions?.lastObject as! PositionEntity +// if mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { +// traceRouteHop.altitude = mostRecent.altitude +// traceRouteHop.latitudeI = mostRecent.latitudeI +// traceRouteHop.longitudeI = mostRecent.longitudeI +// traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized +// } +// traceRouteHop.num = hopNode?.num ?? 0 +// if hopNode != nil { +// hopNodes.append(traceRouteHop) +// } +// routeString += "\(hopNode?.user?.longName ?? "unknown".localized) --> " +// } + traceRoute?.routeText = routeString + traceRoute?.hops = NSOrderedSet(array: hopNodes) + do { + try context!.save() + print("πŸ’Ύ Saved Trace Route") + } catch { + context!.rollback() + let nsError = error as NSError + print("πŸ’₯ Error Updating Core Data TraceRouteHOp: \(nsError)") } + routeString += "\(decodedInfo.packet.from)" let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString) MeshLogger.log("πŸͺ§ \(logString)") diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents index fc942300..d30a2970 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -112,6 +112,9 @@ + + + @@ -244,6 +247,7 @@ + @@ -340,6 +344,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Persistence/QueryCoreData.swift b/Meshtastic/Persistence/QueryCoreData.swift index 32cbffc0..502aa61a 100644 --- a/Meshtastic/Persistence/QueryCoreData.swift +++ b/Meshtastic/Persistence/QueryCoreData.swift @@ -46,6 +46,23 @@ public func getStoreAndForwardMessageIds(seconds: Int, context: NSManagedObjectC return [] } +public func getTraceRoute(id: Int64, context: NSManagedObjectContext) -> TraceRouteEntity? { + + let fetchTraceRouteRequest: NSFetchRequest = NSFetchRequest.init(entityName: "TraceRouteEntity") + fetchTraceRouteRequest.predicate = NSPredicate(format: "id == %lld", Int64(id)) + + do { + guard let fetchedTraceRoute = try context.fetch(fetchTraceRouteRequest) as? [TraceRouteEntity] else { + return nil + } + if fetchedTraceRoute.count == 1 { + return fetchedTraceRoute[0] + } + } catch { + return nil + } + return nil +} public func getUser(id: Int64, context: NSManagedObjectContext) -> UserEntity { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index b644f2b6..ab5765dc 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -93,9 +93,21 @@ struct NodeDetail: View { } .disabled(!node.hasDetectionSensorMetrics) Divider() + if #available(iOS 17.0, macOS 14.0, *) { + NavigationLink { + TraceRouteLog(node: node) + } label: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Trace Route Log") + .font(.title3) + } + .disabled(node.traceRoutes?.count ?? 0 == 0) + Divider() + } } - - if self.bleManager.connectedPeripheral != nil && node.metadata != nil { HStack { if node.metadata?.canShutdown ?? false { diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift new file mode 100644 index 00000000..8f139f77 --- /dev/null +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -0,0 +1,108 @@ +// +// TraceRouteLog.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 12/7/23. +// + +import SwiftUI +#if canImport(MapKit) +import MapKit +#endif + +@available(iOS 17.0, macOS 14.0, *) +struct TraceRouteLog: View { + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + @State private var isPresentingClearLogConfirm: Bool = false + @State var isExporting = false + @State var exportString = "" + @ObservedObject var node: NodeInfoEntity + @State private var selectedRoute: TraceRouteEntity? + + var body: some View { + VStack { + VStack { + List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "Other") : "No Response")") + } + .listStyle(.plain) + } + .navigationTitle("Trace Route List") + VStack { + if selectedRoute != nil { + Divider() + if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { + Text("Trace Route received by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") + .font(.title) + } else if selectedRoute?.response ?? false { + Text("Trace Route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + .font(.title) + } + let hopsArray = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? [] + let lineCoords = hopsArray.compactMap({(hop) -> CLLocationCoordinate2D in + return hop.coordinate ?? LocationHelper.DefaultLocation + }) + if selectedRoute?.response ?? false { + Map() { + 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 + var 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: 3, + lineCap: .round, lineJoin: .round, dash: [7, 10] + ) + MapPolyline(coordinates: traceRouteCoords) + .stroke(.blue, style: dashed) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + Text("Trace Route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + .font(.title) + .padding(.top) + Spacer() + Text("\(selectedRoute?.time?.formatted() ?? "")") + .font(.title3) + Spacer() + Text("No response") + .font(.title2) + Spacer() + } + } + } + .navigationTitle("Route Details") + } + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + }) + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + } + } +}