Merge branch 'ios16' into channel_updates

This commit is contained in:
Garth Vander Houwen 2022-09-25 15:15:25 -07:00 committed by GitHub
commit 5eccf8d543
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 583 additions and 793 deletions

View file

@ -36,7 +36,7 @@
DD4C158C2824A91E0032668E /* module_config.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C158B2824A91E0032668E /* module_config.pb.swift */; };
DD4C158E2824AA7E0032668E /* config.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C158D2824AA7E0032668E /* config.pb.swift */; };
DD4DED9027AD2975004BA27E /* cannedmessages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4DED8F27AD2975004BA27E /* cannedmessages.pb.swift */; };
DD4F23CD28779A3C001D37CB /* TelemetryLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F23CC28779A3C001D37CB /* TelemetryLog.swift */; };
DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */; };
DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; };
DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */; };
DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD539501276DAA6A00AD86B1 /* MapLocation.swift */; };
@ -44,7 +44,8 @@
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; };
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; };
DD6B85A828009258000ACD6B /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B85A728009258000ACD6B /* ShareChannels.swift */; };
DD73FD1128750779000852D6 /* LocationHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* LocationHistory.swift */; };
DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; };
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; };
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */; };
DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */; };
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FE272476C700F4AB02 /* LogDocument.swift */; };
@ -149,7 +150,7 @@
DD4C158B2824A91E0032668E /* module_config.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = module_config.pb.swift; sourceTree = "<group>"; };
DD4C158D2824AA7E0032668E /* config.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = config.pb.swift; sourceTree = "<group>"; };
DD4DED8F27AD2975004BA27E /* cannedmessages.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = cannedmessages.pb.swift; sourceTree = "<group>"; };
DD4F23CC28779A3C001D37CB /* TelemetryLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryLog.swift; sourceTree = "<group>"; };
DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = "<group>"; };
DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = "<group>"; };
DD539501276DAA6A00AD86B1 /* MapLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLocation.swift; sourceTree = "<group>"; };
DD5929A528C0F292003DB21D /* MeshtasticDataModel v 9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModel v 9.xcdatamodel"; sourceTree = "<group>"; };
@ -158,7 +159,8 @@
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = "<group>"; };
DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = "<group>"; };
DD6B85A728009258000ACD6B /* ShareChannels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareChannels.swift; sourceTree = "<group>"; };
DD73FD1028750779000852D6 /* LocationHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHistory.swift; sourceTree = "<group>"; };
DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = "<group>"; };
DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = "<group>"; };
DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLogger.swift; sourceTree = "<group>"; };
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = "<group>"; };
DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = "<group>"; };
@ -273,14 +275,23 @@
path = Custom;
sourceTree = "<group>";
};
DD2160AC28C5019400C17253 /* Messages */ = {
isa = PBXGroup;
children = (
DDA1C48628D82022009933EC /* MessageTemplate.swift */,
);
path = Messages;
sourceTree = "<group>";
};
DD47E3CA26F0E50300029299 /* Nodes */ = {
isa = PBXGroup;
children = (
DD47E3CD26F103C600029299 /* NodeList.swift */,
DD90860D26F69BAE00DC5189 /* NodeMap.swift */,
DD2E65252767A01F00E45FC5 /* NodeDetail.swift */,
DD73FD1028750779000852D6 /* LocationHistory.swift */,
DD4F23CC28779A3C001D37CB /* TelemetryLog.swift */,
DD73FD1028750779000852D6 /* PositionLog.swift */,
DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */,
DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */,
);
path = Nodes;
sourceTree = "<group>";
@ -705,14 +716,16 @@
DDAF8C6E26ED19040058C060 /* Extensions.swift in Sources */,
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */,
DDA1C48728D82022009933EC /* MessageTemplate.swift in Sources */,
DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */,
DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */,
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */,
DD4C158C2824A91E0032668E /* module_config.pb.swift in Sources */,
DDB6ABE828B141AF00384BA1 /* WiFiModes.swift in Sources */,
DD4F23CD28779A3C001D37CB /* TelemetryLog.swift in Sources */,
DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */,
DD6B85A828009258000ACD6B /* ShareChannels.swift in Sources */,
DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */,
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */,
DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */,
DDC4D568275499A500A4208E /* Persistence.swift in Sources */,
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
@ -773,7 +786,7 @@
DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */,
DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */,
DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */,
DD73FD1128750779000852D6 /* LocationHistory.swift in Sources */,
DD73FD1128750779000852D6 /* PositionLog.swift in Sources */,
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */,
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */,
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */,
@ -952,7 +965,7 @@
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -984,7 +997,7 @@
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -1,111 +1,9 @@
{
"images" : [
{
"filename" : "40-2.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "120-1.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "40-1.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58-1.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "80-1.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"filename" : "logo-3.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -29,7 +29,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
@Published var connectedPeripheral: Peripheral!
@Published var lastConnectionError: String
@Published var connectedVersion: String
@State var connectedVersion: String
@Published var isSwitchedOn: Bool = false
@Published var isScanning: Bool = false
@ -114,7 +114,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
if centralManager.isScanning {
self.centralManager.stopScan()
self.isScanning = self.centralManager.isScanning
isScanning = self.centralManager.isScanning
print("🛑 Stopped Scanning")
}
}
@ -416,6 +416,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
}
}
if (![FROMNUM_characteristic, FROMNUM_characteristic, TORADIO_characteristic].contains(nil)) {
sendWantConfig()
}
}
@ -1320,7 +1321,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
var adminPacket = AdminMessage()
adminPacket.getChannelRequest = channelIndex
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(connectedPeripheral.num)
meshPacket.from = 0 //UInt32(connectedPeripheral.num)
@ -1342,23 +1342,11 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
do {
try context!.save()
if meshLoggingEnabled { MeshLogger.log("💾 Saved a Get Channel Request Admin Message for node: \(String(connectedPeripheral.num))") }
if meshLoggingEnabled { MeshLogger.log("🛎️ Send Get Channel \(channelIndex) Request Admin Message for node: \(String(connectedPeripheral.num))") }
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
return true
} catch {
context!.rollback()
let nsError = error as NSError
print("💥 Error Inserting New Core Data MessageEntity: \(nsError)")
}
}
return false

