diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cb1f6722..c70dcb98 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -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 = ""; }; 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = ""; }; 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = ""; }; 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; @@ -274,7 +275,6 @@ DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 28.xcdatamodel"; sourceTree = ""; }; DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DeviceHardware.json; sourceTree = ""; }; DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = ""; }; - DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = ""; }; DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionPopover.swift; sourceTree = ""; }; DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV15.xcdatamodel; sourceTree = ""; }; DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaxCounterConfig.swift; sourceTree = ""; }; @@ -443,6 +443,11 @@ DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 37.xcdatamodel"; sourceTree = ""; }; DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = ""; }; DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClientProxyManager.swift; sourceTree = ""; }; + DDD5BB082C285DDC007E03CA /* AppLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLog.swift; sourceTree = ""; }; + DDD5BB0A2C285E45007E03CA /* LogDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDetail.swift; sourceTree = ""; }; + DDD5BB0C2C285F00007E03CA /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + DDD5BB0F2C285FB3007E03CA /* AppLogFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogFilter.swift; sourceTree = ""; }; + DDD5BB142C28680D007E03CA /* MeshtasticDataModelV 38.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 38.xcdatamodel"; sourceTree = ""; }; DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = ""; }; DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = ""; }; DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = ""; }; @@ -606,9 +611,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 +627,7 @@ DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */, DD61937A2863876A00E59241 /* Config */, DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */, + DDD5BB082C285DDC007E03CA /* AppLog.swift */, ); path = Settings; sourceTree = ""; @@ -917,19 +923,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 = ""; @@ -947,7 +953,6 @@ DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, DDDB443C29F6592F00EE2349 /* NetworkManager.swift */, DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */, - 25183D452C0A6D97001E31D5 /* Logger.swift */, ); path = Helpers; sourceTree = ""; @@ -970,6 +975,15 @@ path = Mqtt; sourceTree = ""; }; + DDD5BB0E2C285F92007E03CA /* Logs */ = { + isa = PBXGroup; + children = ( + DDD5BB0A2C285E45007E03CA /* LogDetail.swift */, + DDD5BB0F2C285FB3007E03CA /* AppLogFilter.swift */, + ); + path = Logs; + sourceTree = ""; + }; DDDB26402AABEF7B003AFCB7 /* Helpers */ = { isa = PBXGroup; children = ( @@ -1003,6 +1017,7 @@ DD1933772B084F4200771CD5 /* Measurement.swift */, DDFFA7462B3A7F3C004730DB /* Bundle.swift */, DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */, + DDD5BB0C2C285F00007E03CA /* Logger.swift */, ); path = Extensions; sourceTree = ""; @@ -1237,6 +1252,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 +1274,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 +1325,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 +1340,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 +1389,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 */, @@ -1595,7 +1612,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.10; + MARKETING_VERSION = 2.3.12; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1629,7 +1646,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.10; + MARKETING_VERSION = 2.3.12; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1837,6 +1854,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDD5BB142C28680D007E03CA /* MeshtasticDataModelV 38.xcdatamodel */, DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */, DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */, DD268D8C2BCC7D11008073AE /* MeshtasticDataModelV 35.xcdatamodel */, @@ -1875,7 +1893,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */; + currentVersion = DDD5BB142C28680D007E03CA /* MeshtasticDataModelV 38.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..a62998be --- /dev/null +++ b/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,51 @@ +{ + "originHash" : "e9855e3a299c14a10f11ee0b8f29e4170b09548533939361223a0f50e7caac8c", + "pins" : [ + { + "identity" : "cocoamqtt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/emqx/CocoaMQTT", + "state" : { + "revision" : "85387a2478551ad84f39be8a3c8587d34dd2bcf5", + "version" : "2.1.5" + } + }, + { + "identity" : "mqttcocoaasyncsocket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/leeway1208/MqttCocoaAsyncSocket", + "state" : { + "revision" : "ce3e18607fd01079495f86ff6195d8a3ca469f73", + "version" : "1.0.8" + } + }, + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb", + "version" : "0.14.1" + } + }, + { + "identity" : "starscream", + "kind" : "remoteSourceControl", + "location" : "https://github.com/daltoniam/Starscream.git", + "state" : { + "revision" : "a063fda2b8145a231953c20e7a646be254365396", + "version" : "3.1.2" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "ce20dc083ee485524b802669890291c0d8090170", + "version" : "1.22.1" + } + } + ], + "version" : 3 +} diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index 521dc937..56776554 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -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) diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index a8adeaaf..0128cf93 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -14,10 +14,6 @@ extension UserEntity { self.value(forKey: "allMessages") as? [MessageEntity] ?? [MessageEntity]() } - var adminMessageList: [MessageEntity] { - self.value(forKey: "adminMessages") as? [MessageEntity] ?? [MessageEntity]() - } - var sensorMessageList: [MessageEntity] { self.value(forKey: "detectionSensorMessages") as? [MessageEntity] ?? [MessageEntity]() } diff --git a/Meshtastic/Extensions/Int.swift b/Meshtastic/Extensions/Int.swift index 40a12d8f..c9087d7f 100644 --- a/Meshtastic/Extensions/Int.swift +++ b/Meshtastic/Extensions/Int.swift @@ -15,3 +15,15 @@ extension Int { } } } + +extension UInt32 { + func toHex() -> String { + return String(format: "!%2X", self) + } +} + +extension Int64 { + func toHex() -> String { + return String(format: "!%2X", self) + } +} diff --git a/Meshtastic/Extensions/Logger.swift b/Meshtastic/Extensions/Logger.swift new file mode 100644 index 00000000..fbc68ea7 --- /dev/null +++ b/Meshtastic/Extensions/Logger.swift @@ -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" + } + } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index caab235d..87e33b4a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -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 @@ -388,7 +396,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } let messageDescription = "๐Ÿ›Ž๏ธ Requested Device Metadata for node \(toUser.longName ?? "unknown".localized) by \(fromUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return Int64(meshPacket.id) } return 0 @@ -492,7 +500,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { - Logger.services.error("didUpdateNotificationStateFor error: \(error?.localizedDescription ?? "Unknown")") + if let error { + Logger.services.error("๐Ÿ’ฅ BLE didUpdateNotificationStateFor error: \(characteristic.uuid, privacy: .public) \(error.localizedDescription, privacy: .public)") + } else { + Logger.services.info("โ„น๏ธ peripheral didUpdateNotificationStateFor \(characteristic.uuid, privacy: .public)") + } } // MARK: Data Read / Update Characteristic Event @@ -514,6 +526,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: @@ -1065,7 +1145,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } let messageDescription = "๐Ÿš€ Sent Set Fixed Postion Admin Message to: \(fromUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: fromUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -1090,7 +1170,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } let messageDescription = "๐Ÿš€ Sent Remove Fixed Position Admin Message to: \(fromUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: fromUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -1160,7 +1240,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } let messageDescription = "๐Ÿš€ Sent Shutdown Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -1185,7 +1265,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } let messageDescription = "๐Ÿš€ Sent Reboot Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -1210,7 +1290,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } let messageDescription = "๐Ÿš€ Sent Reboot OTA Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -1236,7 +1316,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } automaticallyReconnect = false let messageDescription = "๐Ÿš€ Sent enter DFU mode Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -1261,7 +1341,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } let messageDescription = "๐Ÿš€ Sent Factory Reset Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -1285,7 +1365,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿš€ Sent NodeDB Reset Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -1328,7 +1408,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐ŸŽ›๏ธ Requested Channel \(channel.index) for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return Int64(meshPacket.id) } return 0 @@ -1352,7 +1432,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved Channel \(channel.index) for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return Int64(meshPacket.id) } return 0 @@ -1498,7 +1578,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } let messageDescription = "๐Ÿ›Ÿ Saved User Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return Int64(meshPacket.id) } return 0 @@ -1619,7 +1699,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved Ham Parameters for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return Int64(meshPacket.id) } return 0 @@ -1643,7 +1723,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved Bluetooth Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1671,7 +1751,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved Device Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1698,7 +1778,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved Display Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1725,7 +1805,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved LoRa Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1756,7 +1836,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ÿ Saved Position Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertPositionConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1787,7 +1867,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ÿ Saved Power Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertPowerConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1818,7 +1898,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ÿ Saved Network Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertNetworkConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1848,7 +1928,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ÿ Saved Ambient Lighting Module Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1878,7 +1958,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ÿ Saved Canned Message Module Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1909,7 +1989,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ÿ Saved Canned Message Module Messages for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return Int64(meshPacket.id) } @@ -1939,7 +2019,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved Detection Sensor Module Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertDetectionSensorModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1968,7 +2048,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved External Notification Module Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertExternalNotificationModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -1997,7 +2077,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved PAX Counter Module Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertPaxCounterModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -2027,7 +2107,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ÿ Saved RTTTL Ringtone Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -2058,7 +2138,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved MQTT Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertMqttModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -2087,7 +2167,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ÿ Saved Range Test Module Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertRangeTestModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -2117,7 +2197,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved Serial Module Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertSerialModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -2146,7 +2226,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ÿ Saved Store & Forward Module Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertStoreForwardModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -2175,7 +2255,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "Saved Telemetry Module Config for \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { upsertTelemetryModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } @@ -2206,7 +2286,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ž๏ธ Sent a Get Channel \(channelIndex) Request Admin Message for node: \(toUser.longName ?? "unknown".localized))" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } @@ -2280,7 +2360,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ž๏ธ Requested Bluetooth Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2311,7 +2391,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ž๏ธ Requested Device Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2342,7 +2422,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ž๏ธ Requested Display Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2373,7 +2453,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ž๏ธ Requested LoRa Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } @@ -2405,7 +2485,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "๐Ÿ›Ž๏ธ Requested Network Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2435,7 +2515,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested Position Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2465,7 +2545,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested Power Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2495,7 +2575,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested Ambient Lighting Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2525,7 +2605,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested Canned Messages Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2555,7 +2635,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested External Notificaiton Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2585,7 +2665,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested PAX Counter Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2615,7 +2695,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested RTTTL Ringtone Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2645,7 +2725,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested Range Test Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2675,7 +2755,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested MQTT Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2705,7 +2785,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested Detection Sensor Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2735,7 +2815,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested Serial Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2765,7 +2845,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested Store and Forward Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false @@ -2795,14 +2875,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "๐Ÿ›Ž๏ธ Requested Telemetry Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { return true } return false } // Send an admin message to a radio, save a message to core data for logging - private func sendAdminMessageToRadio(meshPacket: MeshPacket, adminDescription: String, fromUser: UserEntity, toUser: UserEntity) -> Bool { + private func sendAdminMessageToRadio(meshPacket: MeshPacket, adminDescription: String) -> Bool { var toRadio: ToRadio! toRadio = ToRadio() @@ -2812,25 +2892,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { - let newMessage = MessageEntity(context: context!) - newMessage.messageId = Int64(meshPacket.id) - newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) - newMessage.receivedACK = false - newMessage.admin = true - newMessage.adminDescription = adminDescription - newMessage.fromUser = fromUser - newMessage.toUser = toUser - - do { - connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - try context!.save() - Logger.mesh.debug("\(adminDescription)") - return true - } catch { - context!.rollback() - let nsError = error as NSError - Logger.data.error("Error inserting new core data MessageEntity: \(nsError)") - } + connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) + Logger.mesh.debug("\(adminDescription)") + return true } return false } diff --git a/Meshtastic/Helpers/Logger.swift b/Meshtastic/Helpers/Logger.swift deleted file mode 100644 index bf9ad575..00000000 --- a/Meshtastic/Helpers/Logger.swift +++ /dev/null @@ -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") -} diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 3250334b..e81fee1d 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -683,6 +683,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage telemetry.voltage = telemetryMessage.deviceMetrics.voltage telemetry.uptimeSeconds = Int32(telemetryMessage.deviceMetrics.uptimeSeconds) telemetry.metricsType = 0 + Logger.statistics.info("๐Ÿ“ˆ [Mesh Statistics] Channel Utilization: \(telemetryMessage.deviceMetrics.channelUtilization) Airtime: \(telemetryMessage.deviceMetrics.airUtilTx) for Node: \(packet.from.toHex())") } else if telemetryMessage.variant == Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) { // Environment Metrics telemetry.barometricPressure = telemetryMessage.environmentMetrics.barometricPressure @@ -708,7 +709,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage try context.save() // Only log telemetry from the mesh not the connected device if connectedNode != Int64(packet.from) { - Logger.data.info("๐Ÿ’พ Telemetry Saved for Node: \(packet.from)") + Logger.data.info("๐Ÿ’พ [Telemetry] Saved for Node: \(packet.from.toHex())") } else if telemetry.metricsType == 0 { // Connected Device Metrics // ------------------------ @@ -749,10 +750,10 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage } catch { context.rollback() let nsError = error as NSError - Logger.data.error("Error Saving Telemetry for Node \(packet.from) Error: \(nsError)") + Logger.data.error("๐Ÿ’ฅ Error Saving Telemetry for Node \(packet.from, privacy: .public) Error: \(nsError, privacy: .public)") } } else { - Logger.data.error("Error Fetching NodeInfoEntity for Node \(packet.from)") + Logger.data.error("๐Ÿ’ฅ Error Fetching NodeInfoEntity for Node \(packet.from.toHex(), privacy: .public)") } } diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index a2e363d0..66f94366 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -81,28 +81,28 @@ class MqttClientProxyManager { } } func subscribe(topic: String, qos: CocoaMQTTQoS) { - Logger.services.info("๐Ÿ“ฒ MQTT Client Proxy subscribed to: \(topic)") + Logger.mqtt.info("๐Ÿ“ฒ [MQTT Client Proxy] subscribed to: \(topic)") mqttClientProxy?.subscribe(topic, qos: qos) } func unsubscribe(topic: String) { mqttClientProxy?.unsubscribe(topic) - Logger.services.info("๐Ÿ“ฒ MQTT Client Proxy unsubscribe for: \(topic)") + Logger.mqtt.info("๐Ÿ“ฒ [MQTT Client Proxy] unsubscribe to topic: \(topic)") } func publish(message: String, topic: String, qos: CocoaMQTTQoS) { mqttClientProxy?.publish(topic, withString: message, qos: qos) - Logger.services.debug("๐Ÿ“ฒ MQTT Client Proxy publish for: \(topic)") + Logger.mqtt.debug("๐Ÿ“ฒ [MQTT Client Proxy] publish for: \(topic)") } func disconnect() { if let client = mqttClientProxy { client.disconnect() - Logger.services.info("๐Ÿ“ฒ MQTT Client Proxy Disconnected") + Logger.mqtt.info("๐Ÿ“ฒ [MQTT Client Proxy] disconnected") } } } extension MqttClientProxyManager: CocoaMQTTDelegate { func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) { - Logger.services.info("๐Ÿ“ฒ MQTT Client Proxy didConnectAck: \(ack)") + Logger.mqtt.info("๐Ÿ“ฒ MQTT Client Proxy didConnectAck: \(ack)") if ack == .accept { delegate?.onMqttConnected() } else { @@ -130,34 +130,33 @@ extension MqttClientProxyManager: CocoaMQTTDelegate { } } func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) { - Logger.services.debug("mqttDidDisconnect: \(err?.localizedDescription ?? "")") - + Logger.mqtt.debug("๐Ÿ“ฒ [MQTT Client Proxy] disconnected: \(err?.localizedDescription ?? "")") if let error = err { delegate?.onMqttError(message: error.localizedDescription) } delegate?.onMqttDisconnected() } func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) { - Logger.services.debug("๐Ÿ“ฒ MQTT Client Proxy didPublishMessage from MqttClientProxyManager: \(message)") + Logger.mqtt.info("๐Ÿ“ฒ [MQTT Client Proxy] published messsage from MqttClientProxyManager: \(message)") } func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) { - Logger.services.debug("๐Ÿ“ฒ MQTT Client Proxy didPublishAck from MqttClientProxyManager: \(id)") + Logger.mqtt.info("๐Ÿ“ฒ [MQTT Client Proxy] published Ack from MqttClientProxyManager: \(id)") } public func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) { delegate?.onMqttMessageReceived(message: message) - Logger.services.debug("๐Ÿ“ฒ MQTT Client Proxy message received on topic: \(message.topic)") + Logger.mqtt.info("๐Ÿ“ฒ [MQTT Client Proxy] message received on topic: \(message.topic)") } func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) { - Logger.services.info("๐Ÿ“ฒ MQTT Client Proxy didSubscribeTopics: \(success.allKeys.count) topics. failed: \(failed.count) topics") + Logger.mqtt.debug("๐Ÿ“ฒ [MQTT Client Proxy] subscribed to topics: \(success.allKeys.count) topics. failed: \(failed.count) topics") } func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopics topics: [String]) { - Logger.services.info("didUnsubscribeTopics: \(topics.joined(separator: ", "))") + Logger.mqtt.debug("๐Ÿ“ฒ [MQTT Client Proxy] unsubscribed from topics: \(topics.joined(separator: "- "))") } func mqttDidPing(_ mqtt: CocoaMQTT) { - Logger.services.info("๐Ÿ“ฒ MQTT Client Proxy mqttDidPing") + Logger.mqtt.debug("๐Ÿ“ฒ [MQTT Client Proxy] ping") } func mqttDidReceivePong(_ mqtt: CocoaMQTT) { - Logger.services.info("๐Ÿ“ฒ MQTT Client Proxy mqttDidReceivePong") + Logger.mqtt.debug("๐Ÿ“ฒ [MQTT Client Proxy] pong") } } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 63ed7101..60188826 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 37.xcdatamodel + MeshtasticDataModelV 38.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 38.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 38.xcdatamodel/contents new file mode 100644 index 00000000..10948c6b --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 38.xcdatamodel/contents @@ -0,0 +1,465 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 1ef04ff7..3479a538 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -134,7 +134,7 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) { - let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, String(packet.from)) + let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, packet.from.toHex()) MeshLogger.log("๐Ÿ“Ÿ \(logString)") guard packet.from > 0 else { return } @@ -383,18 +383,16 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } } -func upsertBluetoothConfigPacket(config: Meshtastic.Config.BluetoothConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.bluetooth.config %@".localized, String(nodeNum)) MeshLogger.log("๐Ÿ“ถ \(logString)") - let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { - return - } + let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save Device Config if !fetchedNode.isEmpty { if fetchedNode[0].bluetoothConfig == nil { @@ -402,26 +400,28 @@ func upsertBluetoothConfigPacket(config: Meshtastic.Config.BluetoothConfig, node newBluetoothConfig.enabled = config.enabled newBluetoothConfig.mode = Int32(config.mode.rawValue) newBluetoothConfig.fixedPin = Int32(config.fixedPin) + newBluetoothConfig.deviceLoggingEnabled = config.deviceLoggingEnabled fetchedNode[0].bluetoothConfig = newBluetoothConfig } else { fetchedNode[0].bluetoothConfig?.enabled = config.enabled fetchedNode[0].bluetoothConfig?.mode = Int32(config.mode.rawValue) fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.fixedPin) + fetchedNode[0].bluetoothConfig?.deviceLoggingEnabled = config.deviceLoggingEnabled } do { try context.save() - Logger.data.info("๐Ÿ’พ Updated Bluetooth Config for node number: \(String(nodeNum))") + Logger.data.info("๐Ÿ’พ Updated Bluetooth Config for node: \(nodeNum.toHex(), privacy: .public)") } catch { context.rollback() let nsError = error as NSError - Logger.data.error("Error Updating Core Data BluetoothConfigEntity: \(nsError)") + Logger.data.error("๐Ÿ’ฅ Error Updating Core Data BluetoothConfigEntity: \(nsError, privacy: .public)") } } else { - Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Bluetooth Config") + Logger.data.error("๐Ÿ’ฅ No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Bluetooth Config") } } catch { let nsError = error as NSError - Logger.data.error("Fetching node for core data BluetoothConfigEntity failed: \(nsError)") + Logger.data.error("๐Ÿ’ฅ Fetching node for core data BluetoothConfigEntity failed: \(nsError, privacy: .public)") } } diff --git a/Meshtastic/Protobufs/meshtastic/admin.pb.swift b/Meshtastic/Protobufs/meshtastic/admin.pb.swift index 230f5304..c3f1c12f 100644 --- a/Meshtastic/Protobufs/meshtastic/admin.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/admin.pb.swift @@ -245,6 +245,16 @@ struct AdminMessage { set {payloadVariant = .deleteFileRequest(newValue)} } + /// + /// Set zero and offset for scale chips + var setScale: UInt32 { + get { + if case .setScale(let v)? = payloadVariant {return v} + return 0 + } + set {payloadVariant = .setScale(newValue)} + } + /// /// Set the owner for this node var setOwner: User { @@ -513,6 +523,9 @@ struct AdminMessage { /// Delete the file by the specified path from the device case deleteFileRequest(String) /// + /// Set zero and offset for scale chips + case setScale(UInt32) + /// /// Set the owner for this node case setOwner(User) /// @@ -667,6 +680,10 @@ struct AdminMessage { guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } return l == r }() + case (.setScale, .setScale): return { + guard case .setScale(let l) = lhs, case .setScale(let r) = rhs else { preconditionFailure() } + return l == r + }() case (.setOwner, .setOwner): return { guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } return l == r @@ -1039,6 +1056,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 20: .standard(proto: "get_node_remote_hardware_pins_response"), 21: .standard(proto: "enter_dfu_mode_request"), 22: .standard(proto: "delete_file_request"), + 23: .standard(proto: "set_scale"), 32: .standard(proto: "set_owner"), 33: .standard(proto: "set_channel"), 34: .standard(proto: "set_config"), @@ -1274,6 +1292,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .deleteFileRequest(v) } }() + case 23: try { + var v: UInt32? + try decoder.decodeSingularUInt32Field(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .setScale(v) + } + }() case 32: try { var v: User? var hadOneofValue = false @@ -1546,6 +1572,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .deleteFileRequest(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularStringField(value: v, fieldNumber: 22) }() + case .setScale?: try { + guard case .setScale(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 23) + }() case .setOwner?: try { guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 32) diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index 90ea2f40..11453fa7 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -630,6 +630,11 @@ struct Config { /// I2C address of INA_2XX to use for reading device battery voltage var deviceBatteryInaAddress: UInt32 = 0 + /// + /// If non-zero, we want powermon log outputs. With the particular (bitfield) sources enabled. + /// Note: we picked an ID of 32 so that lower more efficient IDs can be used for more frequently used options. + var powermonEnables: UInt64 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -799,6 +804,10 @@ struct Config { /// Should we wake the screen up on accelerometer detected motion or tap var wakeOnTapOrMotion: Bool = false + /// + /// Indicates how to rotate or invert the compass output to accurate display on the display. + var compassOrientation: Config.DisplayConfig.CompassOrientation = .degrees0 + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -998,6 +1007,76 @@ struct Config { } + enum CompassOrientation: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// + /// The compass and the display are in the same orientation. + case degrees0 // = 0 + + /// + /// Rotate the compass by 90 degrees. + case degrees90 // = 1 + + /// + /// Rotate the compass by 180 degrees. + case degrees180 // = 2 + + /// + /// Rotate the compass by 270 degrees. + case degrees270 // = 3 + + /// + /// Don't rotate the compass, but invert the result. + case degrees0Inverted // = 4 + + /// + /// Rotate the compass by 90 degrees and invert. + case degrees90Inverted // = 5 + + /// + /// Rotate the compass by 180 degrees and invert. + case degrees180Inverted // = 6 + + /// + /// Rotate the compass by 270 degrees and invert. + case degrees270Inverted // = 7 + case UNRECOGNIZED(Int) + + init() { + self = .degrees0 + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .degrees0 + case 1: self = .degrees90 + case 2: self = .degrees180 + case 3: self = .degrees270 + case 4: self = .degrees0Inverted + case 5: self = .degrees90Inverted + case 6: self = .degrees180Inverted + case 7: self = .degrees270Inverted + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .degrees0: return 0 + case .degrees90: return 1 + case .degrees180: return 2 + case .degrees270: return 3 + case .degrees0Inverted: return 4 + case .degrees90Inverted: return 5 + case .degrees180Inverted: return 6 + case .degrees270Inverted: return 7 + case .UNRECOGNIZED(let i): return i + } + } + + } + init() {} } @@ -1334,6 +1413,10 @@ struct Config { /// Specified PIN for PairingMode.FixedPin var fixedPin: UInt32 = 0 + /// + /// Enables device (serial style logs) over Bluetooth + var deviceLoggingEnabled: Bool = false + var unknownFields = SwiftProtobuf.UnknownStorage() enum PairingMode: SwiftProtobuf.Enum { @@ -1485,6 +1568,20 @@ extension Config.DisplayConfig.DisplayMode: CaseIterable { ] } +extension Config.DisplayConfig.CompassOrientation: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Config.DisplayConfig.CompassOrientation] = [ + .degrees0, + .degrees90, + .degrees180, + .degrees270, + .degrees0Inverted, + .degrees90Inverted, + .degrees180Inverted, + .degrees270Inverted, + ] +} + extension Config.LoRaConfig.RegionCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. static let allCases: [Config.LoRaConfig.RegionCode] = [ @@ -1553,6 +1650,7 @@ extension Config.DisplayConfig.GpsCoordinateFormat: @unchecked Sendable {} extension Config.DisplayConfig.DisplayUnits: @unchecked Sendable {} extension Config.DisplayConfig.OledType: @unchecked Sendable {} extension Config.DisplayConfig.DisplayMode: @unchecked Sendable {} +extension Config.DisplayConfig.CompassOrientation: @unchecked Sendable {} extension Config.LoRaConfig: @unchecked Sendable {} extension Config.LoRaConfig.RegionCode: @unchecked Sendable {} extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {} @@ -1986,6 +2084,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 7: .standard(proto: "ls_secs"), 8: .standard(proto: "min_wake_secs"), 9: .standard(proto: "device_battery_ina_address"), + 32: .standard(proto: "powermon_enables"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2002,6 +2101,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple case 7: try { try decoder.decodeSingularUInt32Field(value: &self.lsSecs) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &self.minWakeSecs) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &self.deviceBatteryInaAddress) }() + case 32: try { try decoder.decodeSingularUInt64Field(value: &self.powermonEnables) }() default: break } } @@ -2032,6 +2132,9 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.deviceBatteryInaAddress != 0 { try visitor.visitSingularUInt32Field(value: self.deviceBatteryInaAddress, fieldNumber: 9) } + if self.powermonEnables != 0 { + try visitor.visitSingularUInt64Field(value: self.powermonEnables, fieldNumber: 32) + } try unknownFields.traverse(visitor: &visitor) } @@ -2044,6 +2147,7 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if lhs.lsSecs != rhs.lsSecs {return false} if lhs.minWakeSecs != rhs.minWakeSecs {return false} if lhs.deviceBatteryInaAddress != rhs.deviceBatteryInaAddress {return false} + if lhs.powermonEnables != rhs.powermonEnables {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2197,6 +2301,7 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp 8: .same(proto: "displaymode"), 9: .standard(proto: "heading_bold"), 10: .standard(proto: "wake_on_tap_or_motion"), + 11: .standard(proto: "compass_orientation"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2215,6 +2320,7 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp case 8: try { try decoder.decodeSingularEnumField(value: &self.displaymode) }() case 9: try { try decoder.decodeSingularBoolField(value: &self.headingBold) }() case 10: try { try decoder.decodeSingularBoolField(value: &self.wakeOnTapOrMotion) }() + case 11: try { try decoder.decodeSingularEnumField(value: &self.compassOrientation) }() default: break } } @@ -2251,6 +2357,9 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if self.wakeOnTapOrMotion != false { try visitor.visitSingularBoolField(value: self.wakeOnTapOrMotion, fieldNumber: 10) } + if self.compassOrientation != .degrees0 { + try visitor.visitSingularEnumField(value: self.compassOrientation, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -2265,6 +2374,7 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if lhs.displaymode != rhs.displaymode {return false} if lhs.headingBold != rhs.headingBold {return false} if lhs.wakeOnTapOrMotion != rhs.wakeOnTapOrMotion {return false} + if lhs.compassOrientation != rhs.compassOrientation {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2306,6 +2416,19 @@ extension Config.DisplayConfig.DisplayMode: SwiftProtobuf._ProtoNameProviding { ] } +extension Config.DisplayConfig.CompassOrientation: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "DEGREES_0"), + 1: .same(proto: "DEGREES_90"), + 2: .same(proto: "DEGREES_180"), + 3: .same(proto: "DEGREES_270"), + 4: .same(proto: "DEGREES_0_INVERTED"), + 5: .same(proto: "DEGREES_90_INVERTED"), + 6: .same(proto: "DEGREES_180_INVERTED"), + 7: .same(proto: "DEGREES_270_INVERTED"), + ] +} + extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = Config.protoMessageName + ".LoRaConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -2471,6 +2594,7 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI 1: .same(proto: "enabled"), 2: .same(proto: "mode"), 3: .standard(proto: "fixed_pin"), + 4: .standard(proto: "device_logging_enabled"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2482,6 +2606,7 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }() case 2: try { try decoder.decodeSingularEnumField(value: &self.mode) }() case 3: try { try decoder.decodeSingularUInt32Field(value: &self.fixedPin) }() + case 4: try { try decoder.decodeSingularBoolField(value: &self.deviceLoggingEnabled) }() default: break } } @@ -2497,6 +2622,9 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI if self.fixedPin != 0 { try visitor.visitSingularUInt32Field(value: self.fixedPin, fieldNumber: 3) } + if self.deviceLoggingEnabled != false { + try visitor.visitSingularBoolField(value: self.deviceLoggingEnabled, fieldNumber: 4) + } try unknownFields.traverse(visitor: &visitor) } @@ -2504,6 +2632,7 @@ extension Config.BluetoothConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageI if lhs.enabled != rhs.enabled {return false} if lhs.mode != rhs.mode {return false} if lhs.fixedPin != rhs.fixedPin {return false} + if lhs.deviceLoggingEnabled != rhs.deviceLoggingEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 7d33c743..a8ee777c 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -114,6 +114,18 @@ enum HardwareModel: SwiftProtobuf.Enum { /// wiphone https://www.wiphone.io/ case wiphone // = 20 + /// + /// WIO Tracker WM1110 family from Seeed Studio. Includes wio-1110-tracker and wio-1110-sdk + case wioWm1110 // = 21 + + /// + /// RAK2560 Solar base station based on RAK4630 + case rak2560 // = 22 + + /// + /// Heltec HRU-3601: https://heltec.org/project/hru-3601/ + case heltecHru3601 // = 23 + /// /// B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station case stationG1 // = 25 @@ -288,6 +300,10 @@ enum HardwareModel: SwiftProtobuf.Enum { /// ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS case radiomaster900BanditNano // = 64 + /// + /// Heltec Capsule Sensor V3 with ESP32-S3 CPU, Portable LoRa device that can replace GNSS modules or sensors + case heltecCapsuleSensorV3 // = 65 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -322,6 +338,9 @@ enum HardwareModel: SwiftProtobuf.Enum { case 18: self = .nanoG2Ultra case 19: self = .loraType case 20: self = .wiphone + case 21: self = .wioWm1110 + case 22: self = .rak2560 + case 23: self = .heltecHru3601 case 25: self = .stationG1 case 26: self = .rak11310 case 27: self = .senseloraRp2040 @@ -362,6 +381,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 62: self = .twcMeshV4 case 63: self = .nrf52PromicroDiy case 64: self = .radiomaster900BanditNano + case 65: self = .heltecCapsuleSensorV3 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -390,6 +410,9 @@ enum HardwareModel: SwiftProtobuf.Enum { case .nanoG2Ultra: return 18 case .loraType: return 19 case .wiphone: return 20 + case .wioWm1110: return 21 + case .rak2560: return 22 + case .heltecHru3601: return 23 case .stationG1: return 25 case .rak11310: return 26 case .senseloraRp2040: return 27 @@ -430,6 +453,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .twcMeshV4: return 62 case .nrf52PromicroDiy: return 63 case .radiomaster900BanditNano: return 64 + case .heltecCapsuleSensorV3: return 65 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -463,6 +487,9 @@ extension HardwareModel: CaseIterable { .nanoG2Ultra, .loraType, .wiphone, + .wioWm1110, + .rak2560, + .heltecHru3601, .stationG1, .rak11310, .senseloraRp2040, @@ -503,6 +530,7 @@ extension HardwareModel: CaseIterable { .twcMeshV4, .nrf52PromicroDiy, .radiomaster900BanditNano, + .heltecCapsuleSensorV3, .privateHw, ] } @@ -2701,6 +2729,129 @@ struct NodeRemoteHardwarePin { fileprivate var _pin: RemoteHardwarePin? = nil } +struct ChunkedPayload { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// The ID of the entire payload + var payloadID: UInt32 = 0 + + /// + /// The total number of chunks in the payload + var chunkCount: UInt32 = 0 + + /// + /// The current chunk index in the total + var chunkIndex: UInt32 = 0 + + /// + /// The binary data of the current chunk + var payloadChunk: Data = Data() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// +/// Wrapper message for broken repeated oneof support +struct resend_chunks { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var chunks: [UInt32] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// +/// Responses to a ChunkedPayload request +struct ChunkedPayloadResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// The ID of the entire payload + var payloadID: UInt32 = 0 + + var payloadVariant: ChunkedPayloadResponse.OneOf_PayloadVariant? = nil + + /// + /// Request to transfer chunked payload + var requestTransfer: Bool { + get { + if case .requestTransfer(let v)? = payloadVariant {return v} + return false + } + set {payloadVariant = .requestTransfer(newValue)} + } + + /// + /// Accept the transfer chunked payload + var acceptTransfer: Bool { + get { + if case .acceptTransfer(let v)? = payloadVariant {return v} + return false + } + set {payloadVariant = .acceptTransfer(newValue)} + } + + /// + /// Request missing indexes in the chunked payload + var resendChunks: resend_chunks { + get { + if case .resendChunks(let v)? = payloadVariant {return v} + return resend_chunks() + } + set {payloadVariant = .resendChunks(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_PayloadVariant: Equatable { + /// + /// Request to transfer chunked payload + case requestTransfer(Bool) + /// + /// Accept the transfer chunked payload + case acceptTransfer(Bool) + /// + /// Request missing indexes in the chunked payload + case resendChunks(resend_chunks) + + #if !swift(>=4.1) + static func ==(lhs: ChunkedPayloadResponse.OneOf_PayloadVariant, rhs: ChunkedPayloadResponse.OneOf_PayloadVariant) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.requestTransfer, .requestTransfer): return { + guard case .requestTransfer(let l) = lhs, case .requestTransfer(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.acceptTransfer, .acceptTransfer): return { + guard case .acceptTransfer(let l) = lhs, case .acceptTransfer(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.resendChunks, .resendChunks): return { + guard case .resendChunks(let l) = lhs, case .resendChunks(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) extension HardwareModel: @unchecked Sendable {} extension Constants: @unchecked Sendable {} @@ -2736,6 +2887,10 @@ extension Neighbor: @unchecked Sendable {} extension DeviceMetadata: @unchecked Sendable {} extension Heartbeat: @unchecked Sendable {} extension NodeRemoteHardwarePin: @unchecked Sendable {} +extension ChunkedPayload: @unchecked Sendable {} +extension resend_chunks: @unchecked Sendable {} +extension ChunkedPayloadResponse: @unchecked Sendable {} +extension ChunkedPayloadResponse.OneOf_PayloadVariant: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -2765,6 +2920,9 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 18: .same(proto: "NANO_G2_ULTRA"), 19: .same(proto: "LORA_TYPE"), 20: .same(proto: "WIPHONE"), + 21: .same(proto: "WIO_WM1110"), + 22: .same(proto: "RAK2560"), + 23: .same(proto: "HELTEC_HRU_3601"), 25: .same(proto: "STATION_G1"), 26: .same(proto: "RAK11310"), 27: .same(proto: "SENSELORA_RP2040"), @@ -2805,6 +2963,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 62: .same(proto: "TWC_MESH_V4"), 63: .same(proto: "NRF52_PROMICRO_DIY"), 64: .same(proto: "RADIOMASTER_900_BANDIT_NANO"), + 65: .same(proto: "HELTEC_CAPSULE_SENSOR_V3"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -4775,3 +4934,169 @@ extension NodeRemoteHardwarePin: SwiftProtobuf.Message, SwiftProtobuf._MessageIm return true } } + +extension ChunkedPayload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ChunkedPayload" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "payload_id"), + 2: .standard(proto: "chunk_count"), + 3: .standard(proto: "chunk_index"), + 4: .standard(proto: "payload_chunk"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.payloadID) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.chunkCount) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.chunkIndex) }() + case 4: try { try decoder.decodeSingularBytesField(value: &self.payloadChunk) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.payloadID != 0 { + try visitor.visitSingularUInt32Field(value: self.payloadID, fieldNumber: 1) + } + if self.chunkCount != 0 { + try visitor.visitSingularUInt32Field(value: self.chunkCount, fieldNumber: 2) + } + if self.chunkIndex != 0 { + try visitor.visitSingularUInt32Field(value: self.chunkIndex, fieldNumber: 3) + } + if !self.payloadChunk.isEmpty { + try visitor.visitSingularBytesField(value: self.payloadChunk, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ChunkedPayload, rhs: ChunkedPayload) -> Bool { + if lhs.payloadID != rhs.payloadID {return false} + if lhs.chunkCount != rhs.chunkCount {return false} + if lhs.chunkIndex != rhs.chunkIndex {return false} + if lhs.payloadChunk != rhs.payloadChunk {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension resend_chunks: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".resend_chunks" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "chunks"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedUInt32Field(value: &self.chunks) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.chunks.isEmpty { + try visitor.visitPackedUInt32Field(value: self.chunks, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: resend_chunks, rhs: resend_chunks) -> Bool { + if lhs.chunks != rhs.chunks {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension ChunkedPayloadResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ChunkedPayloadResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "payload_id"), + 2: .standard(proto: "request_transfer"), + 3: .standard(proto: "accept_transfer"), + 4: .standard(proto: "resend_chunks"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.payloadID) }() + case 2: try { + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .requestTransfer(v) + } + }() + case 3: try { + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .acceptTransfer(v) + } + }() + case 4: try { + var v: resend_chunks? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .resendChunks(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .resendChunks(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.payloadID != 0 { + try visitor.visitSingularUInt32Field(value: self.payloadID, fieldNumber: 1) + } + switch self.payloadVariant { + case .requestTransfer?: try { + guard case .requestTransfer(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularBoolField(value: v, fieldNumber: 2) + }() + case .acceptTransfer?: try { + guard case .acceptTransfer(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularBoolField(value: v, fieldNumber: 3) + }() + case .resendChunks?: try { + guard case .resendChunks(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ChunkedPayloadResponse, rhs: ChunkedPayloadResponse) -> Bool { + if lhs.payloadID != rhs.payloadID {return false} + if lhs.payloadVariant != rhs.payloadVariant {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Meshtastic/Protobufs/meshtastic/powermon.pb.swift b/Meshtastic/Protobufs/meshtastic/powermon.pb.swift new file mode 100644 index 00000000..c6762ef1 --- /dev/null +++ b/Meshtastic/Protobufs/meshtastic/powermon.pb.swift @@ -0,0 +1,179 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: meshtastic/powermon.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). +///But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) +struct PowerMon { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Any significant power changing event in meshtastic should be tagged with a powermon state transition. + ///If you are making new meshtastic features feel free to add new entries at the end of this definition. + enum State: SwiftProtobuf.Enum { + typealias RawValue = Int + case none // = 0 + case cpuDeepSleep // = 1 + case cpuLightSleep // = 2 + + /// + ///The external Vext1 power is on. Many boards have auxillary power rails that the CPU turns on only + ///occasionally. In cases where that rail has multiple devices on it we usually want to have logging on + ///the state of that rail as an independent record. + ///For instance on the Heltec Tracker 1.1 board, this rail is the power source for the GPS and screen. + /// + ///The log messages will be short and complete (see PowerMon.Event in the protobufs for details). + ///something like "S:PM:C,0x00001234,REASON" where the hex number is the bitmask of all current states. + ///(We use a bitmask for states so that if a log message gets lost it won't be fatal) + case vext1On // = 4 + case loraRxon // = 8 + case loraTxon // = 16 + case loraRxactive // = 32 + case btOn // = 64 + case ledOn // = 128 + case screenOn // = 256 + case screenDrawing // = 512 + case wifiOn // = 1024 + + /// + ///GPS is actively trying to find our location + ///See GPSPowerState for more details + case gpsActive // = 2048 + case UNRECOGNIZED(Int) + + init() { + self = .none + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .none + case 1: self = .cpuDeepSleep + case 2: self = .cpuLightSleep + case 4: self = .vext1On + case 8: self = .loraRxon + case 16: self = .loraTxon + case 32: self = .loraRxactive + case 64: self = .btOn + case 128: self = .ledOn + case 256: self = .screenOn + case 512: self = .screenDrawing + case 1024: self = .wifiOn + case 2048: self = .gpsActive + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .none: return 0 + case .cpuDeepSleep: return 1 + case .cpuLightSleep: return 2 + case .vext1On: return 4 + case .loraRxon: return 8 + case .loraTxon: return 16 + case .loraRxactive: return 32 + case .btOn: return 64 + case .ledOn: return 128 + case .screenOn: return 256 + case .screenDrawing: return 512 + case .wifiOn: return 1024 + case .gpsActive: return 2048 + case .UNRECOGNIZED(let i): return i + } + } + + } + + init() {} +} + +#if swift(>=4.2) + +extension PowerMon.State: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [PowerMon.State] = [ + .none, + .cpuDeepSleep, + .cpuLightSleep, + .vext1On, + .loraRxon, + .loraTxon, + .loraRxactive, + .btOn, + .ledOn, + .screenOn, + .screenDrawing, + .wifiOn, + .gpsActive, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension PowerMon: @unchecked Sendable {} +extension PowerMon.State: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "meshtastic" + +extension PowerMon: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PowerMon" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: PowerMon, rhs: PowerMon) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension PowerMon.State: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "None"), + 1: .same(proto: "CPU_DeepSleep"), + 2: .same(proto: "CPU_LightSleep"), + 4: .same(proto: "Vext1_On"), + 8: .same(proto: "Lora_RXOn"), + 16: .same(proto: "Lora_TXOn"), + 32: .same(proto: "Lora_RXActive"), + 64: .same(proto: "BT_On"), + 128: .same(proto: "LED_On"), + 256: .same(proto: "Screen_On"), + 512: .same(proto: "Screen_Drawing"), + 1024: .same(proto: "Wifi_On"), + 2048: .same(proto: "GPS_Active"), + ] +} diff --git a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift index 7ddb75dc..f6116cf9 100644 --- a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift @@ -120,6 +120,14 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// AHT10 Integrated temperature and humidity sensor case aht10 // = 23 + + /// + /// DFRobot Lark Weather station (temperature, humidity, pressure, wind speed and direction) + case dfrobotLark // = 24 + + /// + /// NAU7802 Scale Chip or compatible + case nau7802 // = 25 case UNRECOGNIZED(Int) init() { @@ -152,6 +160,8 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { case 21: self = .ltr390Uv case 22: self = .tsl25911Fn case 23: self = .aht10 + case 24: self = .dfrobotLark + case 25: self = .nau7802 default: self = .UNRECOGNIZED(rawValue) } } @@ -182,6 +192,8 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { case .ltr390Uv: return 21 case .tsl25911Fn: return 22 case .aht10: return 23 + case .dfrobotLark: return 24 + case .nau7802: return 25 case .UNRECOGNIZED(let i): return i } } @@ -217,6 +229,8 @@ extension TelemetrySensorType: CaseIterable { .ltr390Uv, .tsl25911Fn, .aht10, + .dfrobotLark, + .nau7802, ] } @@ -302,6 +316,27 @@ struct EnvironmentMetrics { /// VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor. var whiteLux: Float = 0 + /// + /// Infrared lux + var irLux: Float = 0 + + /// + /// Ultraviolet lux + var uvLux: Float = 0 + + /// + /// Wind direction in degrees + /// 0 degrees = North, 90 = East, etc... + var windDirection: UInt32 = 0 + + /// + /// Wind speed in m/s + var windSpeed: Float = 0 + + /// + /// Weight in KG + var weight: Float = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -503,6 +538,26 @@ struct Telemetry { init() {} } +/// +/// NAU7802 Telemetry configuration, for saving to flash +struct Nau7802Config { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// The offset setting for the NAU7802 + var zeroOffset: Int32 = 0 + + /// + /// The calibration factor for the NAU7802 + var calibrationFactor: Float = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) extension TelemetrySensorType: @unchecked Sendable {} extension DeviceMetrics: @unchecked Sendable {} @@ -511,6 +566,7 @@ extension PowerMetrics: @unchecked Sendable {} extension AirQualityMetrics: @unchecked Sendable {} extension Telemetry: @unchecked Sendable {} extension Telemetry.OneOf_Variant: @unchecked Sendable {} +extension Nau7802Config: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -543,6 +599,8 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 21: .same(proto: "LTR390UV"), 22: .same(proto: "TSL25911FN"), 23: .same(proto: "AHT10"), + 24: .same(proto: "DFROBOT_LARK"), + 25: .same(proto: "NAU7802"), ] } @@ -615,6 +673,11 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 8: .same(proto: "distance"), 9: .same(proto: "lux"), 10: .standard(proto: "white_lux"), + 11: .standard(proto: "ir_lux"), + 12: .standard(proto: "uv_lux"), + 13: .standard(proto: "wind_direction"), + 14: .standard(proto: "wind_speed"), + 15: .same(proto: "weight"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -633,6 +696,11 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple case 8: try { try decoder.decodeSingularFloatField(value: &self.distance) }() case 9: try { try decoder.decodeSingularFloatField(value: &self.lux) }() case 10: try { try decoder.decodeSingularFloatField(value: &self.whiteLux) }() + case 11: try { try decoder.decodeSingularFloatField(value: &self.irLux) }() + case 12: try { try decoder.decodeSingularFloatField(value: &self.uvLux) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &self.windDirection) }() + case 14: try { try decoder.decodeSingularFloatField(value: &self.windSpeed) }() + case 15: try { try decoder.decodeSingularFloatField(value: &self.weight) }() default: break } } @@ -669,6 +737,21 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.whiteLux != 0 { try visitor.visitSingularFloatField(value: self.whiteLux, fieldNumber: 10) } + if self.irLux != 0 { + try visitor.visitSingularFloatField(value: self.irLux, fieldNumber: 11) + } + if self.uvLux != 0 { + try visitor.visitSingularFloatField(value: self.uvLux, fieldNumber: 12) + } + if self.windDirection != 0 { + try visitor.visitSingularUInt32Field(value: self.windDirection, fieldNumber: 13) + } + if self.windSpeed != 0 { + try visitor.visitSingularFloatField(value: self.windSpeed, fieldNumber: 14) + } + if self.weight != 0 { + try visitor.visitSingularFloatField(value: self.weight, fieldNumber: 15) + } try unknownFields.traverse(visitor: &visitor) } @@ -683,6 +766,11 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if lhs.distance != rhs.distance {return false} if lhs.lux != rhs.lux {return false} if lhs.whiteLux != rhs.whiteLux {return false} + if lhs.irLux != rhs.irLux {return false} + if lhs.uvLux != rhs.uvLux {return false} + if lhs.windDirection != rhs.windDirection {return false} + if lhs.windSpeed != rhs.windSpeed {return false} + if lhs.weight != rhs.weight {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -959,3 +1047,41 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation return true } } + +extension Nau7802Config: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Nau7802Config" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "zeroOffset"), + 2: .same(proto: "calibrationFactor"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.zeroOffset) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self.calibrationFactor) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.zeroOffset != 0 { + try visitor.visitSingularInt32Field(value: self.zeroOffset, fieldNumber: 1) + } + if self.calibrationFactor != 0 { + try visitor.visitSingularFloatField(value: self.calibrationFactor, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Nau7802Config, rhs: Nau7802Config) -> Bool { + if lhs.zeroOffset != rhs.zeroOffset {return false} + if lhs.calibrationFactor != rhs.calibrationFactor {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Meshtastic/Views/Settings/AdminMessageList.swift b/Meshtastic/Views/Settings/AdminMessageList.swift deleted file mode 100644 index fc4fa5f8..00000000 --- a/Meshtastic/Views/Settings/AdminMessageList.swift +++ /dev/null @@ -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 - } - } - } -} diff --git a/Meshtastic/Views/Settings/AppLog.swift b/Meshtastic/Views/Settings/AppLog.swift new file mode 100644 index 00000000..45652edf --- /dev/null +++ b/Meshtastic/Views/Settings/AppLog.swift @@ -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 { } diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 0b71dcb3..980ed8f4 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -5,8 +5,8 @@ // Copyright (c) Garth Vander Houwen 8/18/22. // -import SwiftUI import OSLog +import SwiftUI struct BluetoothConfig: View { @Environment(\.managedObjectContext) var context @@ -18,6 +18,7 @@ struct BluetoothConfig: View { @State var mode = 0 @State var fixedPin = "123456" @State var shortPin = false + @State var deviceLoggingEnabled = false var pinLength: Int = 6 let numberFormatter: NumberFormatter = { let formatter = NumberFormatter() @@ -68,18 +69,24 @@ struct BluetoothConfig: View { .foregroundColor(.red) } } + + Toggle(isOn: $deviceLoggingEnabled) { + Label("Device Logging Enabled", systemImage: "ladybug") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } .disabled(self.bleManager.connectedPeripheral == nil || node?.bluetoothConfig == nil) SaveConfigButton(node: node, hasChanges: $hasChanges) { - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if connectedNode != nil { + if let myNodeNum = bleManager.connectedPeripheral?.num, + let connectedNode = getNodeInfo(id: myNodeNum, context: context) { var bc = Config.BluetoothConfig() bc.enabled = enabled bc.mode = BluetoothModes(rawValue: mode)?.protoEnumValue() ?? Config.BluetoothConfig.PairingMode.randomPin bc.fixedPin = UInt32(fixedPin) ?? 123456 - let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + bc.deviceLoggingEnabled = deviceLoggingEnabled + let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -90,21 +97,26 @@ struct BluetoothConfig: View { } .navigationTitle("bluetooth.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { if self.bleManager.context == nil { self.bleManager.context = context } setBluetoothValues() // Need to request a BluetoothConfig from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.bluetoothConfig == nil { + if let connectedPeripheral = bleManager.connectedPeripheral, let node, node.bluetoothConfig == nil { Logger.mesh.info("empty bluetooth config") - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) - if node != nil && connectedNode != nil { - _ = bleManager.requestBluetoothConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context) + if let connectedNode { + _ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0) } } } @@ -123,11 +135,17 @@ struct BluetoothConfig: View { if newFixedPin != String(node!.bluetoothConfig!.fixedPin) { hasChanges = true } } } + .onChange(of: deviceLoggingEnabled) { newDeviceLogging in + if node != nil && node!.bluetoothConfig != nil { + if newDeviceLogging != node!.bluetoothConfig!.deviceLoggingEnabled { hasChanges = true } + } + } } func setBluetoothValues() { self.enabled = node?.bluetoothConfig?.enabled ?? true self.mode = Int(node?.bluetoothConfig?.mode ?? 0) self.fixedPin = String(node?.bluetoothConfig?.fixedPin ?? 123456) + self.deviceLoggingEnabled = node?.bluetoothConfig?.deviceLoggingEnabled ?? false self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Logs/AppLogFilter.swift b/Meshtastic/Views/Settings/Logs/AppLogFilter.swift new file mode 100644 index 00000000..39e5e113 --- /dev/null +++ b/Meshtastic/Views/Settings/Logs/AppLogFilter.swift @@ -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) + } +} diff --git a/Meshtastic/Views/Settings/Logs/LogDetail.swift b/Meshtastic/Views/Settings/Logs/LogDetail.swift new file mode 100644 index 00000000..7308da5b --- /dev/null +++ b/Meshtastic/Views/Settings/Logs/LogDetail.swift @@ -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) + } +} diff --git a/Meshtastic/Views/Settings/MeshLog.swift b/Meshtastic/Views/Settings/MeshLog.swift index 10575098..beb5153a 100644 --- a/Meshtastic/Views/Settings/MeshLog.swift +++ b/Meshtastic/Views/Settings/MeshLog.swift @@ -41,6 +41,7 @@ struct MeshLog: View { // Stop adding logs when an error is thrown } } + .listStyle(.plain) .fileExporter( isPresented: $isExporting, document: document, diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index f2a38030..2817e8b6 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -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 { diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 511ecdaa..83fe06d3 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -173,6 +173,12 @@ "interval.seventytwo.hours"="Seventy Two Hours"; "keyboard.type"="Keyboard Typ"; "logging"="Logging"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa Einstellungen"; "map"="Mesh Karte"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 4c507879..fb3af6f6 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -177,6 +177,12 @@ "interval.seventytwo.hours"="Seventy Two Hours"; "keyboard.type"="Keyboard Type"; "logging"="Logging"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa Config"; "map"="Mesh Map"; diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 785a2012..3c517ac3 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -154,6 +154,12 @@ "interval.seventytwo.hours"="Soixante douze heures"; "keyboard.type"="Type de clavier"; "logging"="Enregistrement"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="Configuration LoRa"; "map"="Carte de maillage"; diff --git a/he.lproj/Localizable.strings b/he.lproj/Localizable.strings index 154c2391..fd844980 100644 --- a/he.lproj/Localizable.strings +++ b/he.lproj/Localizable.strings @@ -177,6 +177,12 @@ "interval.seventytwo.hours"="ืฉื‘ืขื™ื ื•ืฉืชื™ื™ื ืฉืขื•ืช"; "keyboard.type"="ืกื•ื’ ืžืงืœื“ืช"; "logging"="ืจื™ืฉื•ื"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="ืœื•ืจื”"; "lora.config"="ื”ื’ื“ืจื•ืช ืœื•ืจื”"; "map"="ืžืคืช ืžืฉ"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index eb8372b0..e09d745b 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -175,6 +175,12 @@ "interval.seventytwo.hours"="Siedemdziesiฤ…t Dwie Godziny"; "keyboard.type"="Typ Klawiatury"; "logging"="Rejestracja"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="Konfiguracja LoRa"; "map"="Mapa Sieci"; diff --git a/protobufs b/protobufs index 1c3029f2..4da558d0 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 1c3029f2868e5fc49809fd378f6c0c66aee0eaf4 +Subproject commit 4da558d0f73c46ef91b74431facee73c09affbfc diff --git a/pt-PT.lproj/Localizable.strings b/pt-PT.lproj/Localizable.strings index 4c507879..fb3af6f6 100644 --- a/pt-PT.lproj/Localizable.strings +++ b/pt-PT.lproj/Localizable.strings @@ -177,6 +177,12 @@ "interval.seventytwo.hours"="Seventy Two Hours"; "keyboard.type"="Keyboard Type"; "logging"="Logging"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa Config"; "map"="Mesh Map"; diff --git a/se.lproj/Localizable.strings b/se.lproj/Localizable.strings index e45ca2fe..81d00170 100644 --- a/se.lproj/Localizable.strings +++ b/se.lproj/Localizable.strings @@ -177,6 +177,12 @@ "interval.seventytwo.hours"="Sjuttiotvรฅ Timmar"; "keyboard.type"="Tangentbordstyp"; "logging"="Loggning"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa Konfiguration"; "map"="Mesh Karta"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 5606f44b..3a241f3f 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -173,6 +173,12 @@ "interval.eventytwo.hours"="ไธƒๅไบŒๅฐๆ—ถ"; "keyboard.type"="้”ฎ็›˜็ฑปๅž‹"; "logging"="ๅŠ ่ฝฝไธญ"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa ้…็ฝฎ"; "map"="Mesh ๅœฐๅ›พ"; diff --git a/zh-Hant-TW.lproj/Localizable.strings b/zh-Hant-TW.lproj/Localizable.strings index 1665f916..fdc02df2 100644 --- a/zh-Hant-TW.lproj/Localizable.strings +++ b/zh-Hant-TW.lproj/Localizable.strings @@ -173,6 +173,12 @@ "interval.eventytwo.hours"="ไธƒๅไบŒๅฐๆ™‚"; "keyboard.type"="้ต็›ค้กžๅž‹"; "logging"="ๅŠ ่ผ‰ไธญ"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa ่จญๅฎš"; "map"="Mesh ๅœฐๅœ–";