diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index bdd516a9..3261cbc3 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -68,6 +68,27 @@ func detectionsToCsv(detections: [MessageEntity]) -> String { return csvString } +func paxToCsvFile(pax: [PaxCounterEntity]) -> String { + var csvString: String = "" + let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") + // Create PAX Header + csvString = "BLE, WiFi, Total Pax, Uptime, \("timestamp".localized)" + for p in pax { + csvString += "\n" + csvString += String(p.ble) + csvString += ", " + csvString += String(p.wifi) + csvString += ", " + csvString += String(p.ble + p.wifi) + csvString += ", " + csvString += String(p.uptime) + csvString += ", " + csvString += p.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized + } + return csvString +} + func positionToCsvFile(positions: [PositionEntity]) -> String { var csvString: String = "" let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 6c9254fa..b0a0905d 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -31,6 +31,11 @@ extension NodeInfoEntity { return traceRoutes?.count ?? 0 > 0 } + var hasPax: Bool { + return pax?.count ?? 0 > 0 + } + + var isStoreForwardRouter: Bool { return storeForwardConfig?.isRouter ?? false } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 341ab69f..04f4d4fc 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -569,6 +569,7 @@ func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) { return } mutablePax.add(newPax) + fetchedNode![0].pax = mutablePax do { try context.save() } catch { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index ab5765dc..faa633a3 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -81,18 +81,6 @@ struct NodeDetail: View { } .disabled(!node.hasEnvironmentMetrics) Divider() - NavigationLink { - DetectionSensorLog(node: node) - } label: { - Image(systemName: "sensor") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Detection Sensor Log") - .font(.title3) - } - .disabled(!node.hasDetectionSensorMetrics) - Divider() if #available(iOS 17.0, macOS 14.0, *) { NavigationLink { TraceRouteLog(node: node) @@ -107,6 +95,32 @@ struct NodeDetail: View { .disabled(node.traceRoutes?.count ?? 0 == 0) Divider() } + NavigationLink { + DetectionSensorLog(node: node) + } label: { + Image(systemName: "sensor") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Detection Sensor Log") + .font(.title3) + } + .disabled(!node.hasDetectionSensorMetrics) + Divider() + if node.hasPax { + NavigationLink { + PaxCounterLog(node: node) + } label: { + Image(systemName: "figure.walk.motion") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("paxcounter.log") + .font(.title3) + } + .disabled(!node.hasPax) + Divider() + } } if self.bleManager.connectedPeripheral != nil && node.metadata != nil { HStack { diff --git a/Meshtastic/Views/Nodes/PaxCounterLog.swift b/Meshtastic/Views/Nodes/PaxCounterLog.swift index 8075a8b1..45c4bbed 100644 --- a/Meshtastic/Views/Nodes/PaxCounterLog.swift +++ b/Meshtastic/Views/Nodes/PaxCounterLog.swift @@ -5,4 +5,227 @@ // Created by Garth Vander Houwen on 2/25/24. // -import Foundation +import SwiftUI +import Charts + +struct PaxCounterLog: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + @State private var isPresentingClearLogConfirm: Bool = false + @State var isExporting = false + @State var exportString = "" + + @State private var bleChartColor: Color = .blue + @State private var wifiChartColor: Color = .orange + @State private var paxChartColor: Color = .green + @ObservedObject var node: NodeInfoEntity + + var body: some View { + VStack { + if node.hasPax { + + let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) + let pax = node.pax?.reversed() as? [PaxCounterEntity] ?? [] + let chartData = pax + .filter { $0.time != nil && $0.time! >= oneWeekAgo! } + .sorted { $0.time! < $1.time! } + let maxValue = (chartData.map{ $0.wifi }.max() ?? 0) + (chartData.map{ $0.ble }.max() ?? 0) + 5 + if chartData.count > 0 { + GroupBox(label: Label("\(pax.count) Readings Total", systemImage: "chart.xyaxis.line")) { + + Chart { + ForEach(chartData, id: \.self) { point in + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", (point.wifi + point.ble)) + ) + } + .accessibilityLabel("paxcounter.total") + .accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)") + .foregroundStyle(paxChartColor) + .interpolationMethod(.cardinal) + + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.wifi) + ) + } + .accessibilityLabel("paxcounter.wifi") + .accessibilityValue("X: \(point.time!), Y: \(point.wifi)") + .foregroundStyle(wifiChartColor) + + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.ble) + ) + } + .accessibilityLabel("paxcounter.ble") + .accessibilityValue("X: \(point.time!), Y: \(point.ble)") + .foregroundStyle(bleChartColor) + } + } + .chartXAxis(content: { + AxisMarks(position: .top) + }) + .chartXAxis(.automatic) + .chartYScale(domain: 0...maxValue) + .chartForegroundStyleScale([ + "BLE": .blue, + "WiFi": .orange, + "paxcounter.total".localized: .green + ]) + .chartLegend(position: .automatic, alignment: .bottom) + } + .frame(minHeight: 250) + } + let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") + if UIScreen.main.bounds.size.width > 768 && (UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac) { + // Add a table for mac and ipad + Table(pax) { + TableColumn("paxcounter.ble") { pc in + Text("\(pc.ble)") + } + TableColumn("paxcounter.wifi") { pc in + Text("\(pc.wifi)") + } + TableColumn("paxcounter.total") { pc in + Text("\(pc.wifi + pc.ble)") + } + TableColumn("paxcounter.uptime") { pc in + let now = Date.now + let later = now + TimeInterval(pc.uptime) + let components = (now..