diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9650f2ca..3062e336 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -15,6 +15,9 @@ }, " Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin" : { + }, + "-12dB" : { + }, ": %@" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 353226be..c8a3006f 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -100,6 +100,7 @@ DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; }; DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; }; DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; + DD6D5A332CA1178300ED3032 /* TraceRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D5A322CA1178300ED3032 /* TraceRoute.swift */; }; DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65712C6AB8EC0053C113 /* SecureInput.swift */; }; DD6F65742C6CB80A0053C113 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65732C6CB80A0053C113 /* View.swift */; }; DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65752C6EA5490053C113 /* AckErrors.swift */; }; @@ -365,6 +366,8 @@ DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = ""; }; + DD6D5A322CA1178300ED3032 /* TraceRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRoute.swift; sourceTree = ""; }; + DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 45.xcdatamodel"; sourceTree = ""; }; DD6F65712C6AB8EC0053C113 /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = ""; }; DD6F65732C6CB80A0053C113 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; DD6F65752C6EA5490053C113 /* AckErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AckErrors.swift; sourceTree = ""; }; @@ -747,6 +750,14 @@ path = Module; sourceTree = ""; }; + DD6D5A312CA1176A00ED3032 /* Layouts */ = { + isa = PBXGroup; + children = ( + DD6D5A322CA1178300ED3032 /* TraceRoute.swift */, + ); + path = Layouts; + sourceTree = ""; + }; DD6F65772C6EAB860053C113 /* Help */ = { isa = PBXGroup; children = ( @@ -901,6 +912,7 @@ DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( + DD6D5A312CA1176A00ED3032 /* Layouts */, C9483F6B2773016700998F6B /* MapKitMap */, DDC2E18D26CE25CB0042C5E4 /* Helpers */, DD47E3D726F2F21A00029299 /* Bluetooth */, @@ -1433,6 +1445,7 @@ DD6F65742C6CB80A0053C113 /* View.swift in Sources */, DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, + DD6D5A332CA1178300ED3032 /* TraceRoute.swift in Sources */, DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */, BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */, DD73FD1128750779000852D6 /* PositionLog.swift in Sources */, @@ -1899,6 +1912,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */, DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */, DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */, DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */, @@ -1944,7 +1958,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */; + currentVersion = DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 8a88003b..4269da21 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 44.xcdatamodel + MeshtasticDataModelV 45.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents index 98de0347..ae7c08e4 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 44.xcdatamodel/contents @@ -428,11 +428,14 @@ + + + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents new file mode 100644 index 00000000..98de0347 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 45.xcdatamodel/contents @@ -0,0 +1,476 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Views/Layouts/TraceRoute.swift b/Meshtastic/Views/Layouts/TraceRoute.swift new file mode 100644 index 00000000..bb79f747 --- /dev/null +++ b/Meshtastic/Views/Layouts/TraceRoute.swift @@ -0,0 +1,68 @@ +// +// TraceRoute.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 9/22/24. +// +import SwiftUI + +struct Rotation: LayoutValueKey { + static let defaultValue: Binding? = nil +} + +struct TraceRouteComponent: View { + var animation: Animation? + @ViewBuilder let content: () -> V + @State private var rotation: Angle = .zero + + var body: some View { + content() + .rotationEffect(rotation) + .layoutValue(key: Rotation.self, value: $rotation.animation(animation)) + } +} + +struct TraceRoute: Layout { + var animatableData: AnimatablePair { + get { + AnimatablePair(rotation.radians, radius) + } + set { + rotation = Angle.radians(newValue.first) + radius = newValue.second + } + } + + var radius: CGFloat + var rotation: Angle + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let maxSize = subviews.map { $0.sizeThatFits(proposal) }.reduce(CGSize.zero) { + return CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height)) + } + return CGSize(width: (maxSize.width / 2 + radius) * 2, + height: (maxSize.height / 2 + radius) * 2) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + let angleStep = (Angle.degrees(360).radians / Double(subviews.count)) + + for (index, subview) in subviews.enumerated() { + let angle = angleStep * CGFloat(index) + rotation.radians + + var point = CGPoint(x: 0, y: -radius).applying(CGAffineTransform(rotationAngle: angle)) + point.x += bounds.midX + point.y += bounds.midY + + subview.place(at: point, anchor: .center, proposal: .unspecified) + + DispatchQueue.main.async { + if index % 2 == 0 { + subview[Rotation.self]?.wrappedValue = .zero + } else { + subview[Rotation.self]?.wrappedValue = .radians(angle) + } + } + } + } +} diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 1f2cc017..25347c25 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -30,7 +30,7 @@ struct MessageText: View { .background(isCurrentUser ? .accentColor : Color(.gray)) .cornerRadius(15) .overlay { - if message.pkiEncrypted && message.ackError == 0 && message.realACK { + if message.pkiEncrypted && message.ackError <= 0 && (message.realACK || message.ackError == nil) { VStack(alignment: .trailing) { Spacer() HStack { diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 345a4299..0f947ecf 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -15,7 +15,6 @@ struct TraceRouteLog: View { @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @State private var isPresentingClearLogConfirm: Bool = false @State var isExporting = false @State var exportString = "" @@ -26,13 +25,21 @@ 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) + /// State for the circle of routes + @State var angle: Angle = .zero + @State var radius: CGFloat = 230.00 + @State var animation: Animation? var body: some View { HStack(alignment: .top) { VStack { VStack { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in - Label { Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : "No Response")") } icon: { @@ -45,8 +52,33 @@ struct TraceRouteLog: View { .frame(minHeight: 200, maxHeight: 230) VStack { if selectedRoute != nil { + if true {// selectedRoute?.hops?.count ?? 2 > 3 { + VStack { + Spacer() + HStack(spacing: 15) { + TraceRoute(radius: radius, rotation: angle) { + contents() + } + } + .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)) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { - Label { Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") } icon: { @@ -94,8 +126,6 @@ struct TraceRouteLog: View { MapPolyline(coordinates: traceRouteCoords) .stroke(.blue, style: dashed) } - } else if selectedRoute?.hops?.count ?? 0 == 0 { - } } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -104,10 +134,10 @@ struct TraceRouteLog: View { /// Distance if selectedRoute?.node?.positions?.count ?? 0 > 0, selectedRoute?.coordinate != nil, - let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { - + 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 { @@ -145,4 +175,26 @@ struct TraceRouteLog: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) } + @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)) + } + } + } + } }