mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #124 from meshtastic/feature/module_settings
CSV export for Position and Device Metrics
This commit is contained in:
commit
e87132a50f
10 changed files with 204 additions and 9 deletions
|
|
@ -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 */,
|
||||
|
|
|
|||
38
Meshtastic/Export/CsvDocument.swift
Normal file
38
Meshtastic/Export/CsvDocument.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
65
Meshtastic/Export/WriteCsvFile.swift
Normal file
65
Meshtastic/Export/WriteCsvFile.swift
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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).")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue