mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
OS Log Viewer, device serial log over BLE
This commit is contained in:
parent
fec8d546bd
commit
26e785926f
10 changed files with 743 additions and 125 deletions
|
|
@ -7,7 +7,6 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
25183D462C0A6D97001E31D5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25183D452C0A6D97001E31D5 /* Logger.swift */; };
|
||||
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; };
|
||||
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; };
|
||||
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; };
|
||||
|
|
@ -33,7 +32,6 @@
|
|||
DD0E20FD2B87090400F2D100 /* clientonly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0E20FA2B87090400F2D100 /* clientonly.pb.swift */; };
|
||||
DD0E20FE2B87090400F2D100 /* paxcount.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0E20FB2B87090400F2D100 /* paxcount.pb.swift */; };
|
||||
DD0E21012B8A6F1300F2D100 /* DeviceHardware.json in Resources */ = {isa = PBXBuildFile; fileRef = DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */; };
|
||||
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */; };
|
||||
DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */; };
|
||||
DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */; };
|
||||
DD15E4F52B8BFC8E00654F61 /* PaxCounterLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD15E4F42B8BFC8E00654F61 /* PaxCounterLog.swift */; };
|
||||
|
|
@ -168,6 +166,10 @@
|
|||
DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DDCDC6CD29481FCC004C1DDA /* Localizable.strings */; };
|
||||
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */; };
|
||||
DDD43FE32A78C8900083A3E9 /* MqttClientProxyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */; };
|
||||
DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB082C285DDC007E03CA /* AppLog.swift */; };
|
||||
DDD5BB0B2C285E45007E03CA /* LogDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0A2C285E45007E03CA /* LogDetail.swift */; };
|
||||
DDD5BB0D2C285F00007E03CA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0C2C285F00007E03CA /* Logger.swift */; };
|
||||
DDD5BB102C285FB3007E03CA /* AppLogFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0F2C285FB3007E03CA /* AppLogFilter.swift */; };
|
||||
DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6EEAE29BC024700383354 /* Firmware.swift */; };
|
||||
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */; };
|
||||
DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */; };
|
||||
|
|
@ -245,7 +247,6 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
25183D452C0A6D97001E31D5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
|
||||
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
|
||||
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -274,7 +275,6 @@
|
|||
DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 28.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DeviceHardware.json; sourceTree = "<group>"; };
|
||||
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = "<group>"; };
|
||||
DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionPopover.swift; sourceTree = "<group>"; };
|
||||
DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV15.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaxCounterConfig.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -443,6 +443,10 @@
|
|||
DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 37.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = "<group>"; };
|
||||
DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClientProxyManager.swift; sourceTree = "<group>"; };
|
||||
DDD5BB082C285DDC007E03CA /* AppLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLog.swift; sourceTree = "<group>"; };
|
||||
DDD5BB0A2C285E45007E03CA /* LogDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDetail.swift; sourceTree = "<group>"; };
|
||||
DDD5BB0C2C285F00007E03CA /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
DDD5BB0F2C285FB3007E03CA /* AppLogFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogFilter.swift; sourceTree = "<group>"; };
|
||||
DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = "<group>"; };
|
||||
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = "<group>"; };
|
||||
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -606,9 +610,9 @@
|
|||
DD4A911C2708C57100501B7E /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DDD5BB0E2C285F92007E03CA /* Logs */,
|
||||
DD93800C2BA74CE3008BEC06 /* Channels */,
|
||||
DD97E96728EFE9A00056DDA4 /* About.swift */,
|
||||
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */,
|
||||
DD4A911D2708C65400501B7E /* AppSettings.swift */,
|
||||
DDAB580C2B0DAA9E00147258 /* Routes.swift */,
|
||||
DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */,
|
||||
|
|
@ -622,6 +626,7 @@
|
|||
DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */,
|
||||
DD61937A2863876A00E59241 /* Config */,
|
||||
DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */,
|
||||
DDD5BB082C285DDC007E03CA /* AppLog.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -917,19 +922,19 @@
|
|||
DDC2E18D26CE25CB0042C5E4 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */,
|
||||
DD5E523D298F5A7D00D21B61 /* Weather */,
|
||||
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */,
|
||||
DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */,
|
||||
DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */,
|
||||
DD47E3D526F17ED900029299 /* CircleText.swift */,
|
||||
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */,
|
||||
DDC3B273283F411B00AC321C /* LastHeardText.swift */,
|
||||
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */,
|
||||
DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */,
|
||||
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */,
|
||||
DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */,
|
||||
DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */,
|
||||
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */,
|
||||
DDC3B273283F411B00AC321C /* LastHeardText.swift */,
|
||||
DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */,
|
||||
DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */,
|
||||
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */,
|
||||
DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */,
|
||||
DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */,
|
||||
DD5E523D298F5A7D00D21B61 /* Weather */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -947,7 +952,6 @@
|
|||
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */,
|
||||
DDDB443C29F6592F00EE2349 /* NetworkManager.swift */,
|
||||
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */,
|
||||
25183D452C0A6D97001E31D5 /* Logger.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -970,6 +974,15 @@
|
|||
path = Mqtt;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DDD5BB0E2C285F92007E03CA /* Logs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DDD5BB0A2C285E45007E03CA /* LogDetail.swift */,
|
||||
DDD5BB0F2C285FB3007E03CA /* AppLogFilter.swift */,
|
||||
);
|
||||
path = Logs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DDDB26402AABEF7B003AFCB7 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1003,6 +1016,7 @@
|
|||
DD1933772B084F4200771CD5 /* Measurement.swift */,
|
||||
DDFFA7462B3A7F3C004730DB /* Bundle.swift */,
|
||||
DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */,
|
||||
DDD5BB0C2C285F00007E03CA /* Logger.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1237,6 +1251,7 @@
|
|||
DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */,
|
||||
DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */,
|
||||
DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */,
|
||||
DDD5BB102C285FB3007E03CA /* AppLogFilter.swift in Sources */,
|
||||
DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */,
|
||||
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */,
|
||||
DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */,
|
||||
|
|
@ -1258,8 +1273,10 @@
|
|||
DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */,
|
||||
DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */,
|
||||
DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */,
|
||||
DDD5BB0D2C285F00007E03CA /* Logger.swift in Sources */,
|
||||
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,
|
||||
DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */,
|
||||
DDD5BB0B2C285E45007E03CA /* LogDetail.swift in Sources */,
|
||||
DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */,
|
||||
DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */,
|
||||
B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */,
|
||||
|
|
@ -1307,7 +1324,6 @@
|
|||
D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */,
|
||||
DD0E20FE2B87090400F2D100 /* paxcount.pb.swift in Sources */,
|
||||
DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */,
|
||||
25183D462C0A6D97001E31D5 /* Logger.swift in Sources */,
|
||||
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */,
|
||||
DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */,
|
||||
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */,
|
||||
|
|
@ -1323,6 +1339,7 @@
|
|||
D93068DD2B81CA820066FBC8 /* ConfigHeader.swift in Sources */,
|
||||
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */,
|
||||
D9BC22DB2B7DE8E2006A37D5 /* TileDownloadStatus.swift in Sources */,
|
||||
DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */,
|
||||
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */,
|
||||
DD5E5202298EE33B00D21B61 /* admin.pb.swift in Sources */,
|
||||
DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */,
|
||||
|
|
@ -1371,7 +1388,6 @@
|
|||
DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */,
|
||||
DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */,
|
||||
DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */,
|
||||
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */,
|
||||
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
|
||||
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */,
|
||||
D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> String {
|
||||
var csvString: String = ""
|
||||
|
|
@ -64,6 +65,27 @@ func detectionsToCsv(detections: [MessageEntity]) -> String {
|
|||
return csvString
|
||||
}
|
||||
|
||||
func logToCsvFile(log: [OSLogEntryLog]) -> String {
|
||||
var csvString: String = ""
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
|
||||
// Create PAX Header
|
||||
csvString = "Process, Category, Level, Message, \("timestamp".localized)"
|
||||
for l in log {
|
||||
csvString += "\n"
|
||||
csvString += String(l.process)
|
||||
csvString += ", "
|
||||
csvString += String(l.category)
|
||||
csvString += ", "
|
||||
csvString += String(l.level.description)
|
||||
csvString += ", "
|
||||
csvString += String(l.composedMessage)
|
||||
csvString += ", "
|
||||
csvString += l.date.formattedDate(format: dateFormatString)
|
||||
}
|
||||
return csvString
|
||||
}
|
||||
|
||||
func paxToCsvFile(pax: [PaxCounterEntity]) -> String {
|
||||
var csvString: String = ""
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
|
||||
|
|
|
|||
75
Meshtastic/Extensions/Logger.swift
Normal file
75
Meshtastic/Extensions/Logger.swift
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// Logger.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 6/3/24.
|
||||
//
|
||||
|
||||
import OSLog
|
||||
|
||||
extension Logger {
|
||||
|
||||
/// The logger's subsystem.
|
||||
private static var subsystem = Bundle.main.bundleIdentifier!
|
||||
|
||||
/// All admin messages
|
||||
static let admin = Logger(subsystem: subsystem, category: "🏛 Admin")
|
||||
|
||||
/// All logs related to data such as decoding error, parsing issues, etc.
|
||||
static let data = Logger(subsystem: subsystem, category: "🗄️ Data")
|
||||
|
||||
/// All logs related to the mesh
|
||||
static let mesh = Logger(subsystem: subsystem, category: "🕸️ Mesh")
|
||||
|
||||
/// All logs related to MQTT
|
||||
static let mqtt = Logger(subsystem: subsystem, category: "📱 MQTT")
|
||||
|
||||
/// All detailed logs originating from the device (radio).
|
||||
static let radio = Logger(subsystem: subsystem, category: "📟 Radio")
|
||||
|
||||
/// All logs related to services such as network calls, location, etc.
|
||||
static let services = Logger(subsystem: subsystem, category: "🍏 Services")
|
||||
|
||||
/// All logs related to tracking and analytics.
|
||||
static let statistics = Logger(subsystem: subsystem, category: "📊 Stats")
|
||||
|
||||
|
||||
/// Fetch from the logstore
|
||||
static public func fetch(predicateFormat: String) async throws -> [OSLogEntryLog] {
|
||||
|
||||
let store = try OSLogStore(scope: .currentProcessIdentifier)
|
||||
let position = store.position(timeIntervalSinceLatestBoot: 0)
|
||||
//let calendar = Calendar.current
|
||||
//let dayAgo = calendar.date(byAdding: .day, value: -1, to: Date.now)
|
||||
//let position = store.position(date: dayAgo!)
|
||||
let predicate = NSPredicate(format: predicateFormat)
|
||||
let entries = try store.getEntries(at: position, matching: predicate)
|
||||
|
||||
var logs: [OSLogEntryLog] = []
|
||||
for entry in entries {
|
||||
|
||||
try Task.checkCancellation()
|
||||
|
||||
if let log = entry as? OSLogEntryLog {
|
||||
logs.append(log)
|
||||
}
|
||||
}
|
||||
|
||||
if logs.isEmpty { logs = [] }
|
||||
return logs
|
||||
}
|
||||
}
|
||||
|
||||
extension OSLogEntryLog.Level {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .undefined: "undefined"
|
||||
case .debug: "🩺 Debug"
|
||||
case .info: "ℹ️ Info"
|
||||
case .notice: "⚠️ Notice"
|
||||
case .error: "🚨 Error"
|
||||
case .fault: "💥 Fault"
|
||||
@unknown default: "default"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import SwiftUI
|
|||
import MapKit
|
||||
import CocoaMQTT
|
||||
import OSLog
|
||||
import RegexBuilder
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// Meshtastic BLE Device Manager
|
||||
|
|
@ -41,11 +42,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
var TORADIO_characteristic: CBCharacteristic!
|
||||
var FROMRADIO_characteristic: CBCharacteristic!
|
||||
var FROMNUM_characteristic: CBCharacteristic!
|
||||
var LOGRADIO_characteristic: CBCharacteristic!
|
||||
let meshtasticServiceCBUUID = CBUUID(string: "0x6BA1B218-15A8-461F-9FA8-5DCAE273EAFD")
|
||||
let TORADIO_UUID = CBUUID(string: "0xF75C76D2-129E-4DAD-A1DD-7866124401E7")
|
||||
let FROMRADIO_UUID = CBUUID(string: "0x2C55E69E-4993-11ED-B878-0242AC120002")
|
||||
let EOL_FROMRADIO_UUID = CBUUID(string: "0x8BA2BCC2-EE02-4A55-A531-C525C5E454D5")
|
||||
let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453")
|
||||
let LOGRADIO_UUID = CBUUID(string: "0x6C6FD238-78FA-436B-AACF-15C5BE1EF2E2")
|
||||
|
||||
// MARK: init BLEManager
|
||||
override init() {
|
||||
|
|
@ -277,7 +280,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
guard let services = peripheral.services else { return }
|
||||
for service in services where service.uuid == meshtasticServiceCBUUID {
|
||||
peripheral.discoverCharacteristics([TORADIO_UUID, FROMRADIO_UUID, FROMNUM_UUID], for: service)
|
||||
peripheral.discoverCharacteristics([TORADIO_UUID, FROMRADIO_UUID, FROMNUM_UUID, LOGRADIO_UUID], for: service)
|
||||
Logger.services.info("✅ BLE Service for Meshtastic discovered by \(peripheral.name ?? "Unknown")")
|
||||
}
|
||||
}
|
||||
|
|
@ -310,6 +313,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
Logger.services.info("✅ BLE did discover FROMNUM (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown")")
|
||||
FROMNUM_characteristic = characteristic
|
||||
peripheral.setNotifyValue(true, for: characteristic)
|
||||
|
||||
case LOGRADIO_UUID:
|
||||
Logger.services.info("✅ BLE did discover LOGRADIO (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown")")
|
||||
LOGRADIO_characteristic = characteristic
|
||||
peripheral.setNotifyValue(true, for: characteristic)
|
||||
|
||||
default:
|
||||
break
|
||||
|
|
@ -514,6 +522,74 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
|
||||
switch characteristic.uuid {
|
||||
case LOGRADIO_UUID:
|
||||
if (characteristic.value == nil || characteristic.value!.isEmpty) {
|
||||
return
|
||||
}
|
||||
let coordsSearch = Regex {
|
||||
Capture {
|
||||
Regex {
|
||||
"lat="
|
||||
OneOrMore(.digit)
|
||||
}
|
||||
}
|
||||
Capture {" "}
|
||||
Capture {
|
||||
Regex {
|
||||
"long="
|
||||
OneOrMore(.digit)
|
||||
}
|
||||
}
|
||||
}
|
||||
.anchorsMatchLineEndings()
|
||||
if var log = String(data: characteristic.value!, encoding: .utf8) {
|
||||
|
||||
/// Debug Log Level
|
||||
if (log.starts(with: "DEBUG |")) {
|
||||
do {
|
||||
let logString = log
|
||||
if let coordsMatch = try coordsSearch.firstMatch(in: logString) {
|
||||
log = "\(log.replacingOccurrences(of: "DEBUG |", with: "").trimmingCharacters(in: .whitespaces))"
|
||||
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
|
||||
Logger.radio.debug("🛰️ \(log.prefix(upTo: coordsMatch.range.lowerBound), privacy: .public) \(coordsMatch.0.replacingOccurrences(of: "[,]", with: "", options: .regularExpression), privacy: .private) \(log.suffix(from: coordsMatch.range.upperBound), privacy: .public)")
|
||||
}else {
|
||||
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
|
||||
Logger.radio.debug("🐞 \(log.replacingOccurrences(of: "DEBUG |", with: "").trimmingCharacters(in: .whitespaces), privacy: .public)")
|
||||
}
|
||||
|
||||
} catch {
|
||||
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
|
||||
Logger.radio.debug("🐞 \(log.replacingOccurrences(of: "DEBUG |", with: "").trimmingCharacters(in: .whitespaces), privacy: .public)")
|
||||
}
|
||||
} else if (log.starts(with: "INFO |")) {
|
||||
do {
|
||||
let logString = log
|
||||
if let coordsMatch = try coordsSearch.firstMatch(in: logString) {
|
||||
log = "\(log.replacingOccurrences(of: "INFO |", with: "").trimmingCharacters(in: .whitespaces))"
|
||||
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
|
||||
Logger.radio.info("🛰️ \(log.prefix(upTo: coordsMatch.range.lowerBound), privacy: .public) \(coordsMatch.0.replacingOccurrences(of: "[,]", with: "", options: .regularExpression), privacy: .private) \(log.suffix(from: coordsMatch.range.upperBound), privacy: .public)")
|
||||
} else {
|
||||
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
|
||||
Logger.radio.debug("🐞 \(log.replacingOccurrences(of: "INFO |", with: "").trimmingCharacters(in: .whitespaces), privacy: .public)")
|
||||
}
|
||||
} catch {
|
||||
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
|
||||
Logger.radio.info("✅ \(log.replacingOccurrences(of: "INFO |", with: "").trimmingCharacters(in: .whitespaces), privacy: .public)")
|
||||
}
|
||||
} else if (log.starts(with: "WARN |")) {
|
||||
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
|
||||
Logger.radio.warning("⚠️ \(log.replacingOccurrences(of: "WARN |", with: "").trimmingCharacters(in: .whitespaces), privacy: .public)")
|
||||
} else if (log.starts(with: "ERROR |")) {
|
||||
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
|
||||
Logger.radio.error("💥 \(log.replacingOccurrences(of: "ERROR |", with: "").trimmingCharacters(in: .whitespaces), privacy: .public)")
|
||||
} else if (log.starts(with: "CRIT |")) {
|
||||
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
|
||||
Logger.radio.critical("💥 \(log.replacingOccurrences(of: "CRIT |", with: "").trimmingCharacters(in: .whitespaces), privacy: .public)")
|
||||
} else {
|
||||
log = log.replacingOccurrences(of: "[,]", with: "", options: .regularExpression)
|
||||
Logger.radio.debug("📟 \(log)")
|
||||
}
|
||||
}
|
||||
|
||||
case FROMRADIO_UUID:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import OSLog
|
||||
|
||||
extension Logger {
|
||||
|
||||
/// The logger's subsystem.
|
||||
private static var subsystem = Bundle.main.bundleIdentifier!
|
||||
|
||||
/// All logs related to data such as decoding error, parsing issues, etc.
|
||||
static let data = Logger(subsystem: subsystem, category: "🗄️ Data")
|
||||
|
||||
/// All logs related to the mesh
|
||||
static let mesh = Logger(subsystem: subsystem, category: "🕸️ Mesh")
|
||||
|
||||
/// All logs related to services such as network calls, location, etc.
|
||||
static let services = Logger(subsystem: subsystem, category: "🍏 Services")
|
||||
|
||||
/// All logs related to tracking and analytics.
|
||||
static let statistics = Logger(subsystem: subsystem, category: "📈 Stats")
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
//
|
||||
// AdminMessageList.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 7/2/22.
|
||||
//
|
||||
/*
|
||||
Abstract:
|
||||
A view showing the details for a node.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
|
||||
struct AdminMessageList: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
var user: UserEntity?
|
||||
|
||||
var body: some View {
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssa", options: 0, locale: Locale.current)
|
||||
let localeTimeFormat = DateFormatter.dateFormat(fromTemplate: "h:mm:ss a", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss a")
|
||||
let timeFormatString = (localeTimeFormat ?? "h:mm:ss a")
|
||||
|
||||
List {
|
||||
if user != nil {
|
||||
|
||||
ForEach( user!.adminMessageList.reversed() ) { am in
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Text("\(am.adminDescription ?? "unknown".localized)")
|
||||
.font(.caption)
|
||||
|
||||
Text("Sent \(Date(timeIntervalSince1970: TimeInterval(am.messageTimestamp)).formattedDate(format: dateFormatString))")
|
||||
.foregroundColor(.gray)
|
||||
.font(.caption2)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
let ackErrorVal = RoutingError(rawValue: Int(am.ackError))
|
||||
|
||||
if am.ackTimestamp > 0 {
|
||||
if am.realACK {
|
||||
|
||||
Text(ackErrorVal?.display ?? "Empty Ack Error")
|
||||
.foregroundColor(am.receivedACK ? .gray : .red)
|
||||
.font(.caption2)
|
||||
} else {
|
||||
Text("Implicit ACK from another node")
|
||||
.foregroundColor(.orange)
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
|
||||
if am.receivedACK && am.ackTimestamp > 0 {
|
||||
Text(" \(Date(timeIntervalSince1970: TimeInterval(am.ackTimestamp)).formattedDate(format: timeFormatString))")
|
||||
.foregroundColor(am.realACK ? .gray : .orange)
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("admin.log")
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
||||
})
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
227
Meshtastic/Views/Settings/AppLog.swift
Normal file
227
Meshtastic/Views/Settings/AppLog.swift
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
//
|
||||
// AppLog.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 6/4/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
/// Needed for TableColumnForEach
|
||||
@available(iOS 17.4, *)
|
||||
struct AppLog: View {
|
||||
|
||||
@State private var logs: [OSLogEntryLog] = []
|
||||
@State private var sortOrder = [KeyPathComparator(\OSLogEntryLog.date, order: .reverse)]
|
||||
@State private var selection: OSLogEntry.ID?
|
||||
@State private var selectedLog: OSLogEntryLog?
|
||||
@State private var presentingErrorDetails: Bool = false
|
||||
@State private var searchText = ""
|
||||
@State private var category: Int = -1
|
||||
@State private var level: Int = -1
|
||||
@State var isExporting = false
|
||||
@State var exportString = ""
|
||||
@State var isEditingFilters = false
|
||||
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
private let dateFormatStyle = Date.FormatStyle()
|
||||
.hour(.twoDigits(amPM: .omitted))
|
||||
.minute()
|
||||
.second()
|
||||
.secondFraction(.fractional(3))
|
||||
|
||||
var body: some View {
|
||||
|
||||
Table(logs, selection: $selection, sortOrder: $sortOrder) {
|
||||
if idiom != .phone {
|
||||
TableColumn("log.time") { value in
|
||||
Text(value.date.formatted(dateFormatStyle))
|
||||
}
|
||||
.width(min: 125, max: 150)
|
||||
TableColumn("log.category", value: \.category)
|
||||
.width(min: 125, max: 150)
|
||||
TableColumn("log.level") { value in
|
||||
Text(value.level.description)
|
||||
}
|
||||
.width(min: 75, max: 100)
|
||||
|
||||
}
|
||||
TableColumn("log.message", value: \.composedMessage) { value in
|
||||
Text(value.composedMessage)
|
||||
.font(idiom == .phone ? .caption : .body)
|
||||
}
|
||||
.width(ideal: 200, max: .infinity)
|
||||
}
|
||||
.monospaced()
|
||||
.sheet(isPresented: $isEditingFilters) {
|
||||
AppLogFilter(category: $category, level: $level)
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .trailing) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isEditingFilters = !isEditingFilters
|
||||
}
|
||||
}) {
|
||||
Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.padding(.trailing, 5)
|
||||
.searchable(text: $searchText, placement: .navigationBarDrawer, prompt: "Search")
|
||||
.disabled(selection != nil)
|
||||
.overlay {
|
||||
if logs.isEmpty {
|
||||
ContentUnavailableView("No Logs Available", systemImage: "scroll")
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
await logs = searchAppLogs()
|
||||
logs.sort(using: sortOrder)
|
||||
}
|
||||
.onChange(of: sortOrder) { _, sortOrder in
|
||||
withAnimation {
|
||||
logs.sort(using: sortOrder)
|
||||
}
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
Task {
|
||||
await logs = searchAppLogs()
|
||||
logs.sort(using: sortOrder)
|
||||
}
|
||||
}
|
||||
.onChange(of: category) { _ in
|
||||
Task {
|
||||
await logs = searchAppLogs()
|
||||
logs.sort(using: sortOrder)
|
||||
}
|
||||
}
|
||||
.onChange(of: level) { _ in
|
||||
Task {
|
||||
await logs = searchAppLogs()
|
||||
logs.sort(using: sortOrder)
|
||||
}
|
||||
}
|
||||
.onChange(of: selection) { newSelection in
|
||||
presentingErrorDetails = true
|
||||
let log = logs.first {
|
||||
$0.id == newSelection
|
||||
}
|
||||
selectedLog = log
|
||||
}
|
||||
.sheet(item: $selectedLog, onDismiss: didDismiss) { log in
|
||||
LogDetail(log: log)
|
||||
.padding()
|
||||
}
|
||||
.task {
|
||||
logs = await searchAppLogs()
|
||||
logs.sort(using: sortOrder)
|
||||
}
|
||||
.fileExporter(
|
||||
isPresented: $isExporting,
|
||||
document: CsvDocument(emptyCsv: exportString),
|
||||
contentType: .commaSeparatedText,
|
||||
defaultFilename: String("Meshtastic Application Logs"),
|
||||
onCompletion: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.isExporting = false
|
||||
Logger.services.info("Application log download succeeded.")
|
||||
case .failure(let error):
|
||||
Logger.services.error("Application log download failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
)
|
||||
.navigationBarTitle("Debug Logs\(logs.isEmpty ? "" : " (\(logs.count))")", displayMode: .inline)
|
||||
.toolbar {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button(action: {
|
||||
Task {
|
||||
await logs = searchAppLogs()
|
||||
logs.sort(using: sortOrder)
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "arrow.clockwise.circle")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if !logs.isEmpty {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
exportString = logToCsvFile(log: logs)
|
||||
isExporting = true
|
||||
}) {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func didDismiss() {
|
||||
selection = nil
|
||||
selectedLog = nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.4, *)
|
||||
extension AppLog {
|
||||
@MainActor
|
||||
private func searchAppLogs() async -> [OSLogEntryLog] {
|
||||
do {
|
||||
/// Case Insensitive Search Text Predicates
|
||||
let searchPredicates = ["composedMessage", "category", "subsystem", "process"].map { property in
|
||||
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
|
||||
}
|
||||
/// Create a compound predicate using each text search preicate as an OR
|
||||
let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates)
|
||||
/// Create an array of predicates to hold our AND predicates
|
||||
var predicates: [NSPredicate] = []
|
||||
/// Subsystem Predicate
|
||||
let subsystemPredicate = NSPredicate(format: "subsystem IN %@", ["com.apple.SwiftUI", "com.apple.coredata", "gvh.MeshtasticClient"])
|
||||
predicates.append(subsystemPredicate)
|
||||
/// Category
|
||||
if category > -1 {
|
||||
let categoryPredicate = NSPredicate(format: "category == %@", LogCategories(rawValue: category)!.description)
|
||||
predicates.append(categoryPredicate)
|
||||
}
|
||||
/// Log Level
|
||||
if level > -1 {
|
||||
let levelPredicate = NSPredicate(format: "messageType == %@", LogLevels(rawValue: level)?.level ?? "info")
|
||||
predicates.append(levelPredicate)
|
||||
}
|
||||
|
||||
if predicates.count > 0 || !searchText.isEmpty {
|
||||
if !searchText.isEmpty {
|
||||
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
let compoundPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates])
|
||||
let logs = try await Logger.fetch(predicateFormat: compoundPredicate.predicateFormat)
|
||||
return logs
|
||||
} else {
|
||||
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
let logs = try await Logger.fetch(predicateFormat: filterPredicates.predicateFormat)
|
||||
return logs
|
||||
}
|
||||
} else {
|
||||
let logs = try await Logger.fetch(predicateFormat: subsystemPredicate.predicateFormat)
|
||||
|
||||
return logs
|
||||
}
|
||||
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OSLogEntry: Identifiable { }
|
||||
141
Meshtastic/Views/Settings/Logs/AppLogFilter.swift
Normal file
141
Meshtastic/Views/Settings/Logs/AppLogFilter.swift
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
//
|
||||
// AppLogFilter.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 6/15/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
enum LogCategories: Int, CaseIterable, Identifiable {
|
||||
|
||||
case admin = 0
|
||||
case data = 1
|
||||
case mesh = 2
|
||||
case mqtt = 3
|
||||
case radio = 4
|
||||
case services = 5
|
||||
case stats = 6
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var description: String {
|
||||
switch self {
|
||||
|
||||
case .admin:
|
||||
return "🏛 Admin"
|
||||
case .data:
|
||||
return "🗄️ Data"
|
||||
case .mesh:
|
||||
return "🕸️ Mesh"
|
||||
case .mqtt:
|
||||
return "📱 MQTT"
|
||||
case .radio:
|
||||
return "📟 Radio"
|
||||
case .services:
|
||||
return "🍏 Services"
|
||||
case .stats:
|
||||
return "📊 Stats"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LogLevels: Int, CaseIterable, Identifiable {
|
||||
|
||||
case debug = 0
|
||||
case info = 1
|
||||
case notice = 2
|
||||
case error = 3
|
||||
case fault = 4
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var level: String {
|
||||
switch self {
|
||||
case .debug:
|
||||
return "debug"
|
||||
case .info:
|
||||
return "info"
|
||||
case .notice:
|
||||
return "notice"
|
||||
case .error:
|
||||
return "error"
|
||||
case .fault:
|
||||
return "fault"
|
||||
}
|
||||
}
|
||||
var description: String {
|
||||
switch self {
|
||||
case .debug:
|
||||
return "🩺 Debug"
|
||||
case .info:
|
||||
return "ℹ️ Info"
|
||||
case .notice:
|
||||
return "⚠️ Notice"
|
||||
case .error:
|
||||
return "🚨 Error"
|
||||
case .fault:
|
||||
return "💥 Fault"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppLogFilter: View {
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
/// Filters
|
||||
var filterTitle = "App Log Filters"
|
||||
//@Binding
|
||||
@Binding var category: Int
|
||||
@Binding var level: Int
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(header: Text(filterTitle)) {
|
||||
HStack {
|
||||
Label("Category", systemImage: "square.grid.2x2")
|
||||
Picker("", selection: $category) {
|
||||
Text("All Categories")
|
||||
.tag(-1)
|
||||
ForEach(LogCategories.allCases) { lc in
|
||||
Text("\(lc.description)")
|
||||
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
|
||||
HStack {
|
||||
Label("Level", systemImage: "stairs")
|
||||
Picker("", selection: $level) {
|
||||
Text("All Levels")
|
||||
.tag(-1)
|
||||
ForEach(LogLevels.allCases) { ll in
|
||||
Text("\(ll.description)")
|
||||
//.tag(ll.rawValue)
|
||||
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
#endif
|
||||
}
|
||||
.presentationDetents([.fraction(0.6), .fraction(0.75)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
158
Meshtastic/Views/Settings/Logs/LogDetail.swift
Normal file
158
Meshtastic/Views/Settings/Logs/LogDetail.swift
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
//
|
||||
// LogDetail.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 6/5/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import OSLog
|
||||
|
||||
//@available(iOS 17.0, macOS 14.0, *)
|
||||
struct LogDetail: View {
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
var log: OSLogEntryLog
|
||||
var font: Font = .title2
|
||||
|
||||
private let dateFormatStyle = Date.FormatStyle()
|
||||
.day(.defaultDigits)
|
||||
.month(.defaultDigits)
|
||||
.year(.twoDigits)
|
||||
.hour(.twoDigits(amPM: .omitted))
|
||||
.minute()
|
||||
.second()
|
||||
.secondFraction(.fractional(3))
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Text("OS Log Entry Details")
|
||||
.font(.largeTitle)
|
||||
}
|
||||
Divider()
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading) {
|
||||
List {
|
||||
/// Time
|
||||
Label {
|
||||
Text("log.time".localized + ":")
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
|
||||
Text(log.date.formatted(dateFormatStyle))
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
} icon: {
|
||||
Image(systemName: "timer")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(idiom == .phone ? .callout : .title)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.listSectionSeparator(.hidden, edges: .top)
|
||||
.listSectionSeparator(.visible, edges: .bottom)
|
||||
/// Subsystem
|
||||
Label {
|
||||
Text("log.subsystem".localized + ":")
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
|
||||
Text(log.subsystem)
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
} icon: {
|
||||
Image(systemName: "gear")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.listRowSeparator(.visible)
|
||||
/// Process
|
||||
Label {
|
||||
Text("log.process".localized + ":")
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
|
||||
Text(log.process)
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
} icon: {
|
||||
Image(systemName: "tag")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.listRowSeparator(.visible)
|
||||
/// Category
|
||||
Label {
|
||||
Text("log.category".localized + ":")
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
|
||||
Text(log.category)
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
} icon: {
|
||||
Image(systemName: "square.grid.2x2")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.listRowSeparator(.visible)
|
||||
/// Level
|
||||
Label {
|
||||
Text("log.level".localized + ":")
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
|
||||
Text(log.level.description)
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
} icon: {
|
||||
Image(systemName: "stairs")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.listRowSeparator(.visible)
|
||||
/// message
|
||||
Label {
|
||||
Text("log.message".localized + ":")
|
||||
.font(idiom == .phone ? .caption : .title)
|
||||
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
|
||||
Text(log.composedMessage)
|
||||
.textSelection(.enabled)
|
||||
.font(idiom == .phone ? .body : .title)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
} icon: {
|
||||
Image(systemName: "text.bubble")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(idiom == .phone ? .callout : .title)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
}
|
||||
.listStyle(.plain)
|
||||
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
#endif
|
||||
}
|
||||
.monospaced()
|
||||
.presentationDetents([.fraction(0.75), .fraction(0.85), .fraction(1.0)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ struct Settings: View {
|
|||
case meshLog
|
||||
case adminMessageLog
|
||||
case about
|
||||
case appLog
|
||||
}
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
|
|
@ -412,17 +413,18 @@ struct Settings: View {
|
|||
}
|
||||
}
|
||||
.tag(SettingsSidebar.meshLog)
|
||||
NavigationLink {
|
||||
let connectedNode = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
AdminMessageList(user: connectedNode?.user)
|
||||
} label: {
|
||||
Label {
|
||||
Text("admin.log")
|
||||
} icon: {
|
||||
Image(systemName: "building.columns")
|
||||
if #available (iOS 17.4, *) {
|
||||
NavigationLink {
|
||||
AppLog()
|
||||
} label: {
|
||||
Label {
|
||||
Text("Debug Logs")
|
||||
} icon: {
|
||||
Image(systemName: "stethoscope")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.appLog)
|
||||
}
|
||||
.tag(SettingsSidebar.adminMessageLog)
|
||||
}
|
||||
Section(header: Text("Firmware")) {
|
||||
NavigationLink {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue