From 78903f442ab43bfed5f0085291fc5a5ef9123a7c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 24 Sep 2024 16:04:14 -0700 Subject: [PATCH] Wheel of traceroute --- Localizable.xcstrings | 14 +- Meshtastic/Helpers/BLEManager.swift | 36 ++- .../contents | 4 +- Meshtastic/Views/Nodes/TraceRouteLog.swift | 212 +++++++++--------- 4 files changed, 150 insertions(+), 116 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 391fdce4..95809869 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15,9 +15,6 @@ }, " Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin" : { - }, - "-12dB" : { - }, ": %@" : { @@ -21859,8 +21856,15 @@ "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" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index aae1a8ab..63cf1883 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -825,7 +825,7 @@ 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 @@ -836,13 +836,45 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } else { var routeString = "You --> " var hopNodes: [TraceRouteHopEntity] = [] - for node in routingMessage.route { + 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) traceRouteHop.time = Date() + traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) + 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.altitude = mostRecent.altitude + traceRouteHop.latitudeI = mostRecent.latitudeI + traceRouteHop.longitudeI = mostRecent.longitudeI + traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized + } else { + traceRoute?.hasPositions = false + } + } else { + traceRoute?.hasPositions = false + } + 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 --> " + } + 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() + traceRouteHop.back = true + traceRouteHop.snr = Float(routingMessage.snrBack[index] / 4) 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())! { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents index 98de0347..265c23f8 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -428,10 +428,12 @@ + + diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 9498f3e6..87e101f1 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -12,6 +12,7 @@ import MapKit @available(iOS 17.0, macOS 14.0, *) struct TraceRouteLog: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -25,14 +26,9 @@ 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() - /// Mockup Values - let colors: [Color] = [.yellow, .orange, .red, .pink, .purple, .blue, .cyan, .green] - let nums: [Int64] = [366311664, 0, 3662955168, 0, 3663982804, 0, 4202719792, 0, 603700594, 0, 836212501, 0, 3663116644, 0, 8362955168] - let snr: [Double] = [-115.00, 17.5, 7.0, 8.9, -24.0, 5.5, 6.0, 7.5] - @State private var hops: Int = 16 /// Max of 16 (2 8 hop routes) + let modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast /// State for the circle of routes @State var angle: Angle = .zero - @State var radius: CGFloat = 175.00 @State var animation: Animation? var body: some View { @@ -49,8 +45,9 @@ struct TraceRouteLog: View { } .listStyle(.plain) } - .frame(minHeight: 200, maxHeight: 230) - VStack { + .frame(minHeight: CGFloat(node.traceRoutes?.count ?? 0 * 40), maxHeight: 150) + Divider() + ScrollView { if selectedRoute != nil { if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { Label { @@ -59,112 +56,115 @@ struct TraceRouteLog: View { Image(systemName: "signpost.right.and.left") .symbolRenderingMode(.hierarchical) } - .font(.title2) + .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) - } - } - .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, maxHeight: .infinity) - } - 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) - } - } - } - } + .font(.title3) } else { VStack { - Label { - Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") - } icon: { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - } - .font(.title3) - Spacer() + Label { + Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + .font(idiom == .phone ? .headline : .largeTitle) } } - if true {// selectedRoute?.hops?.count ?? 2 > 3 { - VStack { - Spacer() - HStack(spacing: 15) { - TraceRoute(radius: radius, rotation: angle) { + if selectedRoute?.hops?.count ?? 2 > 3 { + HStack(alignment: .center) { + GeometryReader { geometry in + let size = ((geometry.size.width >= geometry.size.height ? geometry.size.height : geometry.size.width) / 2) - (idiom == .phone ? 50 : 70) + Spacer() + TraceRoute(radius: size, rotation: angle) { contents() } + .padding(.leading) } - .onAppear { - // Set the view rotation animation after the view appeared, - // to avoid animating initial rotation - DispatchQueue.main.async { - animation = .easeInOut(duration: 1.0) - withAnimation(.easeInOut(duration: 2.0)) { - angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90)) + .scaledToFit() + } + .onAppear { + // Set the view rotation animation after the view appeared, + // to avoid animating initial rotation + DispatchQueue.main.async { + animation = .easeInOut(duration: 1.0) + withAnimation(.easeInOut(duration: 2.0)) { + angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90)) + } + } + } + .onTapGesture { + withAnimation(.easeInOut(duration: 2.0)) { + angle = (angle == .degrees(-90) ? .degrees(90) : .degrees(-90)) + } + } + } + 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) + } } } } - .onTapGesture { - withAnimation(.easeInOut(duration: 2.0)) { - angle = (angle == .degrees(-90) ? .degrees(90) : .degrees(-90)) - } - } } - .frame(maxWidth: .infinity, maxHeight: .infinity) + Spacer() + .padding(.bottom, 125) } } else { ContentUnavailableView("Select a Trace Route", systemImage: "signpost.right.and.left") } } - Spacer() + .edgesIgnoringSafeArea(.bottom) } .navigationTitle("Trace Route Log") } @@ -174,24 +174,20 @@ struct TraceRouteLog: View { }) } @ViewBuilder func contents(animation: Animation? = nil) -> some View { - ForEach(0.. 0 { - Text("-12dB") - .font(.caption) - .foregroundColor(colors[idx%colors.count].opacity(0.7)) - } - } - } else { - Image(systemName: "arrowshape.right.fill") - .resizable() - .frame(width: 35, height: 35) - .foregroundColor(colors[idx%colors.count].opacity(0.7)) + let nodeColor = UIColor(hex: UInt32(truncatingIfNeeded: idx.num)) + let snrColor = getSnrColor(snr: idx.snr, preset: modemPreset) + VStack { + CircleText(text: String(idx.num.toHex().suffix(4)), color: Color(nodeColor), circleSize: idiom == .phone ? 70 : 100) + Text("\(String(format: "%.2f", idx.snr)) dB") + .font(idiom == .phone ? .caption : .headline) + .foregroundColor(snrColor) } + Image(systemName: "arrowshape.right.fill") + .resizable() + .frame(width: idiom == .phone ? 25 : 40, height: idiom == .phone ? 25 : 40) + .foregroundColor(snrColor.opacity(0.7)) } } }