mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Fix MQTT server port crash Adjust device metrics grid colums Fix some index issues in the channel editor Clean up settings view, add duty cycle warning
236 lines
7.4 KiB
Swift
236 lines
7.4 KiB
Swift
//
|
|
// DeviceMetricsLog.swift
|
|
// Meshtastic
|
|
//
|
|
// Copyright(c) Garth Vander Houwen 7/7/22.
|
|
//
|
|
import SwiftUI
|
|
import Charts
|
|
|
|
struct DeviceMetricsLog: 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 batteryChartColor: Color = .blue
|
|
@State private var airtimeChartColor: Color = .orange
|
|
@State private var channelUtilizationChartColor: Color = .green
|
|
@ObservedObject var node: NodeInfoEntity
|
|
|
|
var body: some View {
|
|
VStack {
|
|
if node.hasDeviceMetrics {
|
|
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
|
|
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).reversed() as? [TelemetryEntity] ?? []
|
|
let chartData = deviceMetrics
|
|
.filter { $0.time != nil && $0.time! >= oneWeekAgo! }
|
|
.sorted { $0.time! < $1.time! }
|
|
if chartData.count > 0 {
|
|
GroupBox(label: Label("\(deviceMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) {
|
|
|
|
Chart {
|
|
ForEach(chartData, id: \.self) { point in
|
|
Plot {
|
|
LineMark(
|
|
x: .value("x", point.time!),
|
|
y: .value("y", point.batteryLevel)
|
|
)
|
|
}
|
|
.accessibilityLabel("Line Series")
|
|
.accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)")
|
|
.foregroundStyle(batteryChartColor)
|
|
.interpolationMethod(.cardinal)
|
|
|
|
Plot {
|
|
PointMark(
|
|
x: .value("x", point.time!),
|
|
y: .value("y", point.channelUtilization)
|
|
)
|
|
}
|
|
.accessibilityLabel("Line Series")
|
|
.accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)")
|
|
.foregroundStyle(channelUtilizationChartColor)
|
|
|
|
RuleMark(y: .value("Limit", 10))
|
|
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
|
|
.foregroundStyle(airtimeChartColor)
|
|
|
|
Plot {
|
|
PointMark(
|
|
x: .value("x", point.time!),
|
|
y: .value("y", point.airUtilTx)
|
|
)
|
|
}
|
|
.accessibilityLabel("Line Series")
|
|
.accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)")
|
|
.foregroundStyle(airtimeChartColor)
|
|
}
|
|
}
|
|
.chartXAxis(content: {
|
|
AxisMarks(position: .top)
|
|
})
|
|
.chartXAxis(.automatic)
|
|
.chartYScale(domain: 0...100)
|
|
.chartForegroundStyleScale([
|
|
"Battery Level": .blue,
|
|
"Channel Utilization": .green,
|
|
"Airtime": .orange
|
|
])
|
|
.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(Array(deviceMetrics),id: \.self) {
|
|
Table(deviceMetrics) {
|
|
TableColumn("battery.level") { dm in
|
|
if dm.batteryLevel > 100 {
|
|
Text("Powered")
|
|
} else {
|
|
Text("\(String(dm.batteryLevel))%")
|
|
}
|
|
}
|
|
TableColumn("voltage") { dm in
|
|
Text("\(String(format: "%.2f", dm.voltage))")
|
|
}
|
|
TableColumn("channel.utilization") { dm in
|
|
Text("\(String(format: "%.2f", dm.channelUtilization))%")
|
|
}
|
|
TableColumn("airtime") { dm in
|
|
Text("\(String(format: "%.2f", dm.airUtilTx))%")
|
|
}
|
|
TableColumn("timestamp") { dm in
|
|
Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
|
}
|
|
.width(min: 180)
|
|
}
|
|
} else {
|
|
ScrollView {
|
|
let columns = [
|
|
GridItem(.flexible(minimum: 35, maximum: 55), spacing: 0.1),
|
|
GridItem(.flexible(minimum: 35, maximum: 60), spacing: 0.1),
|
|
GridItem(.flexible(minimum: 35, maximum: 70), spacing: 0.1),
|
|
GridItem(.flexible(minimum: 35, maximum: 65), spacing: 0.1),
|
|
GridItem(.flexible(minimum: 130, maximum: 200), spacing: 0.1)
|
|
]
|
|
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
|
|
GridRow {
|
|
Text("Batt")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
Text("Volt")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
Text("ChUtil")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
Text("AirTm")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
Text("timestamp")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
}
|
|
ForEach(deviceMetrics) { dm in
|
|
GridRow {
|
|
if dm.batteryLevel > 100 {
|
|
Text("PWD")
|
|
.font(.caption)
|
|
} else {
|
|
Text("\(String(dm.batteryLevel))%")
|
|
.font(.caption)
|
|
}
|
|
Text(String(dm.voltage))
|
|
.font(.caption)
|
|
Text("\(String(format: "%.2f", dm.channelUtilization))%")
|
|
.font(.caption)
|
|
Text("\(String(format: "%.2f", dm.airUtilTx))%")
|
|
.font(.caption)
|
|
Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
|
.font(.caption)
|
|
}
|
|
}
|
|
}
|
|
.padding(.leading, 15)
|
|
.padding(.trailing, 5)
|
|
}
|
|
}
|
|
HStack {
|
|
Button(role: .destructive) {
|
|
isPresentingClearLogConfirm = true
|
|
} label: {
|
|
Label("clear.log", systemImage: "trash.fill")
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.buttonBorderShape(.capsule)
|
|
.controlSize(.large)
|
|
.padding(.bottom)
|
|
.padding(.leading)
|
|
.confirmationDialog(
|
|
"are.you.sure",
|
|
isPresented: $isPresentingClearLogConfirm,
|
|
titleVisibility: .visible
|
|
) {
|
|
Button("device.metrics.delete", role: .destructive) {
|
|
if clearTelemetry(destNum: node.num, metricsType: 0, context: context) {
|
|
print("Cleared Device Metrics for \(node.num)")
|
|
} else {
|
|
print("Clear Device Metrics Log Failed")
|
|
}
|
|
}
|
|
}
|
|
|
|
Button {
|
|
exportString = telemetryToCsvFile(telemetry: deviceMetrics, metricsType: 0)
|
|
isExporting = true
|
|
} label: {
|
|
Label("save", systemImage: "square.and.arrow.down")
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.buttonBorderShape(.capsule)
|
|
.controlSize(.large)
|
|
.padding(.bottom)
|
|
.padding(.trailing)
|
|
}
|
|
} else {
|
|
if #available (iOS 17, *) {
|
|
ContentUnavailableView("No Device Metrics", systemImage: "slash.circle")
|
|
} else {
|
|
Text("No Device Metrics")
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("device.metrics.log")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.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
|
|
}
|
|
}
|
|
.fileExporter(
|
|
isPresented: $isExporting,
|
|
document: CsvDocument(emptyCsv: exportString),
|
|
contentType: .commaSeparatedText,
|
|
defaultFilename: String("\(node.user?.longName ?? "Node") \("device.metrics.log".localized)"),
|
|
onCompletion: { result in
|
|
if case .success = result {
|
|
print("Device metrics log download succeeded.")
|
|
self.isExporting = false
|
|
} else {
|
|
print("Device metrics log download failed: \(result).")
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|