diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index c8dc8f3d..e251c64c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -260,6 +260,18 @@ struct NodeDetail: View { } .disabled(!node.hasDeviceMetrics) + NavigationLink{ + PowerMetricsLog(node: node) + } label: { + Label { + Text("Power Metrics Log") + } icon: { + Image(systemName: "bolt") + .symbolRenderingMode(.multicolor) + } + } + .disabled(!node.hasPowerMetrics) + NavigationLink { NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num) } label: { diff --git a/Meshtastic/Views/Nodes/PowerMetricsLog.swift b/Meshtastic/Views/Nodes/PowerMetricsLog.swift new file mode 100644 index 00000000..9feb4b98 --- /dev/null +++ b/Meshtastic/Views/Nodes/PowerMetricsLog.swift @@ -0,0 +1,134 @@ +// +// PowerMetricsLog.swift +// Meshtastic +// +// Created by Matthew Davies on 1/24/25. +// + +import Foundation +import SwiftUI +import Charts + +struct PowerMetricsLog: View { + + @ObservedObject var node: NodeInfoEntity + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @State private var sortOrder = [KeyPathComparator(\TelemetryEntity.time, order: .reverse)] + @State private var selection: TelemetryEntity.ID? + @State private var chartSelection: Date? + + + @State private var channelSelection = 0 + + var body: some View { + VStack { + if node.hasPowerMetrics { + let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) + let powerMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 2")).reversed() as? [TelemetryEntity] ?? [] + let chartData = powerMetrics + .filter { $0.time != nil && $0.time! >= oneWeekAgo! } + .sorted { $0.time! < $1.time! } + if chartData.count > 0 { + GroupBox(label: Label("\(powerMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) { + + // allow switching between different channels + Picker("Select Channel", selection: $channelSelection) { + Text("Channel 1").tag(0) + Text("Channel 2").tag(1) + Text("Channel 3").tag(2) + } + + Chart { + ForEach(chartData, id: \.self) { point in + + let voltage = channelSelection == 0 ? point.powerCh1Voltage : channelSelection == 1 ? point.powerCh2Voltage : point.powerCh3Voltage + let current = channelSelection == 0 ? point.powerCh1Current : channelSelection == 1 ? point.powerCh2Current : point.powerCh3Current + + LineMark( + x: .value("Time", point.time ?? Date()), + y: .value("Voltage", voltage) + ) + .foregroundStyle(by: .value("Series", "Voltage")) + .interpolationMethod(.linear) + .accessibilityLabel("Voltage") + .accessibilityValue("X: \(point.time ?? Date()), Y: \(voltage)") + + LineMark( + x: .value("Time", point.time ?? Date()), + y: .value("Current", current) + ) + .foregroundStyle(by: .value("Series", "Current")) + .interpolationMethod(.linear) + .accessibilityLabel("Current") + .accessibilityValue("X: \(point.time ?? Date()), Y: \(current)") + + } + + if let chartSelection { + RuleMark(x: .value("Second", chartSelection, unit: .second)) + .foregroundStyle(.tertiary.opacity(0.5)) + } + + } + .chartXAxis(content: { + AxisMarks(position: .top) + }) + .chartXAxis(.automatic) + .chartXSelection(value: $chartSelection) + .chartYScale(domain: -10...10) + .chartForegroundStyleScale([ + "Voltage": .blue, + "Current": .green + ]) + .chartLegend(position: .automatic, alignment: .bottom) + + } + } + let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMdjmma", options: 0, locale: Locale.current) + let dateFormatString = (localeDateFormat ?? "M/d/YY j:mma").replacingOccurrences(of: ",", with: "") + + if idiom == .phone { + } else { + Table(powerMetrics, selection: $selection, sortOrder: $sortOrder) { + TableColumn("Ch1 Voltage") { dm in + Text("\(String(format: "%.2f", dm.powerCh1Voltage))V") + } + .width(min: 75) + TableColumn("Ch1 Current") { dm in + Text("\(String(format: "%.2f", dm.powerCh1Current))mA") + } + .width(min: 75) + TableColumn("Ch2 Voltage") { dm in + Text("\(String(format: "%.2f", dm.powerCh2Voltage))V") + } + .width(min: 75) + TableColumn("Ch2 Current") { dm in + Text("\(String(format: "%.2f", dm.powerCh2Current))mA") + } + .width(min: 75) + TableColumn("Ch3 Voltage") { dm in + Text("\(String(format: "%.2f", dm.powerCh3Voltage))V") + } + .width(min: 75) + TableColumn("Ch3 Current") { dm in + Text("\(String(format: "%.2f", dm.powerCh3Current))mA") + } + .width(min: 75) + TableColumn("timestamp") { dm in + Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) + } + .width(min: 180) + + } + .onChange(of: selection) { _, newSelection in + guard let metrics = powerMetrics.first(where: { $0.id == newSelection }) else { + return + } + chartSelection = metrics.time + } + + } + } + } + } +}