Merge pull request #167 from meshtastic/ios16

Promote iOS 16 version to main
This commit is contained in:
Garth Vander Houwen 2022-10-02 10:41:48 -07:00 committed by GitHub
commit a6427bc9d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 1056 additions and 1019 deletions

View file

@ -27,6 +27,8 @@
DD35018B2852FC79000FC853 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD35018A2852FC79000FC853 /* UserSettings.swift */; };
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; };
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */; };
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */; };
DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BF28E7A60700FA9159 /* MessagingEnums.swift */; };
DD4033C228B286B70096A444 /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4033C128B286B70096A444 /* Onboarding.swift */; };
DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41582528582E9B009B0E59 /* DeviceConfig.swift */; };
DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD415827285859C4009B0E59 /* TelemetryConfig.swift */; };
@ -38,14 +40,15 @@
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 */; };
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; };
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; };
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.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 */; };
@ -58,7 +61,6 @@
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8EBF42285058FA00426DCA /* DisplayConfig.swift */; };
DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8ED9C42898D51F00B3B0AB /* NetworkConfig.swift */; };
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */; };
DD90860C26F684AF00DC5189 /* BatteryIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860B26F684AF00DC5189 /* BatteryIcon.swift */; };
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860D26F69BAE00DC5189 /* NodeMap.swift */; };
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; };
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */; };
@ -136,6 +138,8 @@
DD35018A2852FC79000FC853 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = "<group>"; };
DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareChannels.swift; sourceTree = "<group>"; };
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModel.xcdatamodel; sourceTree = "<group>"; };
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryGauge.swift; sourceTree = "<group>"; };
DD3CC6BF28E7A60700FA9159 /* MessagingEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagingEnums.swift; sourceTree = "<group>"; };
DD4033C128B286B70096A444 /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; };
DD41582528582E9B009B0E59 /* DeviceConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceConfig.swift; sourceTree = "<group>"; };
DD415827285859C4009B0E59 /* TelemetryConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryConfig.swift; sourceTree = "<group>"; };
@ -147,13 +151,14 @@
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>"; };
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
@ -167,7 +172,6 @@
DD8ED9C42898D51F00B3B0AB /* NetworkConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConfig.swift; sourceTree = "<group>"; };
DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingError.swift; sourceTree = "<group>"; };
DD90860A26F645B700DC5189 /* Meshtastic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Meshtastic.entitlements; sourceTree = "<group>"; };
DD90860B26F684AF00DC5189 /* BatteryIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryIcon.swift; sourceTree = "<group>"; };
DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = "<group>"; };
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = "<group>"; };
DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = "<group>"; };
@ -266,8 +270,9 @@
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>";
@ -287,11 +292,11 @@
DD3501882852FC3B000FC853 /* Settings.swift */,
DD4A911D2708C65400501B7E /* AppSettings.swift */,
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */,
DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */,
DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */,
DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */,
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */,
DD61937A2863876A00E59241 /* Config */,
DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */,
);
path = Settings;
sourceTree = "<group>";
@ -346,6 +351,7 @@
DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */,
DD1925B828CDA93900720036 /* SerialConfigEnums.swift */,
DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */,
DD3CC6BF28E7A60700FA9159 /* MessagingEnums.swift */,
);
path = Enums;
sourceTree = "<group>";
@ -494,12 +500,12 @@
children = (
DD47E3D526F17ED900029299 /* CircleText.swift */,
DD47E3D826F3093800029299 /* MessageBubble.swift */,
DD90860B26F684AF00DC5189 /* BatteryIcon.swift */,
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */,
DDC3B273283F411B00AC321C /* LastHeardText.swift */,
DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */,
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */,
DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */,
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -697,21 +703,22 @@
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 */,
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 */,
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,
DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */,
DD90860C26F684AF00DC5189 /* BatteryIcon.swift in Sources */,
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
DD35018B2852FC79000FC853 /* UserSettings.swift in Sources */,
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
DDAF8C6226ED0A230058C060 /* mqtt.pb.swift in Sources */,
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */,
DDAF8C5D26ED09490058C060 /* portnums.pb.swift in Sources */,
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */,
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */,
DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */,
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */,
@ -759,7 +766,8 @@
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 */,
DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */,
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */,
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */,
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
@ -939,7 +947,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",
@ -971,7 +979,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

@ -0,0 +1,11 @@
//
// MessagingEnums.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 9/30/22.
//
enum BubblePosition {
case left
case right
}

View file

@ -34,14 +34,14 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
}
}
} else {
} else if metricsType == 1 {
// Create Environment Telemetry Header
csvString = "Temperature, Relative Humidity, Barometric Pressure, Gas Resistance, Voltage, Current"
for dm in telemetry{
if dm.metricsType == 0 {
if dm.metricsType == 1 {
csvString += "\n"
csvString += String("\(dm.temperature)°")
@ -69,17 +69,26 @@ func PositionToCsvFile(positions: [PositionEntity]) -> String {
var csvString: String = ""
// Create Position Header
csvString = "Latitude, Longitude, Altitude, Timestamp"
csvString = "SeqNo, Latitude, Longitude, Alt, Sats, Speed, Heading, Timestamp"
for pos in positions {
csvString += "\n"
csvString += String(pos.seqNo)
csvString += ", "
csvString += String(pos.latitude ?? 0)
csvString += ", "
csvString += String(pos.longitude ?? 0)
csvString += ", "
csvString += String(pos.altitude)
csvString += ", "
csvString += String(pos.satsInView)
csvString += ", "
csvString += String(pos.speed)
csvString += ", "
csvString += String(pos.heading)
csvString += ", "
csvString += pos.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? "Unknown Age"
}

View file

@ -252,6 +252,15 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
self.isConnected = true
if userSettings?.preferredPeripheralId.count ?? 0 < 1 {
self.userSettings?.preferredPeripheralId = peripheral.identifier.uuidString
self.preferredPeripheral = true
} else if userSettings!.preferredPeripheralId == peripheral.identifier.uuidString {
self.preferredPeripheral = true
} else {
print("Trying to connect a non prefered peripheral")
}
// Invalidate and reset connection timer count, remove any connection errors
self.lastConnectionError = ""
self.timeoutTimerCount = 0
@ -259,7 +268,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
self.timeoutTimer!.invalidate()
}
// Map the peripheral to the connectedPeripheral ObservedObjects
connectedPeripheral = peripherals.filter({ $0.peripheral.identifier == peripheral.identifier }).first
@ -731,7 +740,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
// Get all the channels
var i: UInt32 = 1;
var max: Int32 = self.connectedPeripheral.maxChannels
let max: Int32 = self.connectedPeripheral.maxChannels
Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true) { timer in
@ -1146,6 +1155,56 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
return false
}
public func sendNodeDBReset(destNum: Int64) -> Bool {
var adminPacket = AdminMessage()
adminPacket.nodedbReset = 1
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = 0//UInt32(connectedPeripheral.num)
meshPacket.from = 0 //UInt32(connectedPeripheral.num)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
do {
try context!.save()
if meshLoggingEnabled { MeshLogger.log("💾 Sent a NodeDB Reset Admin Message for node: \(String(destNum))") }
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
PersistenceController.shared.clearDatabase()
return true
} catch {
context!.rollback()
let nsError = error as NSError
print("💥 Error Inserting New Core Data MessageEntity: \(nsError)")
}
}
return false
}
public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity) -> Int64 {
var adminPacket = AdminMessage()

View file

@ -10,6 +10,7 @@ class LocationHelper: NSObject, ObservableObject {
static let DefaultAltitude = CLLocationDistance(integerLiteral: 0)
static let DefaultSpeed = CLLocationSpeed(integerLiteral: 0)
static let DefaultHeading = CLLocationDirection(integerLiteral: 0)
static let DefaultTime = Date.init(timeIntervalSince1970: 0)
static var currentLocation: CLLocationCoordinate2D {
@ -46,7 +47,7 @@ class LocationHelper: NSObject, ObservableObject {
static var currentTimestamp: Date {
guard let timestamp = shared.locationManager.location?.timestamp else {
return Date.now
return DefaultTime
}
return timestamp
}

View file

@ -867,10 +867,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, meshLogging: Bool, context: NSManagedOb
if nodeInfo.position.latitudeI > 0 || nodeInfo.position.longitudeI > 0 {
let position = PositionEntity(context: context)
position.seqNo = Int32(nodeInfo.position.seqNumber)
position.latitudeI = nodeInfo.position.latitudeI
position.longitudeI = nodeInfo.position.longitudeI
position.altitude = nodeInfo.position.altitude
position.satsInView = Int32(nodeInfo.position.satsInView)
position.speed = Int32(nodeInfo.position.groundSpeed)
position.heading = Int32(nodeInfo.position.groundTrack)
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time)))
var newPostions = [PositionEntity]()
@ -1152,11 +1155,20 @@ func positionPacket (packet: MeshPacket, meshLogging: Bool, context: NSManagedOb
if fetchedNode.count == 1 {
let position = PositionEntity(context: context)
position.seqNo = Int32(positionMessage.seqNumber)
position.latitudeI = positionMessage.latitudeI
position.longitudeI = positionMessage.longitudeI
position.altitude = positionMessage.altitude
position.satsInView = Int32(positionMessage.satsInView)
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
position.speed = Int32(positionMessage.groundSpeed)
position.heading = Int32(positionMessage.groundTrack)
if positionMessage.timestamp != 0 {
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp)))
} else {
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
}
let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet
mutablePositions.add(position)

View file

@ -169,9 +169,12 @@
</entity>
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
</entity>

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

@ -13,11 +13,6 @@ class UserSettings: ObservableObject {
UserDefaults.standard.set(meshtasticUsername, forKey: "meshtasticusername")
}
}
@Published var preferredPeripheralName: String {
didSet {
UserDefaults.standard.set(preferredPeripheralName, forKey: "preferredPeripheralName")
}
}
@Published var preferredPeripheralId: String {
didSet {
UserDefaults.standard.set(preferredPeripheralId, forKey: "preferredPeripheralId")
@ -52,7 +47,6 @@ class UserSettings: ObservableObject {
init() {
self.meshtasticUsername = UserDefaults.standard.object(forKey: "meshtasticusername") as? String ?? ""
self.preferredPeripheralName = UserDefaults.standard.object(forKey: "preferredPeripheralName") as? String ?? ""
self.preferredPeripheralId = UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? ""
self.provideLocation = UserDefaults.standard.object(forKey: "provideLocation") as? Bool ?? false
self.provideLocationInterval = UserDefaults.standard.object(forKey: "provideLocationInterval") as? Int ?? 900

View file

@ -63,17 +63,10 @@ struct Connect: View {
if bleManager.connectedPeripheral != nil {
Text("FW Version: ").font(.caption)+Text(bleManager.connectedPeripheral.firmwareVersion)
.font(.caption).foregroundColor(Color.gray)
Text("Bitrate: ").font(.caption)+Text(String(format: "%.2f", bleManager.connectedPeripheral.bitrate ?? 0.00))
.font(.caption).foregroundColor(Color.gray)
Text("Channel Utilization: ").font(.caption)+Text(String(format: "%.2f", bleManager.connectedPeripheral.channelUtilization ?? 0.00))
.font(.caption).foregroundColor(Color.gray)
Text("Air Time: ").font(.caption)+Text(String(format: "%.2f", bleManager.connectedPeripheral.airTime ?? 0.00))
.font(.caption).foregroundColor(Color.gray)
}
if bleManager.connectedPeripheral.subscribed {
Text("Properly Subscribed").font(.caption)
.foregroundColor(.green)
} else {
Text("Attempting to connect. . . ").font(.caption)
.foregroundColor(.orange)
@ -86,38 +79,37 @@ struct Connect: View {
Text("Preferred").font(.caption2)
Text("Radio").font(.caption2)
Toggle("Preferred Radio", isOn: $isPreferredRadio)
Toggle("Preferred Radio", isOn: $bleManager.preferredPeripheral)
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.labelsHidden()
.onChange(of: isPreferredRadio) { value in
.onChange(of: bleManager.preferredPeripheral) { value in
if value {
if bleManager.connectedPeripheral != nil {
let deviceName = (bleManager.connectedPeripheral.peripheral.name ?? "")
userSettings.preferredPeripheralName = deviceName
} else {
userSettings.preferredPeripheralName = bleManager.connectedPeripheral.longName
userSettings.preferredPeripheralId = bleManager.connectedPeripheral!.peripheral.identifier.uuidString
bleManager.preferredPeripheral = true
isPreferredRadio = true
}
userSettings.preferredPeripheralId = bleManager.connectedPeripheral!.peripheral.identifier.uuidString
bleManager.preferredPeripheral = true
} else {
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.identifier.uuidString == userSettings.preferredPeripheralId {
userSettings.preferredPeripheralId = ""
userSettings.preferredPeripheralName = ""
bleManager.preferredPeripheral = false
isPreferredRadio = false
}
}
}
}
}
.font(.caption).foregroundColor(Color.gray)
.padding([.top, .bottom])
.swipeActions {
Button(role: .destructive) {
@ -129,8 +121,18 @@ struct Connect: View {
Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
}
}
.padding([.top, .bottom])
.contextMenu{
Text("Num: \(String(bleManager.connectedPeripheral.num))")
Text("Short Name: \(bleManager.connectedPeripheral.shortName)")
Text("Long Name: \(bleManager.connectedPeripheral.longName)")
Text("Unique Code: \(bleManager.connectedPeripheral.lastFourCode)")
Text("Max Channels: \(String(bleManager.connectedPeripheral.maxChannels))")
Text("Bitrate: \(String(format: "%.2f", bleManager.connectedPeripheral.bitrate ?? 0.00))")
Text("Ch. Utilization: \(String(format: "%.2f", bleManager.connectedPeripheral.channelUtilization ?? 0.00))")
Text("Air Time: \(String(format: "%.2f", bleManager.connectedPeripheral.airTime ?? 0.00))")
Text("RSSI: \(bleManager.connectedPeripheral.rssi)")
}
} else {
HStack {
@ -184,7 +186,7 @@ struct Connect: View {
}
}
HStack(alignment: .center) {
HStack(alignment: .center) {
Spacer()

View file

@ -0,0 +1,67 @@
//
// BatteryGauge.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 9/28/22.
//
import SwiftUI
import Charts
struct BatteryGauge: View {
@State var batteryLevel = 0.0
private let minValue = 1.0
private let maxValue = 100.00
var body: some View {
VStack {
if batteryLevel == 0.0 {
// Plugged in
Image(systemName: "powerplug")
.font(.largeTitle)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
} else {
let gradient = Gradient(colors: [.red, .orange, .green])
Gauge(value: batteryLevel, in: minValue...maxValue) {
if batteryLevel > 1.0 && batteryLevel < 10 {
Label("Battery Level %", systemImage: "battery.0")
} else if batteryLevel >= 10.0 && batteryLevel < 25.00 {
Label("Battery Level %", systemImage: "battery.25")
} else if batteryLevel >= 25.0 && batteryLevel < 50.00 {
Label("Battery Level %", systemImage: "battery.50")
} else if batteryLevel >= 50.0 && batteryLevel < 75.00 {
Label("Battery Level %", systemImage: "battery.75")
} else if batteryLevel >= 75.0 && batteryLevel <= 99.00 {
Label("Battery Level %", systemImage: "battery.100")
} else {
Label("Battery Level %", systemImage: "battery.100.bolt")
}
} currentValueLabel: {
Text(Int(batteryLevel), format: .percent)
}
.tint(gradient)
.gaugeStyle(.accessoryCircular)
}
}
}
}
struct BatteryGauge_Previews: PreviewProvider {
static var previews: some View {
VStack {
BatteryGauge(batteryLevel: 0.0)
BatteryGauge(batteryLevel: 9.0)
BatteryGauge(batteryLevel: 24.0)
BatteryGauge(batteryLevel: 49.0)
BatteryGauge(batteryLevel: 74.0)
BatteryGauge(batteryLevel: 99.0)
BatteryGauge(batteryLevel: 100.0)
}
}
}

View file

@ -1,63 +0,0 @@
import SwiftUI
struct BatteryIcon: View {
var batteryLevel: Int32?
var font: Font
var color: Color
var body: some View {
if batteryLevel == 100 {
Image(systemName: "battery.100.bolt")
.font(font)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel! < 100 && batteryLevel! > 74 {
Image(systemName: "battery.75")
.font(font)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel! < 75 && batteryLevel! > 49 {
Image(systemName: "battery.50")
.font(font)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel! < 50 && batteryLevel! > 14 {
Image(systemName: "battery.25")
.font(font)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel! == 0 {
Image(systemName: "powerplug")
.font(font)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else {
Image(systemName: "battery.0")
.font(font)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
}
}
}
struct BatteryIcon_Previews: PreviewProvider {
static var previews: some View {
BatteryIcon(batteryLevel: 100, font: .title2, color: Color.accentColor)
.previewLayout(.fixed(width: 75, height: 75))
BatteryIcon(batteryLevel: 99, font: .title2, color: Color.accentColor)
.previewLayout(.fixed(width: 75, height: 75))
BatteryIcon(batteryLevel: 74, font: .title2, color: Color.accentColor)
.previewLayout(.fixed(width: 75, height: 75))
BatteryIcon(batteryLevel: 49, font: .title2, color: Color.accentColor)
.previewLayout(.fixed(width: 75, height: 75))
BatteryIcon(batteryLevel: 14, font: .title2, color: Color.accentColor)
.previewLayout(.fixed(width: 75, height: 75))
}
}

View file

@ -16,7 +16,19 @@ struct DistanceText: View {
var body: some View {
let distanceFormatter = MKDistanceFormatter()
Text("Distance: \(distanceFormatter.string(fromDistance: Double(meters)))")
}
}
struct DistanceText_Previews: PreviewProvider {
static var previews: some View {
VStack {
DistanceText(meters: 100)
DistanceText(meters: 1000)
DistanceText(meters: 10000)
DistanceText(meters: 100000)
DistanceText(meters: 1000000)
}
}
}

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,127 @@
//
// 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("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,95 +65,25 @@ 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
}) {
Label("Power Off", systemImage: "power")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"Are you sure?",
isPresented: $isPresentingShutdownConfirm
) {
Button("Shutdown Node?", role: .destructive) {
if !bleManager.sendShutdown(destNum: node.num) {
print("Shutdown Failed")
}
}
}
}
Button(action: {
isPresentingRebootConfirm = true
}) {
Label("Reboot", systemImage: "arrow.triangle.2.circlepath")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"Are you sure?",
isPresented: $isPresentingRebootConfirm
) {
Button("Reboot Node?", role: .destructive) {
if !bleManager.sendReboot(destNum: node.num) {
print("Reboot Failed")
}
}
}
}
.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 +91,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 +104,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))
@ -198,7 +130,11 @@ struct NodeDetail: View {
.font(.largeTitle)
.foregroundColor(.gray)
.fixedSize()
}
}
if node.telemetries?.count ?? 0 >= 1 {
@ -206,33 +142,26 @@ struct NodeDetail: View {
let mostRecent = node.telemetries?.lastObject as! TelemetryEntity
Divider()
VStack(alignment: .center) {
BatteryIcon(batteryLevel: mostRecent.batteryLevel, font: .largeTitle, color: .accentColor)
.padding(.bottom, 10)
if mostRecent.batteryLevel > 0 {
Text(String(mostRecent.batteryLevel) + "%")
.font(.largeTitle)
.frame(width: 100)
.foregroundColor(.gray)
.fixedSize()
}
BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel))
if mostRecent.voltage > 0 {
Text(String(format: "%.2f", mostRecent.voltage) + " V")
.font(.largeTitle)
.font(.title)
.foregroundColor(.gray)
.fixedSize()
}
}
.padding()
}
Divider()
}
.padding()
Divider()
HStack(alignment: .center) {
VStack {
@ -293,11 +222,9 @@ struct NodeDetail: View {
HStack {
VStack(alignment: .center) {
Text("AKA").font(.title2).fixedSize()
CircleText(text: node.user?.shortName ?? "???", color: .accentColor)
.offset(y: 10)
}
.padding(5)
Divider()
@ -342,31 +269,23 @@ struct NodeDetail: View {
VStack(alignment: .center) {
BatteryIcon(batteryLevel: mostRecent.batteryLevel, font: .title, color: .accentColor)
.padding(.bottom)
BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel))
if mostRecent.batteryLevel > 0 {
Text(String(mostRecent.batteryLevel) + "%")
.font(.title3)
.foregroundColor(.gray)
.fixedSize()
}
if mostRecent.voltage > 0 {
Text(String(format: "%.2f", mostRecent.voltage) + " V")
.font(.title3)
.font(.callout)
.foregroundColor(.gray)
.fixedSize()
}
}
.padding(5)
}
}
.padding(4)
Divider()
HStack(alignment: .center) {
VStack {
HStack {
Image(systemName: "person")
@ -399,70 +318,148 @@ struct NodeDetail: View {
Text("MAC Address: ")
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray)
}
.padding([.bottom], 0)
Divider()
}
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)
if self.bleManager.connectedPeripheral != nil && self.bleManager.connectedPeripheral.num == node.num && self.bleManager.connectedPeripheral.num == node.num {
HStack {
if hwModelString == "TBEAM" || hwModelString == "TECHO" || hwModelString.contains("4631") {
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(destNum: node.num) {
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(destNum: node.num) {
print("Reboot Failed")
}
}
}
}
.padding(5)
}
}
.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,117 @@
//
// 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("Lat / Long")
.font(.caption2)
.fontWeight(.bold)
Text("Sat")
.font(.caption2)
.fontWeight(.bold)
Text("Alt")
.font(.caption2)
.fontWeight(.bold)
Text("Spd")
.font(.caption2)
.fontWeight(.bold)
Text("Hd")
.font(.caption2)
.fontWeight(.bold)
Text("Timestamp")
.font(.caption2)
.fontWeight(.bold)
}
Divider()
ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
GridRow {
Text("\(String(mappin.latitude ?? 0)) \(String(mappin.longitude ?? 0))")
.font(.caption2)
Text(String(mappin.satsInView))
.font(.caption2)
Text(String(mappin.altitude))
.font(.caption2)
Text(String(mappin.speed))
.font(.caption2)
Text(String(mappin.heading))
.font(.caption2)
Text(mappin.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
.font(.caption2)
}
}
}
.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

@ -105,18 +105,18 @@ struct AppSettings: View {
.disableAutocorrection(true)
.listRowSeparator(.visible)
HStack {
Label("Radio", systemImage: "flipphone")
Text(userSettings.preferredPeripheralName)
.foregroundColor(.gray)
}
Text("This option is set via the preferred radio toggle for the connected device on the bluetooth tab.")
.font(.caption)
.listRowSeparator(.hidden)
Text("The preferred radio will automatically reconnect if it becomes disconnected and is still within range.")
.font(.caption2)
.foregroundColor(.gray)
// HStack {
// Label("Radio", systemImage: "flipphone")
// Text(userSettings.preferredPeripheralName)
// .foregroundColor(.gray)
//
// }
// Text("This option is set via the preferred radio toggle for the connected device on the bluetooth tab.")
// .font(.caption)
// .listRowSeparator(.hidden)
// Text("The preferred radio will automatically reconnect if it becomes disconnected and is still within range.")
// .font(.caption2)
// .foregroundColor(.gray)
}
Section(header: Text("Options")) {

View file

@ -54,9 +54,58 @@ struct DeviceConfig: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
.disabled(bleManager.connectedPeripheral == nil)
HStack {
Button("Reset NodeDB", role: .destructive) {
isPresentingFactoryResetConfirm = true
}
.disabled(bleManager.connectedPeripheral == nil)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"Are you sure?",
isPresented: $isPresentingFactoryResetConfirm,
titleVisibility: .visible
) {
Button("Erase the NodeDB from node and app?", role: .destructive) {
if !bleManager.sendNodeDBReset(destNum: bleManager.connectedPeripheral.num) {
print("NodeDB Reset Failed")
}
}
}
Button("Factory Reset", role: .destructive) {
isPresentingFactoryResetConfirm = true
}
.disabled(bleManager.connectedPeripheral == nil)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"Are you sure?",
isPresented: $isPresentingFactoryResetConfirm,
titleVisibility: .visible
) {
Button("Erase all device settings?", role: .destructive) {
if !bleManager.sendFactoryReset(destNum: bleManager.connectedPeripheral.num) {
print("Factory Reset Failed")
}
}
}
}
HStack {
Button {
@ -102,28 +151,6 @@ struct DeviceConfig: View {
Text("After device config saves the node will reboot.")
}
Button("Factory Reset", role: .destructive) {
isPresentingFactoryResetConfirm = true
}
.disabled(bleManager.connectedPeripheral == nil)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"Are you sure?",
isPresented: $isPresentingFactoryResetConfirm
) {
Button("Erase all device settings?", role: .destructive) {
if !bleManager.sendFactoryReset(destNum: bleManager.connectedPeripheral.num) {
print("Factory Reset Failed")
}
}
}
}
Spacer()
}

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

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

View file

@ -13,7 +13,7 @@ struct PositionFlags: OptionSet
static let Altitude = PositionFlags(rawValue: 1)
static let AltitudeMsl = PositionFlags(rawValue: 2)
static let GeoSep = PositionFlags(rawValue: 4)
static let GeoidalSeparation = PositionFlags(rawValue: 4)
static let Dop = PositionFlags(rawValue: 8)
static let Hvdop = PositionFlags(rawValue: 16)
static let Satsinview = PositionFlags(rawValue: 32)
@ -48,7 +48,7 @@ struct PositionConfig: View {
/// Altitude value is MSL - 2
@State var includeAltitudeMsl = false
/// Include geoidal separation - 4
@State var includeGeoSep = false
@State var includeGeoidalSeparation = false
/// Include the DOP value ; PDOP used by default, see below - 8
@State var includeDop = false
/// If POS_DOP set, send separate HDOP / VDOP values instead of PDOP - 16
@ -56,7 +56,7 @@ struct PositionConfig: View {
/// Include number of "satellites in view" - 32
@State var includeSatsinview = false
/// Include a sequence number incremented per packet - 64
@State var includeSeqNos = false
@State var includeSeqNo = false
/// Include positional timestamp (from GPS solution) - 128
@State var includeTimestamp = false
/// Include positional heading - 256
@ -162,7 +162,7 @@ struct PositionConfig: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $includeSeqNos) { //64
Toggle(isOn: $includeSeqNo) { //64
Label("Sequence number", systemImage: "number")
}
@ -188,7 +188,7 @@ struct PositionConfig: View {
}
Section(header: Text("Advanced Position Flags")) {
Toggle(isOn: $includeGeoSep) {
Toggle(isOn: $includeGeoidalSeparation) {
Text("Geoidal Seperation")
}
@ -242,11 +242,11 @@ struct PositionConfig: View {
if includeAltitude { pf.insert(.Altitude) }
if includeAltitudeMsl { pf.insert(.AltitudeMsl) }
if includeGeoSep { pf.insert(.GeoSep) }
if includeGeoidalSeparation { pf.insert(.GeoidalSeparation) }
if includeDop { pf.insert(.Dop) }
if includeHvdop { pf.insert(.Hvdop) }
if includeSatsinview { pf.insert(.Satsinview) }
if includeSeqNos { pf.insert(.SeqNos) }
if includeSeqNo { pf.insert(.SeqNos) }
if includeTimestamp { pf.insert(.Timestamp) }
if includeSpeed { pf.insert(.Speed) }
if includeHeading { pf.insert(.Heading) }
@ -296,11 +296,11 @@ struct PositionConfig: View {
if pf.contains(.Altitude) { self.includeAltitude = true } else { self.includeAltitude = false }
if pf.contains(.AltitudeMsl) { self.includeAltitudeMsl = true } else { self.includeAltitudeMsl = false }
if pf.contains(.GeoSep) { self.includeGeoSep = true } else { self.includeGeoSep = false }
if pf.contains(.GeoidalSeparation) { self.includeGeoidalSeparation = true } else { self.includeGeoidalSeparation = false }
if pf.contains(.Dop) { self.includeDop = true } else { self.includeDop = false }
if pf.contains(.Hvdop) { self.includeHvdop = true } else { self.includeHvdop = false }
if pf.contains(.Satsinview) { self.includeSatsinview = true } else { self.includeSatsinview = false }
if pf.contains(.SeqNos) { self.includeSeqNos = true } else { self.includeSeqNos = false }
if pf.contains(.SeqNos) { self.includeSeqNo = true } else { self.includeSeqNo = false }
if pf.contains(.Timestamp) { self.includeTimestamp = true } else { self.includeTimestamp = false }
if pf.contains(.Speed) { self.includeSpeed = true } else { self.includeSpeed = false }
if pf.contains(.Heading) { self.includeHeading = true } else { self.includeHeading = false }
@ -352,7 +352,7 @@ struct PositionConfig: View {
if newPositionBroadcastSeconds != node!.positionConfig!.positionBroadcastSeconds { hasChanges = true }
}
}
.onChange(of: includeAltitude || includeAltitudeMsl || includeGeoSep || includeDop || includeHvdop || includeSatsinview || includeSeqNos || includeTimestamp || includeSpeed || includeHeading) { newFlags in
.onChange(of: includeAltitude || includeAltitudeMsl || includeGeoidalSeparation || includeDop || includeHvdop || includeSatsinview || includeSeqNo || includeTimestamp || includeSpeed || includeHeading) { newFlags in
// hasChanges = true
}
.navigationViewStyle(StackNavigationViewStyle())

View file

@ -17,11 +17,33 @@ struct MeshLog: View {
let url = logFile!
logs.removeAll()
for try await log in url.lines {
logs.append(log)
document.logFile.append("\(log) \n")
var lineCount = 0
let lineLimit = 500
// Get the number of lines
for try await _ in url.lines {
lineCount += 1
}
logs.reverse()
// Set the record to start with if we have more lines than the limit
var startingLog = 0
if lineCount > lineLimit {
startingLog = lineCount - lineLimit
}
var lineNumber = 0
for try await log in url.lines {
if lineNumber >= startingLog {
logs.append(log)
document.logFile.append("\(log) \n")
}
lineNumber += 1
}
logs.reverse()
} catch {
// Stop adding logs when an error is thrown
}

View file

@ -35,9 +35,18 @@ struct ShareChannels: View {
@EnvironmentObject var bleManager: BLEManager
@EnvironmentObject var userSettings: UserSettings
@State var includeChannel0 = true
@State var includeChannel1 = true
@State var includeChannel2 = true
@State var includeChannel3 = false
@State var includeChannel4 = false
@State var includeChannel5 = false
@State var includeChannel6 = false
@State var includeChannel7 = true
var node: NodeInfoEntity?
@State private var text = "https://meshtastic.org/E/#test"
@State private var channelsUrl = "https://meshtastic.org/e/#test"
var qrCodeImage = QrCodeImage()
var body: some View {
@ -51,54 +60,114 @@ struct ShareChannels: View {
ScrollView {
VStack {
Text("Scan the QR code below with the Apple or Android device you would like to share your channel settings with.")
.fixedSize(horizontal: false, vertical: true)
.font(.callout)
let image = qrCodeImage.generateQRCode(from: text)
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(
minWidth: smallest * 0.8,
maxWidth: smallest * 0.8,
minHeight: smallest * 0.8,
maxHeight: smallest * 0.8,
alignment: .center
)
if node != nil && node!.loRaConfig != nil {
if node != nil {
HStack {
Grid(alignment: .top, horizontalSpacing: 2) {
let preset = ModemPresets(rawValue: Int(node!.loRaConfig!.modemPreset))
Text("Modem Preset \(preset!.description)").font(.title3)
}
}
VStack {
Text("Number of Channels: \(node?.myInfo?.maxChannels ?? 0)").font(.title2)
if node != nil {
GridRow {
Spacer()
Text("Include")
.font(.caption)
.fontWeight(.bold)
Text("Name")
.font(.caption)
.fontWeight(.bold)
Spacer()
}
Divider()
ForEach(node!.myInfo!.channels?.array.sorted(by: { ($0 as! ChannelEntity).index < ($1 as! ChannelEntity).index }) as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in
VStack {
Text("Channel: \(channel.index) Name: \(channel.name ?? "")")
GridRow {
Spacer()
if channel.index == 0 {
Toggle("Channel 0 Included", isOn: $includeChannel0)
.toggleStyle(.switch)
.labelsHidden()
.disabled(true)
Text("Primary Channel")
} else if channel.index == 1 {
Toggle("Channel 1 Included", isOn: $includeChannel1)
.toggleStyle(.switch)
.labelsHidden()
Text("Public Channel")
} else if channel.index == 2 {
Toggle("Channel 2 Included", isOn: $includeChannel2)
.toggleStyle(.switch)
.labelsHidden()
} else if channel.index == 3 {
Toggle("Channel 3 Included", isOn: $includeChannel3)
.toggleStyle(.switch)
.labelsHidden()
} else if channel.index == 4 {
Toggle("Channel 4 Included", isOn: $includeChannel4)
.toggleStyle(.switch)
.labelsHidden()
.disabled(true)
} else if channel.index == 5 {
Toggle("Channel 5 Included", isOn: $includeChannel5)
.toggleStyle(.switch)
.labelsHidden()
.disabled(true)
} else if channel.index == 6 {
Toggle("Channel 6 Included", isOn: $includeChannel6)
.toggleStyle(.switch)
.labelsHidden()
.disabled(true)
} else if channel.index == 7 {
Toggle("Channel 7 Included", isOn: $includeChannel7)
.toggleStyle(.switch)
.labelsHidden()
Text("Admin Channel")
}
if channel.index > 1 && channel.index < 4{
Text("Private Chat - \(channel.index)")
}
if channel.index > 3 && channel.index < 7{
Text("Channel - \(channel.index)")
}
Spacer()
}
}
}
}
.frame(width: bounds.size.width, height: bounds.size.height)
}
let qrImage = qrCodeImage.generateQRCode(from: channelsUrl)
VStack {
Divider()
ShareLink("Share QR Code & Link",
item: Image(uiImage: qrImage),
subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"),
message: Text(channelsUrl),
preview: SharePreview("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you",
image: Image(uiImage: qrImage))
)
.presentationDetents([.large, .large])
Divider()
Image(uiImage: qrImage)
.resizable()
.scaledToFit()
.frame(
minWidth: smallest * 0.7,
maxWidth: smallest * 0.7,
minHeight: smallest * 0.7,
maxHeight: smallest * 0.7,
alignment: .top
)
}
}
.navigationTitle("Share Channel")
.navigationBarTitleDisplayMode(.automatic)
.navigationTitle("Generate QR Code")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})