From 3275bbf348e59a2bf1114fd8fb705f7441e6bb9c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 23 Aug 2023 07:03:05 -0500 Subject: [PATCH] Added initial detection sensor log --- Meshtastic.xcodeproj/project.pbxproj | 8 + Meshtastic/Export/WriteCsvFile.swift | 15 ++ .../Persistence/MessageEntityExtension.swift | 21 +++ Meshtastic/Persistence/QueryCoreData.swift | 20 +++ .../Views/Helpers/Node/NodeInfoView.swift | 13 ++ .../Views/Messages/ChannelMessageList.swift | 3 +- .../Views/Nodes/DetectionSensorLog.swift | 157 ++++++++++++++++++ en.lproj/Localizable.strings | 1 + unthebenternify.sh | 4 + 9 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 Meshtastic/Persistence/MessageEntityExtension.swift create mode 100644 Meshtastic/Views/Nodes/DetectionSensorLog.swift create mode 100755 unthebenternify.sh diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 307fa716..8c91ecab 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; }; + 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; }; + 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; }; C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; }; @@ -197,6 +199,8 @@ /* Begin PBXFileReference section */ 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = ""; }; + 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = ""; }; + 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = ""; }; DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = ""; }; @@ -433,6 +437,7 @@ DD90860D26F69BAE00DC5189 /* NodeMap.swift */, DD73FD1028750779000852D6 /* PositionLog.swift */, DD14E72D2A82A614006E39BC /* RemoteHardware.swift */, + 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, ); path = Nodes; sourceTree = ""; @@ -746,6 +751,7 @@ DD964FC52975DBFD007C176F /* QueryCoreData.swift */, DD3CC6C128EB9D4900FA9159 /* UpdateCoreData.swift */, DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */, + 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */, ); path = Persistence; sourceTree = ""; @@ -1009,6 +1015,7 @@ DD3501892852FC3B000FC853 /* Settings.swift in Sources */, DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */, DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */, + 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */, DD5E5203298EE33B00D21B61 /* config.pb.swift in Sources */, DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */, DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */, @@ -1039,6 +1046,7 @@ DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, + 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */, DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */, DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */, diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index db8ffdbe..c246fa40 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -53,6 +53,21 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin return csvString } +func detectionsToCsv(detections: [MessageEntity]) -> 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 Header + csvString = "Detection event, \("timestamp".localized)" + for d in detections { + csvString += "\n" + csvString += d.messagePayload ?? "Detection" + csvString += ", " + csvString += d.timestamp.formattedDate(format: dateFormatString).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/Persistence/MessageEntityExtension.swift b/Meshtastic/Persistence/MessageEntityExtension.swift new file mode 100644 index 00000000..400b2f97 --- /dev/null +++ b/Meshtastic/Persistence/MessageEntityExtension.swift @@ -0,0 +1,21 @@ +// +// MessageEntityExtension.swift +// Meshtastic +// +// Created by Ben on 8/22/23. +// + +import Foundation + +import CoreData +import CoreLocation +import MapKit +import SwiftUI + +extension MessageEntity { + + var timestamp: Date { + let time = messageTimestamp <= 0 ? receivedTimestamp : messageTimestamp + return Date(timeIntervalSince1970: TimeInterval(time)) + } +} diff --git a/Meshtastic/Persistence/QueryCoreData.swift b/Meshtastic/Persistence/QueryCoreData.swift index cf1a3190..d49c8176 100644 --- a/Meshtastic/Persistence/QueryCoreData.swift +++ b/Meshtastic/Persistence/QueryCoreData.swift @@ -60,3 +60,23 @@ public func getWaypoint(id: Int64, context: NSManagedObjectContext) -> WaypointE } return WaypointEntity(context: context) } + + +public func getDetectionSensorMessages(nodeNum: Int64?, context: NSManagedObjectContext) -> [MessageEntity] { + + let fetchDetectionMessagesPredicate: NSFetchRequest = NSFetchRequest.init(entityName: "MessageEntity") + fetchDetectionMessagesPredicate.predicate = NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue)) + + do { + let fetched = try context.fetch(fetchDetectionMessagesPredicate) as? [MessageEntity] ?? [] + if nodeNum == nil { + return fetched.reversed() + } + return fetched.filter { message in + return message.fromUser?.num == nodeNum! + }.reversed() + } + catch { + return [] + } +} diff --git a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift index 528aa263..c9570bd4 100644 --- a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift +++ b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift @@ -243,6 +243,19 @@ struct NodeInfoView: View { } Divider() } + NavigationLink { + DetectionSensorLog(node: node) + } label: { + + Image(systemName: "sensor") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Detection Sensor Log") + .font(.title3) + } + .fixedSize(horizontal: false, vertical: true) + Divider() } } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index ba8ae5af..e82b2138 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -198,8 +198,7 @@ struct ChannelMessageList: View { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) .font(.caption2).foregroundColor(.red) } else if isDetectionSensorMessage { - let timeStamp = message.messageTimestamp <= 0 ? message.receivedTimestamp : message.messageTimestamp - let messageDate = Date(timeIntervalSince1970: TimeInterval(timeStamp)) + let messageDate = message.timestamp Text(" \(messageDate.formattedDate(format: dateFormatString))").font(.caption2).foregroundColor(.gray) } } diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift new file mode 100644 index 00000000..f8dd841d --- /dev/null +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -0,0 +1,157 @@ +// +// DetectionSensorLog.swift +// Meshtastic +// +// Created by Ben on 8/22/23. +// + +import SwiftUI +import Charts + +struct DetectionSensorLog: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + @State private var isPresentingClearLogConfirm: Bool = false + @State var isExporting = false + @State var exportString = "" + + var node: NodeInfoEntity + + var body: some View { + + let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date()) + let detections = getDetectionSensorMessages(nodeNum: node.num, context: context) + let chartData = detections + .filter { $0.timestamp >= oneDayAgo! } + .sorted { $0.timestamp < $1.timestamp } + + NavigationStack { + + if chartData.count > 0 { + GroupBox(label: Label("\(detections.count) Total Detection Events", systemImage: "sensor")) { + + Chart { + ForEach(chartData, id: \.self) { point in + Plot { + BarMark( + x: .value("x", point.timestamp), + y: .value("y", 1) + ) + } + .accessibilityLabel("Bar Series") + .accessibilityValue("X: \(point.timestamp), Y: \(1)") + .interpolationMethod(.cardinal) + .foregroundStyle( + .linearGradient( + colors: [.green, .yellow, .orange, .red], + startPoint: .bottom, + endPoint: .top + ) + ) + .alignsMarkStylesWithPlotArea() + } + } + .chartXAxis(content: { + AxisMarks(position: .top) +// AxisMarks(position: .top, values: .stride(by: .hour)) { date in +// AxisValueLabel(format: .dateTime.hour()) +// } + }) + .chartXAxis(.automatic) + .chartYScale(domain: 0...20) + .chartForegroundStyleScale([ + "Detection events" : .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 UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + + // Add a table for mac and ipad + Table(detections) { + TableColumn("Detection event") { d in + Text(d.messagePayload ?? "Detected") + } + + TableColumn("timestamp") { d in + Text(d.timestamp.formattedDate(format: dateFormatString)) + } + .width(min: 180) + } + } else { + ScrollView { + let columns = [ + GridItem(), + GridItem() + ] + LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { + GridRow { + Text("Detection") + .font(.caption) + .fontWeight(.bold) + Text("timestamp") + .font(.caption) + .fontWeight(.bold) + } + ForEach(detections) { d in + GridRow { + Text(d.messagePayload ?? "Detected") + Text(d.timestamp.formattedDate(format: dateFormatString)) + .font(.caption) + } + } + } + .padding(.leading, 15) + .padding(.trailing, 5) + } + } + } + HStack { + Button { + exportString = detectionsToCsv(detections: chartData) + isExporting = true + } label: { + Label("save", systemImage: "square.and.arrow.down") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + .padding(.trailing) + } + .navigationTitle("detection.sensor.log") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") + }) + .onAppear { + self.bleManager.context = context + } + .fileExporter( + isPresented: $isExporting, + document: CsvDocument(emptyCsv: exportString), + contentType: .commaSeparatedText, + defaultFilename: String("\(node.user?.longName ?? "Node") \("detection.sensor.log".localized)"), + onCompletion: { result in + if case .success = result { + print("Detections metrics log download succeeded.") + self.isExporting = false + } else { + print("Detections log download failed: \(result).") + } + } + ) + } +} +// +//struct DetectionSensorLog_Previews: PreviewProvider { +// static var previews: some View { +// DetectionSensorLog() +// } +//} diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index b6166ab2..18db64fc 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -59,6 +59,7 @@ "delete"="Delete"; "detection.sensor"="Detection Sensor"; "detection.sensor.config"="Detection Sensor Config"; +"detection.sensor.log"="Detection Sensor Log"; "device"="Device"; "device.config"="Device Config"; "device.metrics.delete"="Delete all device metrics?"; diff --git a/unthebenternify.sh b/unthebenternify.sh new file mode 100755 index 00000000..60dbd6bb --- /dev/null +++ b/unthebenternify.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +sed -i '' -e 's/6YF6QJH524/GCH7VS5Y9R/g' ./Meshtastic.xcodeproj/project.pbxproj +sed -i '' -e 's/thebentern.Meshtastic/gvh.Meshtastic/g' ./Meshtastic.xcodeproj/project.pbxproj