Merge pull request #124 from meshtastic/feature/module_settings

CSV export for Position and Device Metrics
This commit is contained in:
Garth Vander Houwen 2022-07-15 15:16:28 -07:00 committed by GitHub
commit e87132a50f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 204 additions and 9 deletions

View file

@ -47,6 +47,8 @@
DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD836AE626F6B38600ABCC23 /* Connect.swift */; };
DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD86D409287F04F100BAEB7A /* InvalidVersion.swift */; };
DD86D40C287F401000BAEB7A /* SaveChannelQRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */; };
DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD86D40E2881BE4C00BAEB7A /* CsvDocument.swift */; };
DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD86D4102881D16900BAEB7A /* WriteCsvFile.swift */; };
DD882F5D2772E4640005BF05 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD882F5C2772E4640005BF05 /* Contacts.swift */; };
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8EBF42285058FA00426DCA /* DisplayConfig.swift */; };
DD90860C26F684AF00DC5189 /* BatteryIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860B26F684AF00DC5189 /* BatteryIcon.swift */; };
@ -139,6 +141,8 @@
DD836AE626F6B38600ABCC23 /* Connect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connect.swift; sourceTree = "<group>"; };
DD86D409287F04F100BAEB7A /* InvalidVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvalidVersion.swift; sourceTree = "<group>"; };
DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelQRCode.swift; sourceTree = "<group>"; };
DD86D40E2881BE4C00BAEB7A /* CsvDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CsvDocument.swift; sourceTree = "<group>"; };
DD86D4102881D16900BAEB7A /* WriteCsvFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCsvFile.swift; sourceTree = "<group>"; };
DD882F5C2772E4640005BF05 /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = "<group>"; };
DD8EBF42285058FA00426DCA /* DisplayConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayConfig.swift; sourceTree = "<group>"; };
DD90860A26F645B700DC5189 /* Meshtastic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Meshtastic.entitlements; sourceTree = "<group>"; };
@ -254,7 +258,6 @@
DD3501882852FC3B000FC853 /* Settings.swift */,
DD4A911D2708C65400501B7E /* AppSettings.swift */,
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */,
DD8169FE272476C700F4AB02 /* LogDocument.swift */,
DD6B85A728009258000ACD6B /* ShareChannel.swift */,
DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */,
DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */,
@ -288,6 +291,16 @@
path = Module;
sourceTree = "<group>";
};
DD86D40D2881BDB300BAEB7A /* Export */ = {
isa = PBXGroup;
children = (
DD8169FE272476C700F4AB02 /* LogDocument.swift */,
DD86D40E2881BE4C00BAEB7A /* CsvDocument.swift */,
DD86D4102881D16900BAEB7A /* WriteCsvFile.swift */,
);
path = Export;
sourceTree = "<group>";
};
DD8EDE9226F97A2B00A5A10B /* Frameworks */ = {
isa = PBXGroup;
children = (
@ -345,6 +358,7 @@
DD90860A26F645B700DC5189 /* Meshtastic.entitlements */,
DDC4D5662754996200A4208E /* Persistence */,
DDAF8C5626ED07740058C060 /* Protobufs */,
DD86D40D2881BDB300BAEB7A /* Export */,
DDC2E1A526CEB32B0042C5E4 /* Helpers */,
DDC2E18726CE24E40042C5E4 /* Views */,
DDC2E18826CE24EE0042C5E4 /* Model */,
@ -667,6 +681,8 @@
DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */,
DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */,
DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */,
DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */,
DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */,
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */,
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */,
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */,

View file

@ -0,0 +1,38 @@
//
// CsvDocument.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 7/15/22.
//
import SwiftUI
import UniformTypeIdentifiers
struct CsvDocument: FileDocument {
static var readableContentTypes = [UTType.commaSeparatedText]
@State var csvData: String
init(emptyCsv: String = "" ) {
csvData = emptyCsv
}
init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents {
csvData = String(decoding: data, as: UTF8.self)
} else {
throw CocoaError(.fileReadCorruptFile)
}
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = Data(csvData.utf8)
return FileWrapper(regularFileWithContents: data)
}
}

View file

@ -0,0 +1,65 @@
//
// WriteCsvFile.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 7/15/22.
//
import SwiftUI
func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> String {
var csvString: String = ""
if metricsType == 0 {
// Create Device Metrics Header
csvString = "Battery Level, Voltage, Channel Utilization, Airtime, Timestamp"
for dm in telemetry{
if dm.metricsType == 0 {
csvString += "\n"
csvString += String("\(dm.batteryLevel) %")
csvString += ", "
csvString += String(dm.voltage)
csvString += ", "
csvString += String(dm.channelUtilization)
csvString += ", "
csvString += String(dm.airUtilTx)
csvString += ", "
csvString += dm.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? "Unknown Age"
}
}
} else {
// Create Device Telemetry Header
csvString = "Battery Level, Voltage, Channel Utilization, Airtime, Timestamp"
}
return csvString
}
func PositionToCsvFile(positions: [PositionEntity]) -> String {
var csvString: String = ""
// Create Position Header
csvString = "Latitude, Longitude, Altitude, Timestamp"
for pos in positions {
csvString += "\n"
csvString += String(pos.latitude ?? 0)
csvString += ", "
csvString += String(pos.longitude ?? 0)
csvString += ", "
csvString += String(pos.altitude)
csvString += ", "
csvString += pos.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? "Unknown Age"
}
return csvString
}

View file

@ -15,6 +15,12 @@ extension Date {
static var currentTimeStamp: Int64 {
return Int64(Date().timeIntervalSince1970 * 1000)
}
func formattedDate(format: String) -> String {
let dateformat = DateFormatter()
dateformat.dateFormat = format
return dateformat.string(from: self)
}
}
extension String {
@ -55,5 +61,4 @@ extension String {
UIGraphicsEndImageContext()
return image
}
}

View file

@ -12,7 +12,7 @@ struct MeshtasticAppleApp: App {
@ObservedObject private var userSettings: UserSettings = UserSettings()
@State var saveQR = false
@State var channelUrl = ""
@State private var channelUrl: URL?
@Environment(\.scenePhase) var scenePhase
@ -26,12 +26,11 @@ struct MeshtasticAppleApp: App {
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
print("QR Code URL received from the Camera \(userActivity)")
guard let url = userActivity.webpageURL else {
guard let channelUrl = userActivity.webpageURL else {
return
}
print("User wants to open URL: \(url)")
channelUrl = url.absoluteString
print("User wants to open URL: \(channelUrl)")
saveQR = true
}

View file

@ -11,6 +11,9 @@ 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 {
@ -96,7 +99,38 @@ struct LocationHistory: View {
}
}
}
Button {
exportString = PositionToCsvFile(positions: node.positions!.array as! [PositionEntity])
isExporting = true
} label: {
Label("Export", 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:

View file

@ -11,6 +11,9 @@ 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 {
@ -334,6 +337,19 @@ struct TelemetryLog: View {
}
}
}
Button {
exportString = TelemetryToCsvFile(telemetry: node.telemetries!.array as! [TelemetryEntity], metricsType: 0)
isExporting = true
} label: {
Label("Export", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.navigationTitle("Telemetry Log \(node.telemetries?.count ?? 0) Readings")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
@ -346,5 +362,24 @@ struct TelemetryLog: View {
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

@ -302,7 +302,7 @@ struct CannedMessagesConfig: View {
if rotary1Enabled {
/// Input event origin accepted by the canned messages
/// Can be e.g. "rotEnc1", "upDownEnc1", "cardkb", "faceskb" 623or keyword "_any"
/// Can be e.g. "rotEnc1", "upDownEnc1", "cardkb", "faceskb" or keyword "_any"
cmc.allowInputSource = "rotEnc1"
} else if updown1Enabled {
@ -373,6 +373,9 @@ struct CannedMessagesConfig: View {
// RAK Rotary Encoder
updown1Enabled = true
rotary1Enabled = false
inputbrokerEventCw = InputEventChars.keyUp.rawValue
inputbrokerEventCcw = InputEventChars.keyDown.rawValue
inputbrokerEventPress = InputEventChars.keySelect.rawValue
} else if newPreset == 2 {

View file

@ -8,7 +8,7 @@ import SwiftUI
struct SaveChannelQRCode: View {
@State var channelHash: String = "empty hash"
var channelHash: URL?
var body: some View {
@ -24,7 +24,7 @@ struct SaveChannelQRCode: View {
.font(.callout)
.padding()
Text(channelHash)
Text(String(channelHash?.path ?? "empty"))
.font(.title2)
.padding()