mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Wheel of traceroute
This commit is contained in:
parent
1de64e6659
commit
78903f442a
4 changed files with 150 additions and 116 deletions
|
|
@ -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" : {
|
||||
|
||||
|
|
|
|||
|
|
@ -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())! {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue