Merge pull request #715 from meshtastic/Logging_View

OS Log Logging view
This commit is contained in:
Garth Vander Houwen 2024-06-23 12:33:34 -07:00 committed by GitHub
commit bf7d57a1eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 2239 additions and 245 deletions

View file

@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
25183D462C0A6D97001E31D5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25183D452C0A6D97001E31D5 /* Logger.swift */; };
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; };
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; };
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; };
@ -33,7 +32,6 @@
DD0E20FD2B87090400F2D100 /* clientonly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0E20FA2B87090400F2D100 /* clientonly.pb.swift */; };
DD0E20FE2B87090400F2D100 /* paxcount.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0E20FB2B87090400F2D100 /* paxcount.pb.swift */; };
DD0E21012B8A6F1300F2D100 /* DeviceHardware.json in Resources */ = {isa = PBXBuildFile; fileRef = DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */; };
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */; };
DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */; };
DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */; };
DD15E4F52B8BFC8E00654F61 /* PaxCounterLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD15E4F42B8BFC8E00654F61 /* PaxCounterLog.swift */; };
@ -168,6 +166,10 @@
DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DDCDC6CD29481FCC004C1DDA /* Localizable.strings */; };
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */; };
DDD43FE32A78C8900083A3E9 /* MqttClientProxyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */; };
DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB082C285DDC007E03CA /* AppLog.swift */; };
DDD5BB0B2C285E45007E03CA /* LogDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0A2C285E45007E03CA /* LogDetail.swift */; };
DDD5BB0D2C285F00007E03CA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0C2C285F00007E03CA /* Logger.swift */; };
DDD5BB102C285FB3007E03CA /* AppLogFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0F2C285FB3007E03CA /* AppLogFilter.swift */; };
DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6EEAE29BC024700383354 /* Firmware.swift */; };
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */; };
DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */; };
@ -245,7 +247,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
25183D452C0A6D97001E31D5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = "<group>"; };
@ -274,7 +275,6 @@
DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 28.xcdatamodel"; sourceTree = "<group>"; };
DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DeviceHardware.json; sourceTree = "<group>"; };
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = "<group>"; };
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = "<group>"; };
DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionPopover.swift; sourceTree = "<group>"; };
DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV15.xcdatamodel; sourceTree = "<group>"; };
DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaxCounterConfig.swift; sourceTree = "<group>"; };
@ -443,6 +443,11 @@
DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 37.xcdatamodel"; sourceTree = "<group>"; };
DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = "<group>"; };
DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClientProxyManager.swift; sourceTree = "<group>"; };
DDD5BB082C285DDC007E03CA /* AppLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLog.swift; sourceTree = "<group>"; };
DDD5BB0A2C285E45007E03CA /* LogDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDetail.swift; sourceTree = "<group>"; };
DDD5BB0C2C285F00007E03CA /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
DDD5BB0F2C285FB3007E03CA /* AppLogFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogFilter.swift; sourceTree = "<group>"; };
DDD5BB142C28680D007E03CA /* MeshtasticDataModelV 38.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 38.xcdatamodel"; sourceTree = "<group>"; };
DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = "<group>"; };
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = "<group>"; };
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = "<group>"; };
@ -606,9 +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 = "<group>";
@ -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 = "<group>";
@ -947,7 +953,6 @@
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */,
DDDB443C29F6592F00EE2349 /* NetworkManager.swift */,
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */,
25183D452C0A6D97001E31D5 /* Logger.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -970,6 +975,15 @@
path = Mqtt;
sourceTree = "<group>";
};
DDD5BB0E2C285F92007E03CA /* Logs */ = {
isa = PBXGroup;
children = (
DDD5BB0A2C285E45007E03CA /* LogDetail.swift */,
DDD5BB0F2C285FB3007E03CA /* AppLogFilter.swift */,
);
path = Logs;
sourceTree = "<group>";
};
DDDB26402AABEF7B003AFCB7 /* Helpers */ = {
isa = PBXGroup;
children = (
@ -1003,6 +1017,7 @@
DD1933772B084F4200771CD5 /* Measurement.swift */,
DDFFA7462B3A7F3C004730DB /* Bundle.swift */,
DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */,
DDD5BB0C2C285F00007E03CA /* Logger.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -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 = "<group>";

View file

@ -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
}

View file

@ -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)

View file

@ -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]()
}

View file

@ -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)
}
}

View file

@ -0,0 +1,75 @@
//
// Logger.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 6/3/24.
//
import OSLog
extension Logger {
/// The logger's subsystem.
private static var subsystem = Bundle.main.bundleIdentifier!
/// All admin messages
static let admin = Logger(subsystem: subsystem, category: "🏛 Admin")
/// All logs related to data such as decoding error, parsing issues, etc.
static let data = Logger(subsystem: subsystem, category: "🗄️ Data")
/// All logs related to the mesh
static let mesh = Logger(subsystem: subsystem, category: "🕸️ Mesh")
/// All logs related to MQTT
static let mqtt = Logger(subsystem: subsystem, category: "📱 MQTT")
/// All detailed logs originating from the device (radio).
static let radio = Logger(subsystem: subsystem, category: "📟 Radio")
/// All logs related to services such as network calls, location, etc.
static let services = Logger(subsystem: subsystem, category: "🍏 Services")
/// All logs related to tracking and analytics.
static let statistics = Logger(subsystem: subsystem, category: "📊 Stats")
/// Fetch from the logstore
static public func fetch(predicateFormat: String) async throws -> [OSLogEntryLog] {
let store = try OSLogStore(scope: .currentProcessIdentifier)
let position = store.position(timeIntervalSinceLatestBoot: 0)
//let calendar = Calendar.current
//let dayAgo = calendar.date(byAdding: .day, value: -1, to: Date.now)
//let position = store.position(date: dayAgo!)
let predicate = NSPredicate(format: predicateFormat)
let entries = try store.getEntries(at: position, matching: predicate)
var logs: [OSLogEntryLog] = []
for entry in entries {
try Task.checkCancellation()
if let log = entry as? OSLogEntryLog {
logs.append(log)
}
}
if logs.isEmpty { logs = [] }
return logs
}
}
extension OSLogEntryLog.Level {
var description: String {
switch self {
case .undefined: "undefined"
case .debug: "🩺 Debug"
case .info: " Info"
case .notice: "⚠️ Notice"
case .error: "🚨 Error"
case .fault: "💥 Fault"
@unknown default: "default"
}
}
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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)")
}
}

View file

@ -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")
}
}

View file

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>MeshtasticDataModelV 37.xcdatamodel</string>
<string>MeshtasticDataModelV 38.xcdatamodel</string>
</dict>
</plist>

View file

@ -0,0 +1,465 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="green" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ledState" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="red" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="ambientLightingConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="ambientLightingConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="deviceLoggingEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
<attribute name="downlinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="positionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
<attribute name="psk" optional="YES" attributeType="Binary"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="uplinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
<fetchedProperty name="allPrivateMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="channel == $FETCH_SOURCE.index &amp;&amp; toUser == nil AND isEmoji == false"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="index"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="detectionTriggeredHigh" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="disableTripleClick" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="ledHeartbeatEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="nodeInfoBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tzdef" optional="YES" attributeType="String"/>
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hwModel" optional="YES" attributeType="String"/>
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="units" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="useI2SAsBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="fetchedProperty" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="ExternalNotificationConfigEntity"/>
</fetchedProperty>
</entity>
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
</entity>
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ignoreMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="adminDescription" optional="YES" attributeType="String"/>
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="read" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="receivedTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
<fetchedProperty name="tapbacks" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="messageId"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="mapPositionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="13" usesScalarValueType="YES"/>
<attribute name="mapPublishIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="mapReportingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="allMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="toUser == nil"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="myNodeNum"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ntpServer" optional="YES" attributeType="String"/>
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="favorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="firstHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hopsAway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="viaMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
<relationship name="pax" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PaxCounterEntity" inverseName="paxNode" inverseEntity="PaxCounterEntity"/>
<relationship name="paxCounterConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PaxCounterConfigEntity" inverseName="paxCounterConfigNode" inverseEntity="PaxCounterConfigEntity"/>
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
<relationship name="powerConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PowerConfigEntity" inverseName="powerConfigNode" inverseEntity="PowerConfigEntity"/>
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
<relationship name="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
<relationship name="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="num"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PaxCounterConfigEntity" representedClassName="PaxCounterConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="bleThreshold" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="updateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifiThreshold" optional="YES" attributeType="Integer 32" defaultValueString="-80" usesScalarValueType="YES"/>
<relationship name="paxCounterConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="paxCounterConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PaxCounterEntity" representedClassName="PaxCounterEntity" syncable="YES" codeGenerationType="class">
<attribute name="ble" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uptime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="paxNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="pax" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="gpsEnGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="gpsMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="precisionBits" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PowerConfigEntity" representedClassName="PowerConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="adcMultiplierOverride" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
<attribute name="deviceBatteryInaAddress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isPowerSaving" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="lsSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="minWakeSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="onBatteryShutdownAfterSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="waitBluetoothSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="powerConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="powerConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
<attribute name="color" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="distance" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="elevationGain" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="notes" optional="YES" attributeType="String"/>
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
</entity>
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="overrideConsoleSerialPort" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isRouter" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastHeartbeat" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastRequest" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="powerMeasurementEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="powerScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="powerUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="iaq" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uptimeSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="route" optional="YES" attributeType="Transformable" customClassName="[UInt32]"/>
<attribute name="routeText" optional="YES" attributeType="String"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
</entity>
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
<attribute name="hwModel" attributeType="String"/>
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="longName" attributeType="String"/>
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numString" optional="YES" attributeType="String"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="shortName" attributeType="String"/>
<attribute name="userId" attributeType="String"/>
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="adminMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND isEmoji == false AND admin = true"/>
</fetchedProperty>
<fetchedProperty name="allMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10 "/>
</fetchedProperty>
<fetchedProperty name="detectionSensorMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND portNum = 10"/>
</fetchedProperty>
</entity>
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View file

@ -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<NSFetchRequestResult> = 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)")
}
}

View file

@ -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)

View file

@ -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<D: SwiftProtobuf.Decoder>(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<D: SwiftProtobuf.Decoder>(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<D: SwiftProtobuf.Decoder>(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
}

View file

@ -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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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
}
}

View file

@ -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<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let _ = try decoder.nextFieldNumber() {
}
}
func traverse<V: SwiftProtobuf.Visitor>(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"),
]
}

View file

@ -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<D: SwiftProtobuf.Decoder>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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
}
}

View file

@ -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
}
}
}
}

View file

@ -0,0 +1,227 @@
//
// AppLog.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 6/4/24.
//
import SwiftUI
import OSLog
/// Needed for TableColumnForEach
@available(iOS 17.4, *)
struct AppLog: View {
@State private var logs: [OSLogEntryLog] = []
@State private var sortOrder = [KeyPathComparator(\OSLogEntryLog.date, order: .reverse)]
@State private var selection: OSLogEntry.ID?
@State private var selectedLog: OSLogEntryLog?
@State private var presentingErrorDetails: Bool = false
@State private var searchText = ""
@State private var category: Int = -1
@State private var level: Int = -1
@State var isExporting = false
@State var exportString = ""
@State var isEditingFilters = false
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
private let dateFormatStyle = Date.FormatStyle()
.hour(.twoDigits(amPM: .omitted))
.minute()
.second()
.secondFraction(.fractional(3))
var body: some View {
Table(logs, selection: $selection, sortOrder: $sortOrder) {
if idiom != .phone {
TableColumn("log.time") { value in
Text(value.date.formatted(dateFormatStyle))
}
.width(min: 125, max: 150)
TableColumn("log.category", value: \.category)
.width(min: 125, max: 150)
TableColumn("log.level") { value in
Text(value.level.description)
}
.width(min: 75, max: 100)
}
TableColumn("log.message", value: \.composedMessage) { value in
Text(value.composedMessage)
.font(idiom == .phone ? .caption : .body)
}
.width(ideal: 200, max: .infinity)
}
.monospaced()
.sheet(isPresented: $isEditingFilters) {
AppLogFilter(category: $category, level: $level)
}
.safeAreaInset(edge: .bottom, alignment: .trailing) {
HStack {
Button(action: {
withAnimation {
isEditingFilters = !isEditingFilters
}
}) {
Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill")
.padding(.vertical, 5)
}
.tint(Color(UIColor.secondarySystemBackground))
.foregroundColor(.accentColor)
.buttonStyle(.borderedProminent)
}
.controlSize(.regular)
.padding(5)
}
.padding(.bottom, 5)
.padding(.trailing, 5)
.searchable(text: $searchText, placement: .navigationBarDrawer, prompt: "Search")
.disabled(selection != nil)
.overlay {
if logs.isEmpty {
ContentUnavailableView("No Logs Available", systemImage: "scroll")
}
}
.refreshable {
await logs = searchAppLogs()
logs.sort(using: sortOrder)
}
.onChange(of: sortOrder) { _, sortOrder in
withAnimation {
logs.sort(using: sortOrder)
}
}
.onChange(of: searchText) { _ in
Task {
await logs = searchAppLogs()
logs.sort(using: sortOrder)
}
}
.onChange(of: category) { _ in
Task {
await logs = searchAppLogs()
logs.sort(using: sortOrder)
}
}
.onChange(of: level) { _ in
Task {
await logs = searchAppLogs()
logs.sort(using: sortOrder)
}
}
.onChange(of: selection) { newSelection in
presentingErrorDetails = true
let log = logs.first {
$0.id == newSelection
}
selectedLog = log
}
.sheet(item: $selectedLog, onDismiss: didDismiss) { log in
LogDetail(log: log)
.padding()
}
.task {
logs = await searchAppLogs()
logs.sort(using: sortOrder)
}
.fileExporter(
isPresented: $isExporting,
document: CsvDocument(emptyCsv: exportString),
contentType: .commaSeparatedText,
defaultFilename: String("Meshtastic Application Logs"),
onCompletion: { result in
switch result {
case .success:
self.isExporting = false
Logger.services.info("Application log download succeeded.")
case .failure(let error):
Logger.services.error("Application log download failed: \(error.localizedDescription)")
}
}
)
.navigationBarTitle("Debug Logs\(logs.isEmpty ? "" : " (\(logs.count))")", displayMode: .inline)
.toolbar {
#if targetEnvironment(macCatalyst)
ToolbarItem(placement: .topBarLeading) {
Button(action: {
Task {
await logs = searchAppLogs()
logs.sort(using: sortOrder)
}
}) {
Image(systemName: "arrow.clockwise.circle")
}
}
#endif
if !logs.isEmpty {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
exportString = logToCsvFile(log: logs)
isExporting = true
}) {
Image(systemName: "square.and.arrow.down")
}
}
}
}
}
func didDismiss() {
selection = nil
selectedLog = nil
}
}
@available(iOS 17.4, *)
extension AppLog {
@MainActor
private func searchAppLogs() async -> [OSLogEntryLog] {
do {
/// Case Insensitive Search Text Predicates
let searchPredicates = ["composedMessage", "category", "subsystem", "process"].map { property in
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
}
/// Create a compound predicate using each text search preicate as an OR
let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates)
/// Create an array of predicates to hold our AND predicates
var predicates: [NSPredicate] = []
/// Subsystem Predicate
let subsystemPredicate = NSPredicate(format: "subsystem IN %@", ["com.apple.SwiftUI", "com.apple.coredata", "gvh.MeshtasticClient"])
predicates.append(subsystemPredicate)
/// Category
if category > -1 {
let categoryPredicate = NSPredicate(format: "category == %@", LogCategories(rawValue: category)!.description)
predicates.append(categoryPredicate)
}
/// Log Level
if level > -1 {
let levelPredicate = NSPredicate(format: "messageType == %@", LogLevels(rawValue: level)?.level ?? "info")
predicates.append(levelPredicate)
}
if predicates.count > 0 || !searchText.isEmpty {
if !searchText.isEmpty {
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
let compoundPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates])
let logs = try await Logger.fetch(predicateFormat: compoundPredicate.predicateFormat)
return logs
} else {
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
let logs = try await Logger.fetch(predicateFormat: filterPredicates.predicateFormat)
return logs
}
} else {
let logs = try await Logger.fetch(predicateFormat: subsystemPredicate.predicateFormat)
return logs
}
} catch {
return []
}
}
}
extension OSLogEntry: Identifiable { }

View file

@ -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
}
}

View file

@ -0,0 +1,141 @@
//
// AppLogFilter.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 6/15/24.
//
import Foundation
import OSLog
import SwiftUI
enum LogCategories: Int, CaseIterable, Identifiable {
case admin = 0
case data = 1
case mesh = 2
case mqtt = 3
case radio = 4
case services = 5
case stats = 6
var id: Int { self.rawValue }
var description: String {
switch self {
case .admin:
return "🏛 Admin"
case .data:
return "🗄️ Data"
case .mesh:
return "🕸️ Mesh"
case .mqtt:
return "📱 MQTT"
case .radio:
return "📟 Radio"
case .services:
return "🍏 Services"
case .stats:
return "📊 Stats"
}
}
}
enum LogLevels: Int, CaseIterable, Identifiable {
case debug = 0
case info = 1
case notice = 2
case error = 3
case fault = 4
var id: Int { self.rawValue }
var level: String {
switch self {
case .debug:
return "debug"
case .info:
return "info"
case .notice:
return "notice"
case .error:
return "error"
case .fault:
return "fault"
}
}
var description: String {
switch self {
case .debug:
return "🩺 Debug"
case .info:
return " Info"
case .notice:
return "⚠️ Notice"
case .error:
return "🚨 Error"
case .fault:
return "💥 Fault"
}
}
}
struct AppLogFilter: View {
@Environment(\.dismiss) private var dismiss
/// Filters
var filterTitle = "App Log Filters"
//@Binding
@Binding var category: Int
@Binding var level: Int
var body: some View {
NavigationStack {
Form {
Section(header: Text(filterTitle)) {
HStack {
Label("Category", systemImage: "square.grid.2x2")
Picker("", selection: $category) {
Text("All Categories")
.tag(-1)
ForEach(LogCategories.allCases) { lc in
Text("\(lc.description)")
}
}
.pickerStyle(DefaultPickerStyle())
}
HStack {
Label("Level", systemImage: "stairs")
Picker("", selection: $level) {
Text("All Levels")
.tag(-1)
ForEach(LogLevels.allCases) { ll in
Text("\(ll.description)")
//.tag(ll.rawValue)
}
}
.pickerStyle(DefaultPickerStyle())
}
}
}
#if targetEnvironment(macCatalyst)
Spacer()
Button {
dismiss()
} label: {
Label("close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
#endif
}
.presentationDetents([.fraction(0.6), .fraction(0.75)])
.presentationDragIndicator(.visible)
}
}

View file

@ -0,0 +1,158 @@
//
// LogDetail.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 6/5/24.
//
import SwiftUI
import MapKit
import OSLog
//@available(iOS 17.0, macOS 14.0, *)
struct LogDetail: View {
@Environment(\.dismiss) private var dismiss
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
var log: OSLogEntryLog
var font: Font = .title2
private let dateFormatStyle = Date.FormatStyle()
.day(.defaultDigits)
.month(.defaultDigits)
.year(.twoDigits)
.hour(.twoDigits(amPM: .omitted))
.minute()
.second()
.secondFraction(.fractional(3))
var body: some View {
VStack {
HStack {
Text("OS Log Entry Details")
.font(.largeTitle)
}
Divider()
HStack(alignment: .top) {
VStack(alignment: .leading) {
List {
/// Time
Label {
Text("log.time".localized + ":")
.font(idiom == .phone ? .caption : .title)
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
Text(log.date.formatted(dateFormatStyle))
.font(idiom == .phone ? .caption : .title)
} icon: {
Image(systemName: "timer")
.symbolRenderingMode(.hierarchical)
.font(idiom == .phone ? .callout : .title)
.frame(width: 35)
}
.padding(.bottom, 5)
.listSectionSeparator(.hidden, edges: .top)
.listSectionSeparator(.visible, edges: .bottom)
/// Subsystem
Label {
Text("log.subsystem".localized + ":")
.font(idiom == .phone ? .caption : .title)
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
Text(log.subsystem)
.font(idiom == .phone ? .caption : .title)
} icon: {
Image(systemName: "gear")
.symbolRenderingMode(.hierarchical)
.font(idiom == .phone ? .caption : .title)
.frame(width: 35)
}
.padding(.bottom, 5)
.listRowSeparator(.visible)
/// Process
Label {
Text("log.process".localized + ":")
.font(idiom == .phone ? .caption : .title)
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
Text(log.process)
.font(idiom == .phone ? .caption : .title)
} icon: {
Image(systemName: "tag")
.symbolRenderingMode(.hierarchical)
.font(idiom == .phone ? .caption : .title)
.frame(width: 35)
}
.padding(.bottom, 5)
.listRowSeparator(.visible)
/// Category
Label {
Text("log.category".localized + ":")
.font(idiom == .phone ? .caption : .title)
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
Text(log.category)
.font(idiom == .phone ? .caption : .title)
} icon: {
Image(systemName: "square.grid.2x2")
.symbolRenderingMode(.hierarchical)
.font(idiom == .phone ? .caption : .title)
.frame(width: 35)
}
.padding(.bottom, 5)
.listRowSeparator(.visible)
/// Level
Label {
Text("log.level".localized + ":")
.font(idiom == .phone ? .caption : .title)
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
Text(log.level.description)
.font(idiom == .phone ? .caption : .title)
} icon: {
Image(systemName: "stairs")
.symbolRenderingMode(.hierarchical)
.font(idiom == .phone ? .caption : .title)
.frame(width: 35)
}
.padding(.bottom, 5)
.listRowSeparator(.visible)
/// message
Label {
Text("log.message".localized + ":")
.font(idiom == .phone ? .caption : .title)
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
Text(log.composedMessage)
.textSelection(.enabled)
.font(idiom == .phone ? .body : .title)
.padding(.bottom, 5)
} icon: {
Image(systemName: "text.bubble")
.symbolRenderingMode(.hierarchical)
.font(idiom == .phone ? .callout : .title)
.frame(width: 35)
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
}
Spacer()
}
.padding(.top)
#if targetEnvironment(macCatalyst)
Spacer()
Button {
dismiss()
} label: {
Label("close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
#endif
}
.monospaced()
.presentationDetents([.fraction(0.75), .fraction(0.85), .fraction(1.0)])
.presentationDragIndicator(.visible)
}
}

View file

@ -41,6 +41,7 @@ struct MeshLog: View {
// Stop adding logs when an error is thrown
}
}
.listStyle(.plain)
.fileExporter(
isPresented: $isExporting,
document: document,

View file

@ -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 {

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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"="מפת מש";

View file

@ -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";

@ -1 +1 @@
Subproject commit 1c3029f2868e5fc49809fd378f6c0c66aee0eaf4
Subproject commit 4da558d0f73c46ef91b74431facee73c09affbfc

View file

@ -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";

View file

@ -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";

View file

@ -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 地图";

View file

@ -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 地圖";