View file

@ -1072,9 +1072,9 @@ func nodeInfoAppPacket (packet: MeshPacket, meshLogging: Bool, context: NSManage
}
func adminAppPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObjectContext) {
if let channelMessage = try? Channel(serializedData: packet.decoded.payload) {
let fetchedMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(packet.from))
@ -1140,11 +1140,10 @@ func adminAppPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedOb
let nsError = error as NSError
print("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError)")
}
if meshLogging { MeshLogger.log(" Channel Message received for Admin App \(try! channelMessage.jsonString())") }
}
}
func positionPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObjectContext) {
let fetchNodePositionRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
@ -1284,7 +1283,6 @@ func routingPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedObj
let nsError = error as NSError
print("💥 Error Saving ACK for message MessageID \(packet.id) Error: \(nsError)")
}
}
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21G83" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="21G115" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
@ -210,24 +210,4 @@
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND isEmoji == false AND admin = false"/>
</fetchedProperty>
</entity>
<elements>
<element name="BluetoothConfigEntity" positionX="54" positionY="153" width="128" height="89"/>
<element name="CannedMessageConfigEntity" positionX="45" positionY="144" width="128" height="194"/>
<element name="DeviceConfigEntity" positionX="45" positionY="144" width="128" height="89"/>
<element name="DisplayConfigEntity" positionX="54" positionY="153" width="128" height="104"/>
<element name="ExternalNotificationConfigEntity" positionX="63" positionY="162" width="128" height="134"/>
<element name="LoRaConfigEntity" positionX="45" positionY="144" width="128" height="104"/>
<element name="MessageEntity" positionX="-36" positionY="63" width="128" height="245"/>
<element name="MQTTConfigEntity" positionX="45" positionY="144" width="128" height="134"/>
<element name="MyInfoEntity" positionX="-18" positionY="81" width="128" height="209"/>
<element name="NodeInfoEntity" positionX="-63" positionY="-18" width="128" height="344"/>
<element name="PositionConfigEntity" positionX="63" positionY="162" width="128" height="149"/>
<element name="PositionEntity" positionX="-54" positionY="54" width="128" height="119"/>
<element name="RangeTestConfigEntity" positionX="72" positionY="171" width="128" height="89"/>
<element name="SerialConfigEntity" positionX="54" positionY="153" width="128" height="149"/>
<element name="TelemetryConfigEntity" positionX="72" positionY="171" width="128" height="119"/>
<element name="TelemetryEntity" positionX="160" positionY="192" width="128" height="209"/>
<element name="UserEntity" positionX="0" positionY="144" width="128" height="230"/>
<element name="NetworkConfigEntity" positionX="45" positionY="144" width="128" height="119"/>
</elements>
</model>

View file

