diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 238ed7d6..672c6f72 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 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 */; }; + 8D3F8A3F2D44BB02009EAAA4 /* PowerMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */; }; B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; }; B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; }; @@ -290,6 +291,8 @@ 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 = ""; }; + 8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 49.xcdatamodel"; sourceTree = ""; }; + 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetrics.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; BCB613802C67290800485544 /* SendWaypointIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendWaypointIntent.swift; sourceTree = ""; }; @@ -1022,6 +1025,7 @@ DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */, DD5E523D298F5A7D00D21B61 /* Weather */, DD6F65712C6AB8EC0053C113 /* SecureInput.swift */, + 8D3F8A3E2D44BB02009EAAA4 /* PowerMetrics.swift */, ); path = Helpers; sourceTree = ""; @@ -1492,6 +1496,7 @@ DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */, DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */, D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */, + 8D3F8A3F2D44BB02009EAAA4 /* PowerMetrics.swift in Sources */, 2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */, DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */, DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */, @@ -1966,6 +1971,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */, DDA28B1B2D32C89200EF726F /* MeshtasticDataModelV 48.xcdatamodel */, DDDFE7402D0D4A070044463C /* MeshtasticDataModelV 47.xcdatamodel */, DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */, @@ -2015,7 +2021,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDA28B1B2D32C89200EF726F /* MeshtasticDataModelV 48.xcdatamodel */; + currentVersion = 8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 7d313191..b1bbb8c6 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -22,6 +22,10 @@ extension NodeInfoEntity { return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).lastObject as? TelemetryEntity } + var latestPowerMetrics: TelemetryEntity? { + return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 2")).lastObject as? TelemetryEntity + } + var hasPositions: Bool { return self.positions?.count ?? 0 > 0 } @@ -39,6 +43,11 @@ extension NodeInfoEntity { return user?.sensorMessageList.count ?? 0 > 0 } + var hasPowerMetrics: Bool { + let powerMetrics = telemetries?.filter { ($0 as AnyObject).metricsType == 2 } + return powerMetrics?.count ?? 0 > 0 + } + var hasTraceRoutes: Bool { let routes = traceRoutes?.filter { ($0 as AnyObject).response } return routes?.count ?? 0 > 0 diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d7a80c17..b792db29 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -682,7 +682,8 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from)) MeshLogger.log("📈 \(logString)") - if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) { + + if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) { /// Other unhandled telemetry packets return } @@ -736,6 +737,38 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes) telemetry.metricsType = 4 Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)") + } else if telemetryMessage.variant == Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) { + Logger.data.info("📈 [Power Metrics] Received for Node: \(packet.from.toHex(), privacy: .public)") + + if telemetryMessage.powerMetrics.hasCh1Voltage { + telemetry.powerCh1Voltage = telemetryMessage.powerMetrics.ch1Voltage + telemetry.metricsType = 2 + } + + if telemetryMessage.powerMetrics.hasCh1Current { + telemetry.powerCh1Current = telemetryMessage.powerMetrics.ch1Current + telemetry.metricsType = 2 + } + + if telemetryMessage.powerMetrics.hasCh2Voltage { + telemetry.powerCh2Voltage = telemetryMessage.powerMetrics.ch2Voltage + telemetry.metricsType = 2 + } + + if telemetryMessage.powerMetrics.hasCh1Current { + telemetry.powerCh2Current = telemetryMessage.powerMetrics.ch2Current + telemetry.metricsType = 2 + } + + if telemetryMessage.powerMetrics.hasCh3Voltage { + telemetry.powerCh3Voltage = telemetryMessage.powerMetrics.ch3Voltage + telemetry.metricsType = 2 + } + + if telemetryMessage.powerMetrics.hasCh3Current { + telemetry.powerCh3Current = telemetryMessage.powerMetrics.ch3Current + telemetry.metricsType = 2 + } } telemetry.snr = packet.rxSnr telemetry.rssi = packet.rxRssi diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index a702965e..0b4b8e13 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 48.xcdatamodel + MeshtasticDataModelV 49.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 48.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 48.xcdatamodel/contents index 709d5943..1ad6e791 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 48.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 48.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 49.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 49.xcdatamodel/contents new file mode 100644 index 00000000..5f12d9d0 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 49.xcdatamodel/contents @@ -0,0 +1,493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Views/Helpers/PowerMetrics.swift b/Meshtastic/Views/Helpers/PowerMetrics.swift new file mode 100644 index 00000000..4b4076d5 --- /dev/null +++ b/Meshtastic/Views/Helpers/PowerMetrics.swift @@ -0,0 +1,97 @@ +// +// PowerMetrics.swift +// Meshtastic +// +// Created by Matthew Davies on 1/24/25. +// + +import Foundation +import SwiftUI + +struct PowerMetrics: View { + private let gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2) + + let metric: TelemetryEntity + + var body: some View { + + LazyVGrid(columns: gridItemLayout) { + + + if(metric.powerCh1Voltage != nil) { + PowerMetricCompactWidget( + type: .voltage, + value: metric.powerCh1Voltage, + title: "Channel 1 Voltage" + ) + } + + if(metric.powerCh1Current != nil) { + PowerMetricCompactWidget( + type: .current, + value: metric.powerCh1Current, + title: "Channel 1 Current" + ) + } + + if(metric.powerCh2Voltage != nil) { + PowerMetricCompactWidget( + type: .voltage, + value: metric.powerCh2Voltage, + title: "Channel 2 Voltage" + ) + } + + if(metric.powerCh2Current != nil) { + PowerMetricCompactWidget( + type: .current, + value: metric.powerCh2Current, + title: "Channel 2 Current" + ) + } + + if(metric.powerCh3Voltage != nil) { + PowerMetricCompactWidget( + type: .voltage, + value: metric.powerCh3Voltage, + title: "Channel 3 Voltage" + ) + } + + if(metric.powerCh3Current != nil) { + PowerMetricCompactWidget( + type: .current, + value: metric.powerCh3Current, + title: "Channel 3 Current" + ) + } + } + } +} + +enum PowerMetricType: String { + case current = "current" + case voltage = "voltage" +} + +struct PowerMetricCompactWidget: View { + let type: PowerMetricType + let value: Float + let title: String + var body: some View { + VStack(alignment: .leading) { + HStack(spacing: 5.0) { + Image(systemName: type == .current ? "bolt.fill" : "powerplug.fill") + .foregroundColor(.accentColor) + .font(.callout) + Text(title) + .font(.caption) + } + Text("\(value, specifier: type == .current ? "%.1f" : "%.2f") \(type == .current ? "mA" : "V")") + .font(type == .current ? .system(size: 35) : .system(size: 30)) + } + .frame(minWidth: 100, idealWidth: 125, maxWidth: 150, minHeight: 120, idealHeight: 130, maxHeight: 140) + .padding() + .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 706b9d6e..c8dc8f3d 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -237,6 +237,15 @@ struct NodeDetail: View { } } } + if node.hasPowerMetrics && node.latestPowerMetrics != nil { + Section("Power") { + VStack { + if let metric = node.latestPowerMetrics { + PowerMetrics(metric: metric) + } + } + } + } Section("Logs") { // Metrics NavigationLink {