Working 3 column nodes view

This commit is contained in:
Garth Vander Houwen 2023-09-09 09:00:34 -07:00
parent 9fccb74f43
commit d9a424388a
9 changed files with 315 additions and 58 deletions

View file

@ -16,7 +16,6 @@
DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; };
DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; };
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */; };
DD14E72E2A82A614006E39BC /* RemoteHardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD14E72D2A82A614006E39BC /* RemoteHardware.swift */; };
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */; };
DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B828CDA93900720036 /* SerialConfigEnums.swift */; };
DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; };
@ -138,6 +137,7 @@
DDDB263F2AABEE20003AFCB7 /* NodeListSplit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB263E2AABEE20003AFCB7 /* NodeListSplit.swift */; };
DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26412AABF655003AFCB7 /* NodeListItem.swift */; };
DDDB26442AAC0206003AFCB7 /* NodeDetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */; };
DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */; };
DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443529F6287000EE2349 /* MapButtons.swift */; };
DDDB443D29F6592F00EE2349 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443C29F6592F00EE2349 /* NetworkManager.swift */; };
DDDB444029F79AB000EE2349 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443F29F79AB000EE2349 /* UserDefaults.swift */; };
@ -217,7 +217,6 @@
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = "<group>"; };
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = "<group>"; };
DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV15.xcdatamodel; sourceTree = "<group>"; };
DD14E72D2A82A614006E39BC /* RemoteHardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteHardware.swift; sourceTree = "<group>"; };
DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfigEnums.swift; sourceTree = "<group>"; };
DD1925B828CDA93900720036 /* SerialConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfigEnums.swift; sourceTree = "<group>"; };
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = "<group>"; };
@ -357,6 +356,7 @@
DDDB263E2AABEE20003AFCB7 /* NodeListSplit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListSplit.swift; sourceTree = "<group>"; };
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListItem.swift; sourceTree = "<group>"; };
DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetailItem.swift; sourceTree = "<group>"; };
DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoItem.swift; sourceTree = "<group>"; };
DDDB443529F6287000EE2349 /* MapButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapButtons.swift; sourceTree = "<group>"; };
DDDB443C29F6592F00EE2349 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
DDDB443F29F79AB000EE2349 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
@ -471,7 +471,6 @@
DD47E3CD26F103C600029299 /* NodeList.swift */,
DD90860D26F69BAE00DC5189 /* NodeMap.swift */,
DD73FD1028750779000852D6 /* PositionLog.swift */,
DD14E72D2A82A614006E39BC /* RemoteHardware.swift */,
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */,
DDDB263E2AABEE20003AFCB7 /* NodeListSplit.swift */,
);
@ -809,8 +808,9 @@
DDDB26402AABEF7B003AFCB7 /* Helpers */ = {
isa = PBXGroup;
children = (
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */,
DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */,
DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */,
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -1093,6 +1093,7 @@
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */,
DDDB445429F8AD1600EE2349 /* Data.swift in Sources */,
DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */,
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */,
DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */,
DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */,
@ -1194,7 +1195,6 @@
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */,
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */,
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */,
DD14E72E2A82A614006E39BC /* RemoteHardware.swift in Sources */,
DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */,
DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */,
DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */,

View file

@ -26,6 +26,7 @@ struct LoRaSignalStrengthMeter: View {
.foregroundColor(getRssiColor(rssi: rssi))
.font(.caption2)
}
.padding(.bottom, 2)
} else {
Gauge(value: Double(signalStrength.rawValue), in: 0...3) {
} currentValueLabel: {

View file

@ -1,9 +1,7 @@
//
// NodeDetailItem.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 9/8/23.
//
/*
Abstract:
A view showing the details for a node.
*/
import SwiftUI
import WeatherKit
@ -11,20 +9,115 @@ import MapKit
import CoreLocation
struct NodeDetailItem: View {
var node: NodeInfoEntity
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.colorScheme) var colorScheme: ColorScheme
@AppStorage("meshMapType") private var meshMapType = 0
@AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
@AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false
@State private var selectedMapLayer: MapLayer = .standard
@State var waypointCoordinate: WaypointCoordinate?
@State var editingWaypoint: Int = 0
@State private var loadedWeather: Bool = false
@State private var showingDetailsPopover = false
@State private var showingForecast = false
@State private var showingShutdownConfirm: Bool = false
@State private var showingRebootConfirm: Bool = false
@State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
mapName: "offlinemap",
tileType: "png",
canReplaceMapContent: true
)
@ObservedObject var node: NodeInfoEntity
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
predicate: NSPredicate(
format: "expire == nil || expire >= %@", Date() as NSDate
), animation: .none)
private var waypoints: FetchedResults<WaypointEntity>
/// The current weather condition for the city.
@State private var condition: WeatherCondition?
@State private var temperature: Measurement<UnitTemperature>?
@State private var humidity: Int?
@State private var symbolName: String = "cloud.fill"
@State private var attributionLink: URL?
@State private var attributionLogo: URL?
var body: some View {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
NavigationStack {
GeometryReader { bounds in
VStack {
ScrollView {
NodeInfoItem(node: node)
if self.bleManager.connectedPeripheral != nil && node.metadata != nil {
HStack {
if node.metadata?.canShutdown ?? false {
Button(action: {
showingShutdownConfirm = true
}) {
Label("Power Off", systemImage: "power")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"are.you.sure",
isPresented: $showingShutdownConfirm
) {
Button("Shutdown Node?", role: .destructive) {
if !bleManager.sendShutdown(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) {
print("Shutdown Failed")
}
}
}
}
Button(action: {
showingRebootConfirm = true
}) {
Label("reboot", systemImage: "arrow.triangle.2.circlepath")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog("are.you.sure",
isPresented: $showingRebootConfirm
) {
Button("reboot.node", role: .destructive) {
if !bleManager.sendReboot(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) {
print("Reboot Failed")
}
}
}
}
.padding(5)
Divider()
}
}
}
.edgesIgnoringSafeArea([.leading, .trailing])
.sheet(item: $waypointCoordinate, content: { wpc in
WaypointFormView(coordinate: wpc)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.automatic)
})
.navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
}
.padding(.bottom, 2)
}
}
}

View file

@ -0,0 +1,174 @@
//
// NodeInfoItem.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 9/9/23.
//
import SwiftUI
import CoreLocation
import MapKit
struct NodeInfoItem: View {
var node: NodeInfoEntity
enum SelectedDetail {
case positionLog
case nodeMap
case deviceMetricsLog
case environmentMetricsLog
case detectionSensorLog
}
var body: some View {
Divider()
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
} else {
}
HStack {
VStack(alignment: .center) {
CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65)
}
if node.user != nil {
Divider()
VStack {
Image(node.user!.hwModel ?? "unset".localized)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 75, height: 75)
.cornerRadius(5)
Text(String(node.user!.hwModel ?? "unset".localized))
.font(.caption2).fixedSize()
}
}
if node.snr != 0 {
Divider()
VStack(alignment: .center) {
let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate)
LoRaSignalStrengthIndicator(signalStrength: signalStrength)
Text("Signal \(signalStrength.description)").font(.footnote)
Text("SNR \(String(format: "%.2f", node.snr))dB")
.foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate))
.font(.caption2)
Text("RSSI \(node.rssi)dB")
.foregroundColor(getRssiColor(rssi: node.rssi))
.font(.caption2)
}
}
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
if deviceMetrics?.count ?? 0 >= 1 {
Divider()
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
VStack(alignment: .center) {
BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0))
if mostRecent?.voltage ?? 0 > 0 {
Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V")
.font(.callout)
.foregroundColor(.gray)
.fixedSize()
}
}
}
}
Divider()
VStack(alignment: .center) {
VStack {
HStack {
Image(systemName: "number")
.font(.title2)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Node Number:").font(.title2)
}
Text(String(node.num)).font(.title3).foregroundColor(.gray)
}
Divider()
VStack {
HStack {
Image(systemName: "person")
.font(.title2)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("User Id:").font(.title2)
}
Text(node.user?.userId ?? "?").font(.title3).foregroundColor(.gray)
}
Divider()
}
VStack {
// List {
if node.hasPositions {
NavigationLink {
PositionLog(node: node)
.onAppear {
}
} label: {
Image(systemName: "building.columns")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Position Log")
.font(.title3)
}
.fixedSize(horizontal: false, vertical: true)
Divider()
}
if node.hasDeviceMetrics {
NavigationLink {
DeviceMetricsLog(node: node)
} label: {
Image(systemName: "flipphone")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Device Metrics Log")
.font(.title3)
}
Divider()
}
if node.hasEnvironmentMetrics {
NavigationLink {
EnvironmentMetricsLog(node: node)
} label: {
Image(systemName: "chart.xyaxis.line")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Environment Metrics Log")
.font(.title3)
}
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()
// }
// .listStyle(.plain)
}
}
}

View file

@ -6,18 +6,18 @@
//
import SwiftUI
import CoreLocation
struct NodeListItem: View {
@StateObject var node: NodeInfoEntity
public var node: NodeInfoEntity
var connected: Bool
var connectedNode: Int64
var modemPreset: Int
var body: some View {
NavigationLink(value: node) {
let connected: Bool = false //(bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num)
LazyVStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
@ -50,20 +50,20 @@ struct NodeListItem: View {
LastHeardText(lastHeard: node.lastHeard)
.font(.caption)
}
// if node.positions?.count ?? 0 > 0 && (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 != node.num) {
// HStack(alignment: .bottom) {
// let lastPostion = node.positions!.reversed()[0] as! PositionEntity
// let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
// if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
// let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
// let metersAway = nodeCoord.distance(from: myCoord)
// Image(systemName: "lines.measurement.horizontal")
// .font(.footnote)
// .symbolRenderingMode(.hierarchical)
// DistanceText(meters: metersAway).font(.caption)
// }
// }
// }
if node.positions?.count ?? 0 > 0 && connectedNode != node.num {
HStack(alignment: .bottom) {
let lastPostion = node.positions!.reversed()[0] as! PositionEntity
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
let metersAway = nodeCoord.distance(from: myCoord)
Image(systemName: "lines.measurement.horizontal")
.font(.footnote)
.symbolRenderingMode(.hierarchical)
DistanceText(meters: metersAway).font(.caption)
}
}
}
if node.channel > 0 {
HStack(alignment: .bottom) {
Image(systemName: "fibrechannel")
@ -74,11 +74,12 @@ struct NodeListItem: View {
}
}
// if !connected {
// HStack(alignment: .bottom) { let preset = ModemPresets(rawValue: Int(connectedNode?.loRaConfig?.modemPreset ?? 0))
// LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true)
// }
// }
if !connected {
HStack(alignment: .bottom) {
let preset = ModemPresets(rawValue: Int(modemPreset))
LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}

View file

@ -44,7 +44,7 @@ struct NodeDetail: View {
@State private var attributionLink: URL?
@State private var attributionLogo: URL?
var body: some View {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)

View file

@ -39,21 +39,20 @@ struct NodeListSplit: View {
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
List(nodes, id: \.self, selection: $selection) { node in
NodeListItem(node: node)
NodeListItem(node: node, connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1), modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0))
}
.searchable(text: nodesQuery, prompt: "Find a node")
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
.listStyle(.plain)
.navigationSplitViewColumnWidth(300)
.navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500)
.navigationBarItems(leading:
MeshtasticLogo()
)
} content: {
if let node = selection {
//NodeDetailItem(node: node)
NodeDetail(node: node)
.navigationSplitViewColumnWidth(300)
NodeDetailItem(node: node)
} else {
Text("select.node")
}

View file

@ -19,11 +19,8 @@ struct PositionLog: View {
@State var exportString = ""
var node: NodeInfoEntity
@State private var isPresentingClearLogConfirm = false
//@State private var sortOrder = [KeyPathComparator(\PositionEntity.latitude)]
@State private var sortOrder = [KeyPathComparator(\PositionEntity.time)]
@State var sortOrder: [KeyPathComparator<PositionEntity>] = [
.init(\.latitude, order: SortOrder.forward)
]
var body: some View {
NavigationStack {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)

View file

@ -1,8 +0,0 @@
//
// RemoteHardware.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 8/8/23.
//
import Foundation