@ -31,7 +31,7 @@ struct MeshtasticAppleApp: App {
saveQR = true
}
print("User wants to open URL: \(channelUrl?.relativeString)")
print("User wants to open URL: \(String(describing: channelUrl?.relativeString))")
}
.sheet(isPresented: $saveQR) {

View file

@ -0,0 +1,38 @@
//
// MessageTemplate.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 9/18/22.
//
import SwiftUI
struct MessageTemplate: View {
var user: UserEntity
var message: MessageEntity
var messageReply: MessageEntity?
var body: some View {
// Display the message being replied to and the arrow
if message.replyID > 0 {
HStack {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.blue)
.padding(.trailing)
}
}
// Message
}
}

View file

@ -359,6 +359,7 @@ struct UserMessageList: View {
.listRowSeparator(.hidden)
}
}
.scrollDismissesKeyboard(.immediately)
.onAppear(perform: {
self.bleManager.context = context

View file

@ -0,0 +1,153 @@
//
// 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 var isExporting = false
@State var exportString = ""
var node: NodeInfoEntity
struct MountPrice: Identifiable {
var id = UUID()
var mount: String
var value: Double
}
var body: some View {
NavigationStack {
let oneDayAgo = Calendar.current.date(byAdding: .day, value: -3, to: Date())
let data = node.telemetries!.filtered(using: NSPredicate(format: "metricsType == 0 && time !=nil && time >= %@", oneDayAgo! as CVarArg))
if data.count > 0 {
GroupBox(label: Label("Battery Level Trend", systemImage: "battery.100")) {
Chart(data.array as! [TelemetryEntity], id: \.self) {
LineMark(
x: .value("Hour", $0.time!.formattedDate(format: "ha")),
y: .value("Value", $0.batteryLevel)
)
PointMark(
x: .value("Hour", $0.time!.formattedDate(format: "ha")),
y: .value("Value", $0.batteryLevel)
)
}
.frame(height: 150)
}
}
ScrollView {
Grid(alignment: .topLeading, horizontalSpacing: 2) {
GridRow {
Text("Batt")
.font(.callout)
.fontWeight(.bold)
Text("Voltage")
.font(.callout)
.fontWeight(.bold)
Text("ChUtil")
.font(.callout)
.fontWeight(.bold)
Text("AirTm")
.font(.callout)
.fontWeight(.bold)
Text("Timestamp")
.font(.callout)
.fontWeight(.bold)
}
Divider()
ForEach(node.telemetries!.reversed() as! [TelemetryEntity], id: \.self) { (dm: TelemetryEntity) in
if dm.metricsType == 0 {
GridRow {
if dm.batteryLevel == 0 {
Text("USB")
.font(.callout)
} else {
Text("\(String(dm.batteryLevel))%")
.font(.callout)
}
Text(String(dm.voltage))
.font(.callout)
Text("\(String(format: "%.2f", dm.channelUtilization))%")
.font(.callout)
Text("\(String(format: "%.2f", dm.airUtilTx))%")
.font(.callout)
Text(dm.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
.font(.callout)
}
}
}
}
.padding(.leading, 15)
.padding(.trailing, 5)
}
}
Button {
exportString = TelemetryToCsvFile(telemetry: node.telemetries!.array as! [TelemetryEntity], metricsType: 0)
isExporting = true
} label: {
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.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 {
self.bleManager.context = context
}
.fileExporter(
isPresented: $isExporting,
document: CsvDocument(emptyCsv: exportString),
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user!.longName ?? "Node") Device Telemetry Log"),
onCompletion: { result in
if case .success = result {
print("Device Telemetry log download succeeded.")
self.isExporting = false
} else {
print("Device Telemetry log download failed: \(result).")
}
}
)
}
}

View file

@ -0,0 +1,130 @@
//
// DeviceMetricsLog.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 7/7/22.
//
import SwiftUI
struct EnvironmentMetricsLog: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State var isExporting = false
@State var exportString = ""
var node: NodeInfoEntity
var body: some View {
NavigationStack {
ScrollView {
Grid(alignment: .topLeading, horizontalSpacing: 2) {
GridRow {
Text("Temp")
.font(.caption)
.fontWeight(.bold)
Text("Hum")
.font(.caption)
.fontWeight(.bold)
Text("Bar")
.font(.caption)
.fontWeight(.bold)
Text("Gas")
.font(.caption)
.fontWeight(.bold)
Text("Gas")
.font(.caption)
.fontWeight(.bold)
Text("DC")
.font(.caption)
.fontWeight(.bold)
Text("Volt")
.font(.caption)
.fontWeight(.bold)
Text("Timestamp")
.font(.caption)
.fontWeight(.bold)
}
Divider()
ForEach(node.telemetries!.reversed() as! [TelemetryEntity], id: \.self) { (em: TelemetryEntity) in
if em.metricsType == 1 {
let tempReadingType = (!(node.telemetryConfig?.environmentDisplayFahrenheit ?? false)) ? "°C" : "°F"
GridRow {
Text("\(String(format: "%.2f", em.temperature))\(tempReadingType)")
.font(.caption)
Text("\(String(format: "%.2f", em.relativeHumidity))")
.font(.caption)
Text("\(String(format: "%.2f", em.barometricPressure))")
.font(.caption)
Text("\(String(format: "%.2f", em.gasResistance))")
.font(.caption)
Text("\(String(format: "%.2f", em.current))")
.font(.caption)
Text("\(String(format: "%.2f", em.voltage))")
.font(.caption)
Text(em.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
.font(.caption)
}
}
}
}
.padding(.leading, 15)
.padding(.trailing, 5)
}
}
Button {
exportString = TelemetryToCsvFile(telemetry: node.telemetries!.array as! [TelemetryEntity], metricsType: 1)
isExporting = true
} label: {
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.navigationTitle("Environment Metrics 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") Environment Metrics Log"),
onCompletion: { result in
if case .success = result {
print("Environment metrics log download succeeded.")
self.isExporting = false
} else {
print("Environment metrics log download failed: \(result).")
}
}
)
}
}

View file

@ -1,144 +0,0 @@
//
// LocationHistory.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 7/5/22.
//
import SwiftUI
struct LocationHistory: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State var isExporting = false
@State var exportString = ""
var node: NodeInfoEntity
var body: some View {
VStack {
List {
ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
VStack {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
HStack {
Image(systemName: "mappin.and.ellipse")
.foregroundColor(.accentColor)
.font(.callout)
Text("Lat/Long:").font(.callout)
Text("\(String(mappin.latitude ?? 0)) \(String(mappin.longitude ?? 0))")
.foregroundColor(.gray)
.font(.callout)
Image(systemName: "arrow.up.arrow.down.circle")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Alt:")
.font(.callout)
Text("\(String(mappin.altitude))m")
.foregroundColor(.gray)
.font(.callout)
Image(systemName: "clock.badge.checkmark.fill")
.font(.subheadline)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Time:")
.font(.callout)
DateTimeText(dateTime: mappin.time)
.foregroundColor(.gray)
.font(.callout)
}
} else {
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)
}
HStack {
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)
Image(systemName: "clock.badge.checkmark.fill")
.font(.subheadline)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
DateTimeText(dateTime: mappin.time)
.foregroundColor(.gray)
.font(.caption)
}
}
}
}
}
Button {
exportString = PositionToCsvFile(positions: node.positions!.array as! [PositionEntity])
isExporting = true
} label: {
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
}
.fileExporter(
isPresented: $isExporting,
document: CsvDocument(emptyCsv: exportString),
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") Position Log"),
onCompletion: { result in
if case .success = result {
print("Position log download succeeded.")
self.isExporting = false
} else {
print("Position log download failed: \(result).")
}
}
)
.navigationTitle("Location History \(node.positions?.count ?? 0) Points")
.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
}
}
}

View file

@ -12,10 +12,12 @@ struct NodeDetail: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State private var showingDetailsPopover = false
@State var initialLoad: Bool = true
@State var satsInView = 0
@State private var isPresentingShutdownConfirm: Bool = false
@State private var isPresentingRebootConfirm: Bool = false
@State private var showingShutdownConfirm: Bool = false
@State private var showingRebootConfirm: Bool = false
var node: NodeInfoEntity
@ -23,7 +25,7 @@ struct NodeDetail: View {
let hwModelString = node.user?.hwModel ?? "UNSET"
ZStack {
NavigationStack {
GeometryReader { bounds in
@ -63,36 +65,30 @@ struct NodeDetail: View {
}
)
}
.ignoresSafeArea(.all, edges: [.leading, .trailing])
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.6)
.ignoresSafeArea(.all, edges: [.leading, .trailing])
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.70)
}
}
Text("Sats: \(mostRecent.satsInView)").offset( y:-40)
} else {
HStack {
Image(node.user?.hwModel ?? "UNSET")
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(10)
.frame(width: bounds.size.width, height: bounds.size.height / 2.3)
.padding([.top], 40)
}
.offset( y:-40)
.padding([.top], 40)
}
ScrollView {
if self.bleManager.connectedPeripheral != nil && self.bleManager.connectedPeripheral.num == node.num && self.bleManager.connectedPeripheral.num == node.num {
Divider()
HStack {
if hwModelString == "TBEAM" || hwModelString == "TECHO" || hwModelString.contains("4631") {
Button(action: {
isPresentingShutdownConfirm = true
showingShutdownConfirm = true
}) {
Label("Power Off", systemImage: "power")
@ -103,7 +99,7 @@ struct NodeDetail: View {
.padding()
.confirmationDialog(
"Are you sure?",
isPresented: $isPresentingShutdownConfirm
isPresented: $showingShutdownConfirm
) {
Button("Shutdown Node?", role: .destructive) {
@ -117,7 +113,7 @@ struct NodeDetail: View {
Button(action: {
isPresentingRebootConfirm = true
showingRebootConfirm = true
}) {
@ -130,7 +126,7 @@ struct NodeDetail: View {
.confirmationDialog(
"Are you sure?",
isPresented: $isPresentingRebootConfirm
isPresented: $showingRebootConfirm
) {
Button("Reboot Node?", role: .destructive) {
@ -145,13 +141,10 @@ struct NodeDetail: View {
.padding(5)
}
Divider()
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
// Add a divider if there is no map
if (node.positions?.count ?? 0) == 0 {
Divider()
}
HStack {
@ -159,7 +152,7 @@ struct NodeDetail: View {
Text("AKA").font(.largeTitle)
.foregroundColor(.gray).fixedSize()
.offset(y:20)
.offset(y:5)
CircleText(text: node.user?.shortName ?? "???", color: .accentColor, circleSize: 75, fontSize: 26)
}
.padding()
@ -172,8 +165,8 @@ struct NodeDetail: View {
Image(hwModelString)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 90, height: 90)
.aspectRatio(contentMode: .fill)
.frame(width: 200, height: 200)
.cornerRadius(5)
Text(String(hwModelString))
@ -229,10 +222,15 @@ struct NodeDetail: View {
}
.padding()
}
Divider()
}
.padding()
.onLongPressGesture(minimumDuration: 2) {
print("Long pressed!")
}
Divider()
HStack(alignment: .center) {
VStack {
@ -364,8 +362,6 @@ struct NodeDetail: View {
}
.padding(4)
Divider()
HStack(alignment: .center) {
VStack {
HStack {
@ -399,70 +395,84 @@ struct NodeDetail: View {
Text("MAC Address: ")
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray)
}
.padding([.bottom], 0)
}
List {
VStack {
if (node.positions?.count ?? 0) > 0 {
NavigationLink {
LocationHistory(node: node)
PositionLog(node: node)
} label: {
Image(systemName: "building.columns")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Position History \(node.positions?.count ?? 0) Points")
Text("Position Log (\(node.positions?.count ?? 0) Points)")
.font(.title3)
}
.fixedSize(horizontal: false, vertical: true)
Divider()
}
if (node.telemetries?.count ?? 0) > 0 {
NavigationLink {
TelemetryLog(node: node)
DeviceMetricsLog(node: node)
} label: {
Image(systemName: "flipphone")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Device Metrics Log")
.font(.title3)
}
Divider()
NavigationLink {
EnvironmentMetricsLog(node: node)
} label: {
Image(systemName: "chart.xyaxis.line")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Telemetry Log \(node.telemetries?.count ?? 0) Readings")
Text("Environment Metrics Log")
.font(.title3)
}
.fixedSize(horizontal: false, vertical: true)
Divider()
}
}
.listStyle(GroupedListStyle())
.frame(minHeight: 170)
.padding(0)
}
.offset( y: (node.myInfo?.hasGps ?? false ? 0 : -40))
.offset( y:-40)
.padding(.bottom, -40)
}
.edgesIgnoringSafeArea([.leading, .trailing])
.navigationTitle((node.user != nil) ? String(node.user!.longName ?? "Unknown") : "Unknown")
.navigationBarTitleDisplayMode(.inline)
.padding(.bottom, 10)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
}
)
.onAppear {
if self.initialLoad{
self.bleManager.context = context
self.initialLoad = false
}
}
}
}
.navigationTitle((node.user != nil) ? String(node.user!.longName ?? "Unknown") : "Unknown")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
}
)
.onAppear {
if self.initialLoad{
self.bleManager.context = context
self.initialLoad = false
}
}
}
}

View file

@ -20,35 +20,35 @@ struct NodeList: View {
@State var initialLoad: Bool = true
@FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "user.shortName", ascending: true)],
sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)],
animation: .default)
private var nodes: FetchedResults<NodeInfoEntity>
@State private var selection: String? = ""
@State private var selection: NodeInfoEntity? = nil // Nothing selected by default.
var body: some View {
NavigationView {
List {
NavigationSplitView {
List (nodes, id: \.self, selection: $selection) { node in
if nodes.count == 0 {
Text("Scan for Radios").font(.largeTitle)
Text("No Meshtastic Nodes Found").font(.title2)
Text("Go to the bluetooth section in the bottom right menu and click the Start Scanning button to scan for nearby radios and find your Meshtastic device. Make sure your device is powered on and near your iPhone, iPad or Mac.")
.font(.body)
Text("Once the device shows under Available Devices touch the device you want to connect to and it will pull node information over BLE and populate the node list and mesh map in the Meshtastic app.")
Text("Views with bluetooth functionality will show an indicator in the upper right hand corner show if bluetooth is on, and if a device is connected.")
Text("Scan for Radios").font(.largeTitle)
Text("No Meshtastic Nodes Found").font(.title2)
Text("Go to the bluetooth section in the bottom right menu and click the Start Scanning button to scan for nearby radios and find your Meshtastic device. Make sure your device is powered on and near your iPhone, iPad or Mac.")
.font(.body)
Text("Once the device shows under Available Devices touch the device you want to connect to and it will pull node information over BLE and populate the node list and mesh map in the Meshtastic app.")
Text("Views with bluetooth functionality will show an indicator in the upper right hand corner showing if bluetooth is on, and if a device is connected.")
.listRowSeparator(.visible)
} else {
ForEach( nodes ) { node in
} else {
//ForEach( nodes ) { node in
let index = nodes.firstIndex(where: { $0.id == node.id })
// let index = nodes.firstIndex(where: { $0.id == node.id })
NavigationLink(destination: NodeDetail(node: node), tag: String(index!), selection: $selection) {
NavigationLink(value: node) {
let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == node.num)
@ -123,11 +123,10 @@ struct NodeList: View {
}
.padding([.leading, .top, .bottom])
}
.isDetailLink(false)
}
}
}
.navigationTitle("All Nodes")
//}
}
}
.navigationTitle("All Nodes")
.onAppear {
if initialLoad {
@ -137,8 +136,16 @@ struct NodeList: View {
self.initialLoad = false
}
}
}
.ignoresSafeArea(.all, edges: [.leading, .trailing])
.navigationViewStyle(DoubleColumnNavigationViewStyle())
} detail: {
if let node = selection {
NodeDetail(node:node)
} else {
Text("Select a node")
}
}
}
}

View file

@ -0,0 +1,107 @@
//
// LocationHistory.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 7/5/22.
//
import SwiftUI
struct PositionLog: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State var isExporting = false
@State var exportString = ""
var node: NodeInfoEntity
var body: some View {
NavigationStack {
ScrollView {
Grid(alignment: .topLeading, horizontalSpacing: 2) {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
//Add a table for mac and ipad
}
GridRow {
Text("Latitude")
.font(.caption)
.fontWeight(.bold)
Text("Longitude")
.font(.caption)
.fontWeight(.bold)
Text("Alt.")
.font(.caption)
.fontWeight(.bold)
Text("Timestamp")
.font(.caption)
.fontWeight(.bold)
}
Divider()
ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
GridRow {
Text(String(mappin.latitude ?? 0))
.font(.caption)
Text(String(mappin.longitude ?? 0))
.font(.caption)
Text(String(mappin.altitude))
.font(.caption)
Text(mappin.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
.font(.caption)
}
}
}
.padding(.leading, 15)
.padding(.trailing, 5)
}
Button {
exportString = PositionToCsvFile(positions: node.positions!.array as! [PositionEntity])
isExporting = true
} label: {
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
}
.fileExporter(
isPresented: $isExporting,
document: CsvDocument(emptyCsv: exportString),
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") Position Log"),
onCompletion: { result in
if case .success = result {
print("Position log download succeeded.")
self.isExporting = false
} else {
print("Position log download failed: \(result).")
}
}
)
.navigationTitle("Position Log \(node.positions?.count ?? 0) Points")
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
.onAppear {
self.bleManager.context = context
}
}
}

