Initial trace route log

This commit is contained in:
Garth Vander Houwen 2023-12-08 11:41:29 -08:00
parent 126cdfbdb3
commit bd5191ccd2
7 changed files with 316 additions and 10 deletions

View file

@ -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 = "<group>"; };
DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = "<group>"; };
DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = "<group>"; };
DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteLog.swift; sourceTree = "<group>"; };
DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteEntityExtension.swift; sourceTree = "<group>"; };
DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRecorder.swift; sourceTree = "<group>"; };
DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = "<group>"; };
DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = "<group>"; };
@ -485,6 +489,7 @@
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */,
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */,
DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */,
DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */,
);
path = CoreData;
sourceTree = "<group>";
@ -515,6 +520,7 @@
DD73FD1028750779000852D6 /* PositionLog.swift */,
DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */,
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */,
DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */,
);
path = Nodes;
sourceTree = "<group>";
@ -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;

View file

@ -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
}
}
}

View file

@ -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)..<UInt32.max)
meshPacket.to = UInt32(destNum)
meshPacket.from = UInt32(fromNodeNum)
meshPacket.wantAck = true
@ -394,8 +395,43 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
success = true
let logString = String.localizedStringWithFormat("mesh.log.traceroute.sent %@".localized, String(destNum))
MeshLogger.log("🪧 \(logString)")
let traceRoute = TraceRouteEntity(context: context!)
let nodes: NSFetchRequest<NSFetchRequestResult> = 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)")

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="22225" systemVersion="23C5047e" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23C64" 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"/>
@ -112,6 +112,9 @@
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="fetchedProperty" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="ExternalNotificationConfigEntity"/>
</fetchedProperty>
</entity>
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
@ -244,6 +247,7 @@
<relationship name="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
<relationship name="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
@ -340,6 +344,28 @@
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" 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="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="route" optional="YES" attributeType="Transformable" customClassName="[UInt32]"/>
<attribute name="routeText" optional="YES" attributeType="String"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="time" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
</entity>
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
<attribute name="hwModel" attributeType="String"/>
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>

View file

@ -46,6 +46,23 @@ public func getStoreAndForwardMessageIds(seconds: Int, context: NSManagedObjectC
return []
}
public func getTraceRoute(id: Int64, context: NSManagedObjectContext) -> TraceRouteEntity? {
let fetchTraceRouteRequest: NSFetchRequest<NSFetchRequestResult> = 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 {

View file

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

View file

@ -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
}
}
}
}