2022-01-01 08:18:36 -08:00
|
|
|
/*
|
|
|
|
|
Abstract:
|
|
|
|
|
A view showing the details for a node.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
|
import MapKit
|
|
|
|
|
import CoreLocation
|
|
|
|
|
|
|
|
|
|
struct NodeDetail: View {
|
|
|
|
|
|
|
|
|
|
@Environment(\.managedObjectContext) var context
|
|
|
|
|
@EnvironmentObject var bleManager: BLEManager
|
2022-02-22 09:08:06 -10:00
|
|
|
@EnvironmentObject var userSettings: UserSettings
|
2022-05-29 22:02:25 -07:00
|
|
|
|
|
|
|
|
@State private var isPresentingShutdownConfirm: Bool = false
|
|
|
|
|
@State private var isPresentingRebootConfirm: Bool = false
|
2022-01-01 08:18:36 -08:00
|
|
|
|
|
|
|
|
var node: NodeInfoEntity
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
|
|
|
|
|
HStack {
|
|
|
|
|
|
|
|
|
|
GeometryReader { bounds in
|
|
|
|
|
|
|
|
|
|
VStack {
|
|
|
|
|
|
2022-03-19 23:45:01 -07:00
|
|
|
if node.positions?.count ?? 0 >= 1 {
|
2022-01-01 08:18:36 -08:00
|
|
|
|
|
|
|
|
let mostRecent = node.positions?.lastObject as! PositionEntity
|
|
|
|
|
|
|
|
|
|
if mostRecent.coordinate != nil {
|
|
|
|
|
|
|
|
|
|
let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!)
|
|
|
|
|
|
|
|
|
|
let regionBinding = Binding<MKCoordinateRegion>(
|
|
|
|
|
get: {
|
|
|
|
|
MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))
|
|
|
|
|
},
|
|
|
|
|
set: { _ in }
|
|
|
|
|
)
|
2022-05-27 23:56:34 -07:00
|
|
|
|
|
|
|
|
ZStack {
|
|
|
|
|
|
|
|
|
|
let annotations = node.positions?.array as! [PositionEntity]
|
|
|
|
|
|
|
|
|
|
Map(coordinateRegion: regionBinding,
|
|
|
|
|
interactionModes: [.all],
|
|
|
|
|
showsUserLocation: true,
|
|
|
|
|
userTrackingMode: .constant(.follow),
|
|
|
|
|
annotationItems: annotations
|
|
|
|
|
|
2022-01-01 08:18:36 -08:00
|
|
|
)
|
2022-05-27 23:56:34 -07:00
|
|
|
{ location in
|
|
|
|
|
|
|
|
|
|
return MapAnnotation(
|
|
|
|
|
coordinate: location.coordinate ?? CLLocationCoordinate2D(latitude: 0, longitude: 0),
|
|
|
|
|
|
|
|
|
|
content: {
|
|
|
|
|
|
2022-05-28 01:19:44 -07:00
|
|
|
NodeAnnotation(time: location.time!)
|
2022-05-27 23:56:34 -07:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
.frame(idealWidth: bounds.size.width, maxHeight: bounds.size.height / 2)
|
|
|
|
|
.ignoresSafeArea(.all, edges: [.leading, .trailing])
|
2022-01-01 08:18:36 -08:00
|
|
|
}
|
2022-05-27 23:56:34 -07:00
|
|
|
|
2022-01-01 08:18:36 -08:00
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
Image(node.user?.hwModel ?? "UNSET")
|
|
|
|
|
.resizable()
|
|
|
|
|
.aspectRatio(contentMode: .fit)
|
|
|
|
|
.frame(width: bounds.size.width, height: bounds.size.height / 2)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
Image(node.user?.hwModel ?? "UNSET")
|
|
|
|
|
.resizable()
|
|
|
|
|
.aspectRatio(contentMode: .fit)
|
|
|
|
|
.frame(width: bounds.size.width, height: bounds.size.height / 2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ScrollView {
|
2022-05-29 22:02:25 -07:00
|
|
|
|
|
|
|
|
HStack {
|
2022-05-30 21:48:46 -07:00
|
|
|
if self.bleManager.connectedPeripheral != nil && self.bleManager.connectedPeripheral.num == node.num && self.bleManager.connectedPeripheral.num == node.num {
|
2022-05-29 22:02:25 -07:00
|
|
|
|
|
|
|
|
Button(action: {
|
|
|
|
|
|
|
|
|
|
isPresentingShutdownConfirm = true
|
|
|
|
|
}) {
|
|
|
|
|
|
|
|
|
|
Image(systemName: "power")
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
.imageScale(.small)
|
|
|
|
|
.foregroundColor(Color.accentColor)
|
|
|
|
|
Text("Power Off")
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
.padding()
|
|
|
|
|
.background(Color(.systemGray6))
|
|
|
|
|
.clipShape(Capsule())
|
|
|
|
|
.confirmationDialog(
|
|
|
|
|
"Are you sure?",
|
|
|
|
|
isPresented: $isPresentingShutdownConfirm
|
|
|
|
|
) {
|
|
|
|
|
Button("Shutdown Node?", role: .destructive) {
|
|
|
|
|
let success = bleManager.sendShutdown(destNum: node.num, wantResponse: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Button(action: {
|
|
|
|
|
|
|
|
|
|
isPresentingRebootConfirm = true
|
|
|
|
|
|
|
|
|
|
}) {
|
|
|
|
|
|
|
|
|
|
Image(systemName: "arrow.triangle.2.circlepath")
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
.imageScale(.small)
|
|
|
|
|
.foregroundColor(Color.accentColor)
|
|
|
|
|
Text("Reboot")
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
.padding()
|
|
|
|
|
.background(Color(.systemGray6))
|
|
|
|
|
.clipShape(Capsule())
|
|
|
|
|
.confirmationDialog(
|
|
|
|
|
"Are you sure?",
|
|
|
|
|
isPresented: $isPresentingRebootConfirm
|
|
|
|
|
) {
|
|
|
|
|
Button("Reboot Node?", role: .destructive) {
|
|
|
|
|
let success = bleManager.sendReboot(destNum: node.num, wantResponse: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
.padding(5)
|
|
|
|
|
Divider()
|
2022-05-25 22:30:48 -07:00
|
|
|
HStack {
|
2022-01-01 08:18:36 -08:00
|
|
|
|
2022-05-25 22:30:48 -07:00
|
|
|
Image(systemName: "clock.badge.checkmark.fill")
|
|
|
|
|
.font(.title)
|
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
|
|
|
|
|
LastHeardText(lastHeard: node.lastHeard).font(.title3)
|
2022-01-01 08:18:36 -08:00
|
|
|
}
|
2022-05-25 22:30:48 -07:00
|
|
|
.padding()
|
|
|
|
|
Divider()
|
2022-01-01 08:18:36 -08:00
|
|
|
|
|
|
|
|
HStack {
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .center) {
|
|
|
|
|
Text("AKA").font(.title2).fixedSize()
|
|
|
|
|
CircleText(text: node.user?.shortName ?? "???", color: .accentColor)
|
|
|
|
|
.offset(y: 10)
|
|
|
|
|
}
|
|
|
|
|
.padding(5)
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
|
|
|
|
VStack {
|
|
|
|
|
|
2022-01-06 08:41:44 -08:00
|
|
|
if node.user != nil {
|
|
|
|
|
|
|
|
|
|
Image(node.user!.hwModel ?? "UNSET")
|
|
|
|
|
.resizable()
|
|
|
|
|
.frame(width: 50, height: 50)
|
|
|
|
|
.cornerRadius(5)
|
|
|
|
|
|
|
|
|
|
Text(String(node.user!.hwModel ?? "UNSET"))
|
|
|
|
|
.font(.callout).fixedSize()
|
|
|
|
|
}
|
2022-01-01 08:18:36 -08:00
|
|
|
}
|
|
|
|
|
.padding(5)
|
2022-04-19 22:56:59 -07:00
|
|
|
|
|
|
|
|
|
2022-01-01 08:18:36 -08:00
|
|
|
if node.snr > 0 {
|
|
|
|
|
Divider()
|
|
|
|
|
VStack(alignment: .center) {
|
|
|
|
|
|
|
|
|
|
Image(systemName: "waveform.path")
|
|
|
|
|
.font(.title)
|
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
Text("SNR").font(.title2).fixedSize()
|
|
|
|
|
Text(String(node.snr))
|
|
|
|
|
.font(.title2)
|
|
|
|
|
.foregroundColor(.gray)
|
|
|
|
|
.fixedSize()
|
|
|
|
|
}
|
|
|
|
|
.padding(5)
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-05 18:01:20 -07:00
|
|
|
if node.telemetries?.count ?? 0 >= 1 {
|
2022-01-01 08:18:36 -08:00
|
|
|
|
2022-05-05 18:01:20 -07:00
|
|
|
let mostRecent = node.telemetries?.lastObject as! TelemetryEntity
|
2022-01-01 08:18:36 -08:00
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .center) {
|
|
|
|
|
|
2022-05-05 18:01:20 -07:00
|
|
|
BatteryIcon(batteryLevel: mostRecent.batteryLevel, font: .title, color: .accentColor)
|
2022-01-01 08:18:36 -08:00
|
|
|
.padding(.bottom)
|
2022-05-27 19:18:33 -07:00
|
|
|
|
|
|
|
|
if mostRecent.batteryLevel > 0 {
|
|
|
|
|
Text(String(mostRecent.batteryLevel) + "%")
|
|
|
|
|
.font(.title3)
|
|
|
|
|
.foregroundColor(.gray)
|
|
|
|
|
.fixedSize()
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-25 22:30:48 -07:00
|
|
|
Text(String(format: "%.2f", mostRecent.voltage) + " V")
|
|
|
|
|
.font(.title3)
|
|
|
|
|
.foregroundColor(.gray)
|
|
|
|
|
.fixedSize()
|
2022-01-01 08:18:36 -08:00
|
|
|
}
|
|
|
|
|
.padding(5)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(4)
|
|
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
|
|
|
|
HStack(alignment: .center) {
|
|
|
|
|
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 {
|
|
|
|
|
HStack {
|
|
|
|
|
Image(systemName: "number")
|
|
|
|
|
.font(.title2)
|
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
Text("Node Number:").font(.title2)
|
|
|
|
|
}
|
|
|
|
|
Text(String(node.num)).font(.title3).foregroundColor(.gray)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(5)
|
|
|
|
|
Divider()
|
|
|
|
|
HStack {
|
|
|
|
|
Image(systemName: "globe")
|
|
|
|
|
.font(.headline)
|
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
Text("MAC Address: ")
|
|
|
|
|
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray)
|
|
|
|
|
}
|
|
|
|
|
.padding()
|
|
|
|
|
|
2022-03-19 23:45:01 -07:00
|
|
|
if node.positions?.count ?? 0 >= 1 {
|
2022-01-01 08:18:36 -08:00
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
|
|
|
|
HStack {
|
|
|
|
|
|
|
|
|
|
Image(systemName: "location.circle.fill")
|
|
|
|
|
.font(.title)
|
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
Text("Location History").font(.title2)
|
|
|
|
|
}
|
|
|
|
|
.padding()
|
|
|
|
|
|
|
|
|
|
Divider()
|
2022-05-27 23:56:34 -07:00
|
|
|
|
2022-01-01 08:18:36 -08:00
|
|
|
ForEach(node.positions!.array as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
|
2022-03-29 21:16:15 -07:00
|
|
|
|
2022-05-25 22:30:48 -07:00
|
|
|
if mappin.coordinate != nil {
|
|
|
|
|
|
2022-05-27 23:56:34 -07:00
|
|
|
VStack {
|
|
|
|
|
|
|
|
|
|
HStack {
|
|
|
|
|
|
|
|
|
|
Image(systemName: "mappin.and.ellipse").foregroundColor(.accentColor) // .font(.subheadline)
|
|
|
|
|
Text("Lat/Long:").font(.caption)
|
|
|
|
|
Text("\(String(mappin.latitude ?? 0)) \(String(mappin.longitude ?? 0))")
|
|
|
|
|
.foregroundColor(.gray)
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
|
|
|
|
Image(systemName: "arrow.up.arrow.down.circle")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
|
|
|
|
|
Text("Alt:")
|
|
|
|
|
.font(.caption)
|
|
|
|
|
|
|
|
|
|
Text("\(String(mappin.altitude))m")
|
|
|
|
|
.foregroundColor(.gray)
|
|
|
|
|
.font(.caption)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
HStack {
|
|
|
|
|
|
|
|
|
|
Image(systemName: "clock.badge.checkmark.fill")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
Text("Time:")
|
|
|
|
|
.font(.caption)
|
2022-05-30 21:48:46 -07:00
|
|
|
DateTimeText(dateTime: mappin.time)
|
2022-05-27 23:56:34 -07:00
|
|
|
.foregroundColor(.gray)
|
|
|
|
|
.font(.caption)
|
|
|
|
|
Divider()
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-05-25 22:30:48 -07:00
|
|
|
}
|
2022-01-01 08:18:36 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-01-06 08:41:44 -08:00
|
|
|
}
|
|
|
|
|
.edgesIgnoringSafeArea([.leading, .trailing])
|
2022-01-10 07:29:48 -08:00
|
|
|
.padding(1)
|
2022-01-01 08:18:36 -08:00
|
|
|
}
|
|
|
|
|
}
|
2022-03-19 23:45:01 -07:00
|
|
|
.navigationTitle((node.user != nil) ? String(node.user!.longName ?? "Unknown") : "Unknown")
|
2022-01-01 08:18:36 -08:00
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
|
.navigationBarItems(trailing:
|
|
|
|
|
|
|
|
|
|
ZStack {
|
|
|
|
|
|
|
|
|
|
ConnectedDevice(
|
|
|
|
|
bluetoothOn: bleManager.isSwitchedOn,
|
|
|
|
|
deviceConnected: bleManager.connectedPeripheral != nil,
|
2022-05-27 19:18:33 -07:00
|
|
|
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.lastFourCode : "????")
|
2022-01-01 08:18:36 -08:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
.onAppear(perform: {
|
|
|
|
|
|
|
|
|
|
self.bleManager.context = context
|
2022-02-22 09:08:06 -10:00
|
|
|
self.bleManager.userSettings = userSettings
|
2022-01-01 08:18:36 -08:00
|
|
|
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct NodeInfoEntityDetail_Previews: PreviewProvider {
|
|
|
|
|
|
|
|
|
|
static let bleManager = BLEManager()
|
|
|
|
|
|
|
|
|
|
static var previews: some View {
|
|
|
|
|
Group {
|
|
|
|
|
|
|
|
|
|
// NodeDetail(node: node)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|