View file

@ -1,396 +0,0 @@
//
// TelemetryLog.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 7/7/22.
//
import SwiftUI
struct TelemetryLog: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State var isExporting = false
@State var exportString = ""
var node: NodeInfoEntity
var body: some View {
List {
ForEach(node.telemetries!.reversed() as! [TelemetryEntity], id: \.self) { (tel: TelemetryEntity) in
VStack (alignment: .leading) {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
if tel.metricsType == 0 {
// Device Metrics
HStack {
Text("Device Metrics")
.font(.title)
BatteryIcon(batteryLevel: tel.batteryLevel, font: .callout, color: .accentColor)
if tel.batteryLevel == 0 {
Text("Plugged In")
.font(.callout)
.foregroundColor(.gray)
} else {
Text("Battery Level: \(String(tel.batteryLevel))%")
.font(.callout)
.foregroundColor(.gray)
}
if tel.batteryLevel > 0 {
Image(systemName: "bolt")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Voltage: \(String(tel.voltage))")
.foregroundColor(.gray)
.font(.callout)
}
Text("Channel Utilization: \(String(format: "%.2f", tel.channelUtilization))%")
.foregroundColor(.gray)
.font(.callout)
Text("Airtime Utilization: \(String(format: "%.2f", tel.airUtilTx))%")
.foregroundColor(.gray)
.font(.callout)
Image(systemName: "clock.badge.checkmark.fill")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Time:")
.foregroundColor(.gray)
.font(.callout)
DateTimeText(dateTime: tel.time)
.foregroundColor(.gray)
.font(.callout)
}
} else if tel.metricsType == 1 {
// Environment Metrics
HStack {
Text("Environment Metrics")
.font(.title)
let tempReadingType = (!(node.telemetryConfig?.environmentDisplayFahrenheit ?? false)) ? "°C" : "°F"
if tel.temperature > 0 {
Image(systemName: "thermometer")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Temperature: \(String(format: "%.2f", tel.temperature))\(tempReadingType)")
.foregroundColor(.gray)
.font(.callout)
}
if tel.relativeHumidity > 0 {
Image(systemName: "humidity")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Relative Humidity: \(String(format: "%.2f", tel.relativeHumidity))")
.foregroundColor(.gray)
.font(.callout)
}
if tel.barometricPressure > 0 {
Image(systemName: "barometer")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Barometric Pressure: \(String(format: "%.2f", tel.barometricPressure))")
.foregroundColor(.gray)
.font(.callout)
}
if tel.gasResistance > 0 {
Image(systemName: "aqi.medium")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Gas Resistance: \(String(format: "%.2f", tel.gasResistance))")
.foregroundColor(.gray)
.font(.callout)
}
if tel.current > 0 {
Image(systemName: "directcurrent")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Current: \(String(format: "%.2f", tel.current))")
.foregroundColor(.gray)
.font(.callout)
Image(systemName: "bolt")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Voltage: \(String(format: "%.2f", tel.voltage))")
.foregroundColor(.gray)
.font(.callout)
}
Image(systemName: "clock.badge.checkmark.fill")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Time:")
.foregroundColor(.gray)
.font(.callout)
DateTimeText(dateTime: tel.time)
.foregroundColor(.gray)
.font(.callout)
}
}
} else {
if tel.metricsType == 0 {
// Device Metrics iPhone Template
VStack {
HStack {
Spacer()
Text("Device Metrics")
.font(.title3)
Spacer()
}
HStack {
BatteryIcon(batteryLevel: tel.batteryLevel, font: .callout, color: .accentColor)
if tel.batteryLevel == 0 {
Text("Plugged In")
.font(.callout)
.foregroundColor(.gray)
} else {
Text("Battery Level: \(String(tel.batteryLevel))%")
.font(.callout)
.foregroundColor(.gray)
}
}
HStack {
if tel.batteryLevel > 0 {
Image(systemName: "bolt")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Voltage: \(String(tel.voltage))")
.foregroundColor(.gray)
.font(.callout)
}
}
Text("Channel Utilization: \(String(format: "%.2f", tel.channelUtilization))%")
.foregroundColor(.gray)
.font(.callout)
Text("Airtime Utilization: \(String(format: "%.2f", tel.airUtilTx))%")
.foregroundColor(.gray)
.font(.callout)
HStack {
Image(systemName: "clock.badge.checkmark.fill")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Time:")
.foregroundColor(.gray)
.font(.callout)
DateTimeText(dateTime: tel.time)
.foregroundColor(.gray)
.font(.callout)
}
}
} else if tel.metricsType == 1 {
// Environment Metrics
let tempReadingType = (!(node.telemetryConfig?.environmentDisplayFahrenheit ?? true)) ? "°C" : "°F"
// Environment Metrics iPhone Template
VStack {
HStack {
Spacer()
Text("Environment Metrics")
.font(.title3)
Spacer()
}
HStack {
if tel.temperature > 0 {
Image(systemName: "thermometer")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Temperature: \(String(format: "%.2f", tel.temperature))\(tempReadingType)")
.foregroundColor(.gray)
.font(.callout)
}
}
HStack {
if tel.relativeHumidity > 0 {
Image(systemName: "humidity")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Relative Humidity: \(String(format: "%.2f", tel.relativeHumidity))")
.foregroundColor(.gray)
.font(.callout)
}
}
if tel.current > 0 {
HStack {
Image(systemName: "directcurrent")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Current: \(String(format: "%.2f", tel.current))")
.foregroundColor(.gray)
.font(.callout)
}
HStack {
Image(systemName: "bolt")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Voltage: \(String(format: "%.2f", tel.voltage))")
.foregroundColor(.gray)
.font(.callout)
}
}
if tel.barometricPressure > 0 {
HStack {
Image(systemName: "barometer")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Barometric Pressure: \(String(format: "%.2f", tel.barometricPressure))")
.foregroundColor(.gray)
.font(.callout)
}
}
if tel.gasResistance > 0 {
HStack {
Image(systemName: "aqi.medium")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Gas Resistance: \(String(format: "%.2f", tel.gasResistance))")
.foregroundColor(.gray)
.font(.callout)
}
}
HStack {
Image(systemName: "clock.badge.checkmark.fill")
.font(.callout)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("Time:")
.foregroundColor(.gray)
.font(.callout)
DateTimeText(dateTime: tel.time)
.foregroundColor(.gray)
.font(.callout)
}
}
}
}
}
}
}
Button {
exportString = TelemetryToCsvFile(telemetry: node.telemetries!.array as! [TelemetryEntity], metricsType: 0)
isExporting = true
} label: {
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.navigationTitle("Telemetry Log \(node.telemetries?.count ?? 0) Readings")
.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") Telemetry Log"),
onCompletion: { result in
if case .success = result {
print("Telemetry log download succeeded.")
self.isExporting = false
} else {
print("Telemetry log download failed: \(result).")
}
}
)
}
}

