Wheel of traceroute

This commit is contained in:
Garth Vander Houwen 2024-09-24 16:04:14 -07:00
parent 1de64e6659
commit 78903f442a
4 changed files with 150 additions and 116 deletions

View file

@ -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" : {

View file

@ -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())! {

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G5075b" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="23G5075b" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
@ -428,10 +428,12 @@
</entity>
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="back" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
</entity>

View file

@ -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..<hops, id: \.self) { idx in
ForEach(selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? [], id: \.id) { idx in
TraceRouteComponent(animation: animation) {
if idx % 2 == 0 {
VStack {
let nodeColor = UIColor(hex: UInt32(truncatingIfNeeded: nums[idx%nums.count]))
CircleText(text: String(nums[idx%nums.count].toHex().suffix(4)), color: Color(nodeColor), circleSize: 70)
if idx > 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))
}
}
}