View file

@ -90,6 +90,8 @@ struct LoRaConfig: View {
lc.hopLimit = UInt32(hopLimit)
lc.region = RegionCodes(rawValue: region)!.protoEnumValue()
lc.modemPreset = ModemPresets(rawValue: modemPreset)!.protoEnumValue()
lc.usePreset = true
lc.txEnabled = true
let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: node!.user!, toUser: node!.user!)

View file

@ -205,6 +205,7 @@ struct CannedMessagesConfig: View {
}
.disabled(configPreset > 0)
}
.scrollDismissesKeyboard(.immediately)
.disabled(bleManager.connectedPeripheral == nil)
Button {

View file

@ -80,8 +80,9 @@ struct MQTTConfig: View {
hasChanges = true
})
.foregroundColor(.gray)
.keyboardType(.default)
}
.keyboardType(.default)
.autocorrectionDisabled()
HStack {
Label("Username", systemImage: "person.text.rectangle")
@ -109,6 +110,7 @@ struct MQTTConfig: View {
.foregroundColor(.gray)
}
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
HStack {
@ -137,8 +139,10 @@ struct MQTTConfig: View {
.foregroundColor(.gray)
}
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
}
}
.scrollDismissesKeyboard(.interactively)
.disabled(!(node != nil && node!.myInfo?.hasWifi ?? false))
Button {

View file

@ -112,7 +112,7 @@ struct TelemetryConfig: View {
Section(header: Text("Sensor Options")) {
Text("I2C Connected sensors will be detected automatically. Supported sensors are BMP280, BME280, BME680, MCP9808, INA219 and INA260.")
Text("Supported I2C Connected sensors will be detected automatically, sensors are BMP280, BME280, BME680, MCP9808, INA219 and INA260.")
.font(.caption)
Toggle(isOn: $environmentMeasurementEnabled) {

View file

@ -103,6 +103,7 @@ struct NetworkConfig: View {
.keyboardType(.default)
}
}
.scrollDismissesKeyboard(.interactively)
.disabled(!(node != nil && node!.myInfo?.hasWifi ?? false))
Button {

View file

@ -38,11 +38,11 @@ struct Settings: View {
Section("Radio Configuration") {
NavigationLink {
ShareChannel(node: nodes.first(where: { $0.num == connectedNodeNum }))
ShareChannels(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
Image(systemName: "qrcode")
.symbolRenderingMode(.hierarchical)
Text("Share Channel QR Code")
Text("Share Channels QR Code")
}
.disabled(bleManager.connectedPeripheral == nil)

View file

@ -31,7 +31,7 @@ struct QrCodeImage {
}
}
struct ShareChannel: View {
struct ShareChannels: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ -89,7 +89,6 @@ struct ShareChannel: View {
Text("Channel: \(channel.index) Name: \(channel.name ?? "")")
}
}
}
}
.frame(width: bounds.size.width, height: bounds.size.height)
}