Merge branch 'main'

This commit is contained in:
Garth Vander Houwen 2024-08-26 23:04:53 -07:00
commit b3cbdf96da
17 changed files with 822 additions and 200 deletions

View file

@ -140,6 +140,16 @@
},
"%@%%" : {
},
"%@%% %@%%" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@%% %2$@%%"
}
}
}
},
"%@°F" : {
@ -6715,6 +6725,9 @@
},
"Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor." : {
},
"Dupe / Bad Packets: %d" : {
},
"echo" : {
"localizations" : {
@ -16081,6 +16094,12 @@
},
"Override automatic OLED screen detection." : {
},
"Packets Received: %d" : {
},
"Packets Sent: %d" : {
},
"password" : {
"localizations" : {
@ -17402,6 +17421,9 @@
},
"Requires that there be an accelerometer on your device." : {
},
"Reset App Settings" : {
},
"Reset NodeDB" : {
@ -22272,7 +22294,7 @@
}
}
},
"Updated Device Metrics Data." : {
"Updated Node Stats Data." : {
},
"Updated: %@" : {
@ -22709,7 +22731,7 @@
"Your Firmware is up to date" : {
},
"Your MQTT Server must support TLS." : {
"Your MQTT Server must support TLS. Not available via the public mqtt server." : {
},
"Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned." : {

View file

@ -376,6 +376,7 @@
DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = "<group>"; };
DD77093E2AA1B146007A8BF0 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = "<group>"; };
DD798B062915928D005217CD /* ChannelMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMessageList.swift; sourceTree = "<group>"; };
DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 43.xcdatamodel"; sourceTree = "<group>"; };
DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLogger.swift; sourceTree = "<group>"; };
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = "<group>"; };
DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = "<group>"; };
@ -1897,6 +1898,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */,
DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */,
DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */,
DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */,
@ -1940,7 +1942,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */;
currentVersion = DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -52,8 +52,8 @@ extension NodeInfoEntity {
}
var isOnline: Bool {
let fifteenMinutesAgo = Calendar.current.date(byAdding: .minute, value: -15, to: Date())
if lastHeard?.compare(fifteenMinutesAgo!) == .orderedDescending {
let twoHoursAgo = Calendar.current.date(byAdding: .minute, value: -120, to: Date())
if lastHeard?.compare(twoHoursAgo!) == .orderedDescending {
return true
}
return false

View file

@ -681,14 +681,10 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) {
// Only log telemetry from the mesh not the connected device
if connectedNode != Int64(packet.from) {
let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from))
MeshLogger.log("📈 \(logString)")
} else {
// If it is the connected node
}
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) {
let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from))
MeshLogger.log("📈 \(logString)")
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) {
/// Other unhandled telemetry packets
return
}
@ -727,6 +723,18 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
telemetry.windLull = telemetryMessage.environmentMetrics.windLull
telemetry.windDirection = Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.windDirection)
telemetry.metricsType = 1
} else if telemetryMessage.variant == Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) {
// Local Stats for Live activity
telemetry.uptimeSeconds = Int32(telemetryMessage.localStats.uptimeSeconds)
telemetry.channelUtilization = telemetryMessage.localStats.channelUtilization
telemetry.airUtilTx = telemetryMessage.localStats.airUtilTx
telemetry.numPacketsTx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsTx)
telemetry.numPacketsRx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRx)
telemetry.numPacketsRxBad = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRxBad)
telemetry.numOnlineNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numOnlineNodes)
telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes)
telemetry.metricsType = 6
Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)")
}
telemetry.snr = packet.rxSnr
telemetry.rssi = packet.rxRssi
@ -743,34 +751,45 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet
}
try context.save()
// Only log telemetry from the mesh not the connected device
if connectedNode != Int64(packet.from) {
Logger.data.info("💾 [TelemetryEntity] Saved for Node: \(packet.from.toHex())")
} else if telemetry.metricsType == 0 {
Logger.data.info("💾 [TelemetryEntity] Saved for Node: \(packet.from.toHex())")
if telemetry.metricsType == 0 {
// Connected Device Metrics
// ------------------------
// Low Battery notification
if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 {
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(UUID().uuidString)"),
title: "Critically Low Battery!",
subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")",
content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.",
target: "nodes",
path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)"
)
]
manager.schedule()
if connectedNode != Int64(packet.from) {
if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 {
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(UUID().uuidString)"),
title: "Critically Low Battery!",
subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")",
content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.",
target: "nodes",
path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)"
)
]
manager.schedule()
}
}
} else if telemetry.metricsType == 6 {
// Update our live activity if there is one running, not available on mac iOS >= 16.2
#if !targetEnvironment(macCatalyst)
let oneMinuteLater = Calendar.current.date(byAdding: .minute, value: (Int(1) ), to: Date())!
let date = Date.now...oneMinuteLater
let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(timerRange: date, connected: true, channelUtilization: telemetry.channelUtilization, airtime: telemetry.airUtilTx, batteryLevel: UInt32(telemetry.batteryLevel), nodes: 17, nodesOnline: 9)
let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Device Metrics Data.", sound: .default)
let fifteenMinutesLater = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())!
let date = Date.now...fifteenMinutesLater
let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(uptimeSeconds: UInt32(telemetry.uptimeSeconds),
channelUtilization: telemetry.channelUtilization,
airtime: telemetry.airUtilTx,
sentPackets: UInt32(telemetry.numPacketsTx),
receivedPackets: UInt32(telemetry.numPacketsRx),
badReceivedPackets: UInt32(telemetry.numPacketsRxBad),
nodesOnline: UInt32(telemetry.numOnlineNodes),
totalNodes: UInt32(telemetry.numTotalNodes),
timerRange: date)
let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Node Stats Data.", sound: .default)
let updatedContent = ActivityContent(state: updatedMeshStatus, staleDate: nil)
let meshActivity = Activity<MeshActivityAttributes>.activities.first(where: { $0.attributes.nodeNum == connectedNode })

View file

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

View file

@ -0,0 +1,475 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G5075b" 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"/>
<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"/>
</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="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
<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="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"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="messageId"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String"/>
<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"/>
<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="sessionExpiration" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sessionPasskey" optional="YES" attributeType="Binary"/>
<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="securityConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SecurityConfigEntity" inverseName="securityConfigNode" inverseEntity="SecurityConfigEntity"/>
<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="SecurityConfigEntity" representedClassName="SecurityConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="adminChannelEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="adminKey" optional="YES" attributeType="Binary"/>
<attribute name="bluetoothLoggingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="debugLogApiEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isManaged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="privateKey" optional="YES" attributeType="Binary"/>
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
<attribute name="serialEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="securityConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="securityConfig" 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="numOnlineNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numPacketsRx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numPacketsRxBad" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numPacketsTx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numTotalNodes" 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"/>
<attribute name="weight" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="windDirection" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="windGust" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="windLull" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="windSpeed" 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="hwDisplayName" optional="YES" attributeType="String"/>
<attribute name="hwModel" attributeType="String"/>
<attribute name="hwModelId" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="keyMatch" attributeType="Boolean" defaultValueString="YES" 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="newPublicKey" optional="YES" attributeType="Binary"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numString" optional="YES" attributeType="String"/>
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
<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"/>
</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

@ -52,38 +52,80 @@ struct Connect: View {
if #available(iOS 17.0, macOS 14.0, *) {
TipView(BluetoothConnectionTip(), arrowEdge: .bottom)
}
HStack {
VStack(alignment: .center) {
CircleText(text: node?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 90)
}
.padding(.trailing)
VStack(alignment: .leading) {
if node != nil {
Text(connectedPeripheral.longName).font(.title2)
VStack(alignment: .leading) {
HStack {
VStack(alignment: .center) {
CircleText(text: node?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 90)
.padding(.trailing, 5)
if node?.latestDeviceMetrics != nil {
BatteryCompact(batteryLevel: node?.latestDeviceMetrics?.batteryLevel ?? 0, font: .caption, iconFont: .callout, color: .accentColor)
.padding(.trailing, 5)
}
}
Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)")
.font(.callout).foregroundColor(Color.gray)
if node != nil {
Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)")
.padding(.trailing)
VStack(alignment: .leading) {
if node != nil {
Text(connectedPeripheral.longName).font(.title2)
}
Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)")
.font(.callout).foregroundColor(Color.gray)
}
if bleManager.isSubscribed {
Text("subscribed").font(.callout)
.foregroundColor(.green)
} else {
HStack {
if #available(iOS 17.0, macOS 14.0, *) {
Image(systemName: "square.stack.3d.down.forward")
.symbolRenderingMode(.multicolor)
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
if node != nil {
Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)")
.font(.callout).foregroundColor(Color.gray)
}
if bleManager.isSubscribed {
Text("subscribed").font(.callout)
.foregroundColor(.green)
} else {
HStack {
if #available(iOS 17.0, macOS 14.0, *) {
Image(systemName: "square.stack.3d.down.forward")
.symbolRenderingMode(.multicolor)
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
.foregroundColor(.orange)
}
Text("communicating").font(.callout)
.foregroundColor(.orange)
}
Text("communicating").font(.callout)
.foregroundColor(.orange)
}
}
}
VStack {
let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity
if localStats != nil {
Divider()
if localStats?.numTotalNodes ?? 0 >= 100 {
Text("\(String(format: "Connected: %d nodes online", localStats?.numOnlineNodes ?? 0))")
.font(.callout)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.fixedSize()
} else {
Text("\(String(format: "Connected: %d of %d nodes online", localStats?.numOnlineNodes ?? 0, localStats?.numTotalNodes ?? 0))")
.font(.callout)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.fixedSize()
}
Text("\(String(format: "Ch. Util: %.2f", localStats?.channelUtilization ?? 0))% \(String(format: "Airtime: %.2f", localStats?.airUtilTx ?? 0))%")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
Text("Packets Sent: \(localStats?.numPacketsTx ?? 0)")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
Text("Packets Received: \(localStats?.numPacketsRx ?? 0)")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
Text("Dupe / Bad Packets: \(localStats?.numPacketsRxBad ?? 0)")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.fixedSize()
}
}
}
.font(.caption)
.foregroundColor(Color.gray)
@ -327,17 +369,25 @@ struct Connect: View {
#if canImport(ActivityKit)
func startNodeActivity() {
liveActivityStarted = true
let timerSeconds = 60
let deviceMetrics = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
// 15 Minutes Local Stats Interval
let timerSeconds = 900
let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6"))
let mostRecent = localStats?.lastObject as? TelemetryEntity
let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown")
let future = Date(timeIntervalSinceNow: Double(timerSeconds))
let initialContentState = MeshActivityAttributes.ContentState(uptimeSeconds: UInt32(mostRecent?.uptimeSeconds ?? 0),
channelUtilization: mostRecent?.channelUtilization ?? 0.0,
airtime: mostRecent?.airUtilTx ?? 0.0,
sentPackets: UInt32(mostRecent?.numPacketsTx ?? 0),
receivedPackets: UInt32(mostRecent?.numPacketsRx ?? 0),
badReceivedPackets: UInt32(mostRecent?.numPacketsRxBad ?? 0),
nodesOnline: UInt32(mostRecent?.numOnlineNodes ?? 0),
totalNodes: UInt32(mostRecent?.numTotalNodes ?? 0),
timerRange: Date.now...future)
let initialContentState = MeshActivityAttributes.ContentState(timerRange: Date.now...future, connected: true, channelUtilization: mostRecent?.channelUtilization ?? 0.0, airtime: mostRecent?.airUtilTx ?? 0.0, batteryLevel: UInt32(mostRecent?.batteryLevel ?? 0), nodes: 17, nodesOnline: 9)
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 2, to: Date())!)
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 15, to: Date())!)
do {
let myActivity = try Activity<MeshActivityAttributes>.request(attributes: activityAttributes, content: activityContent,

View file

@ -98,9 +98,6 @@ struct ChannelMessageList: View {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red)
.font(.caption2)
} else {
let messageDate = message.timestamp
Text(" \(messageDate.formattedDate(format: MessageText.dateFormatString))").font(.caption2).foregroundColor(.gray)
}
}
}

View file

@ -227,6 +227,9 @@ struct UserList: View {
.onChange(of: maxDistance) { _ in
searchUserList()
}
.onReceive(users.publisher) { _ in
searchUserList()
}
.onAppear {
searchUserList()
}
@ -307,7 +310,7 @@ struct UserList: View {
}
/// Online
if isOnline {
let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate)
predicates.append(isOnlinePredicate)
}
/// Encrypted

View file

@ -126,7 +126,7 @@ struct MeshMap: View {
guard case .map(let selectedNodeNum) = router.navigationState else { return }
// TODO: handle deep link for waypoints
}
.onChange(of: (selectedMapLayer)) { newMapLayer in
.onChange(of: selectedMapLayer) { newMapLayer in
switch selectedMapLayer {
case .standard:
UserDefaults.mapLayer = newMapLayer

View file

@ -344,6 +344,11 @@ struct NodeList: View {
self.selectedNode = nil
}
}
.onReceive(nodes.publisher) { _ in
Task {
await searchNodeList()
}
}
.onAppear {
Task {
await searchNodeList()
@ -390,7 +395,7 @@ struct NodeList: View {
}
/// Online
if isOnline {
let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate)
predicates.append(isOnlinePredicate)
}
/// Encrypted

View file

@ -76,9 +76,14 @@ struct AppSettings: View {
}
clearCoreDataDatabase(context: context, includeRoutes: true)
context.refreshAllObjects()
UserDefaults.standard.reset()
}
}
Button {
UserDefaults.standard.reset()
} label: {
Label("Reset App Settings", systemImage: "arrow.counterclockwise.circle")
.foregroundColor(.red)
}
}
if totalDownloadedTileSize != "0MB" {
Section(header: Text("Map Tile Data")) {

View file

@ -92,13 +92,22 @@ struct Channels: View {
positionPrecision = 32
preciseLocation = true
positionsEnabled = true
if channelKey == "AQ==" {
positionPrecision = 13
preciseLocation = false
positionsEnabled = true
}
} else if !supportedVersion && channelRole == 2 {
positionPrecision = 0
preciseLocation = false
positionsEnabled = false
} else {
if positionPrecision == 32 {
if channelKey == "AQ==" {
preciseLocation = false
if positionPrecision < 10 || positionPrecision > 16 {
positionPrecision = 13
}
} else if positionPrecision == 32 {
preciseLocation = true
positionsEnabled = true
} else {
@ -250,7 +259,7 @@ struct Channels: View {
channelKey = key
positionsEnabled = false
preciseLocation = false
positionPrecision = 0
positionPrecision = 13
uplink = false
downlink = false
hasChanges = true

View file

@ -157,7 +157,8 @@ struct ChannelForm: View {
if !preciseLocation {
VStack(alignment: .leading) {
Label("Approximate Location", systemImage: "location.slash.circle.fill")
Slider(value: $positionPrecision, in: 10...19, step: 1) {
Slider(value: $positionPrecision, in: 10...16, step: 1) {
} minimumValueLabel: {
Image(systemName: "minus")
} maximumValueLabel: {
@ -199,6 +200,12 @@ struct ChannelForm: View {
.onChange(of: channelKey) { _ in
hasChanges = true
}
.onChange(of: channelKeySize) { _ in
if channelKeySize == -1 {
preciseLocation = false
channelKey = "AQ=="
}
}
.onChange(of: channelRole) { _ in
hasChanges = true
}
@ -206,7 +213,7 @@ struct ChannelForm: View {
if loc == true {
positionPrecision = 32
} else {
positionPrecision = 14
positionPrecision = 13
}
hasChanges = true
}
@ -216,7 +223,7 @@ struct ChannelForm: View {
.onChange(of: positionsEnabled) { pe in
if pe {
if positionPrecision == 0 {
positionPrecision = 32
positionPrecision = 13
}
} else {
positionPrecision = 0
@ -229,7 +236,7 @@ struct ChannelForm: View {
.onChange(of: downlink) { _ in
hasChanges = true
}
.onAppear {
.onFirstAppear {
let tempKey = Data(base64Encoded: channelKey) ?? Data()
if tempKey.count == channelKeySize || channelKeySize == -1 {
hasValidKey = true

View file

@ -24,7 +24,7 @@ struct MQTTConfig: View {
@State var password = ""
@State var encryptionEnabled = true
@State var jsonEnabled = false
@State var tlsEnabled = true
@State var tlsEnabled = false
@State var root = "msh"
@State var selectedTopic = ""
@State var mqttConnected: Bool = false
@ -234,7 +234,7 @@ struct MQTTConfig: View {
.listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/)
Toggle(isOn: $tlsEnabled) {
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
Text("Your MQTT Server must support TLS.")
Text("Your MQTT Server must support TLS. Not available via the public mqtt server.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
@ -288,9 +288,6 @@ struct MQTTConfig: View {
jsonEnabled = false
}
if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true }
if newProxyToClientEnabled {
jsonEnabled = false
}
}
.onChange(of: address) { newAddress in
if node != nil && node?.mqttConfig != nil {
@ -324,8 +321,12 @@ struct MQTTConfig: View {
}
if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true }
}
.onChange(of: tlsEnabled) {
if $0 != node?.mqttConfig?.tlsEnabled { hasChanges = true }
.onChange(of: tlsEnabled) { newTlsEnabled in
if address.lowercased() == "mqtt.meshtastic.org" {
tlsEnabled = false
} else {
if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true }
}
}
.onChange(of: mqttConnected) { newMqttConnected in
if newMqttConnected == false {

View file

@ -15,13 +15,15 @@ struct MeshActivityAttributes: ActivityAttributes {
public typealias MeshActivityStatus = ContentState
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
var timerRange: ClosedRange<Date>
var connected: Bool
var uptimeSeconds: UInt32
var channelUtilization: Float
var airtime: Float
var batteryLevel: UInt32
var nodes: Int
var nodesOnline: Int
var sentPackets: UInt32
var receivedPackets: UInt32
var badReceivedPackets: UInt32
var nodesOnline: UInt32
var totalNodes: UInt32
var timerRange: ClosedRange<Date>
}
// Fixed non-changing properties about your activity go here!

View file

@ -13,64 +13,84 @@ struct WidgetsLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: MeshActivityAttributes.self) { context in
LiveActivityView(nodeName: context.attributes.name, channelUtilization: context.state.channelUtilization, airtime: context.state.airtime, batteryLevel: context.state.batteryLevel, nodes: 17, nodesOnline: 7, timerRange: context.state.timerRange)
.widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)"))
LiveActivityView(nodeName: context.attributes.name,
uptimeSeconds: 0, // context.attributes.uptimeSeconds,
channelUtilization: context.state.channelUtilization,
airtime: context.state.airtime,
sentPackets: context.state.sentPackets,
receivedPackets: context.state.receivedPackets,
badReceivedPackets: context.state.badReceivedPackets,
nodesOnline: context.state.nodesOnline,
totalNodes: context.state.totalNodes,
timerRange: context.state.timerRange)
.widgetURL(URL(string: "meshtastic:///bluetooth"))
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Text("Network")
.font(.headline)
.fontWeight(.bold)
.foregroundStyle(.secondary)
.fixedSize()
.padding(.top, 10)
HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
Spacer()
Text("Mesh")
.font(.callout)
.fontWeight(.medium)
.foregroundStyle(.primary)
.padding(.bottom, 10)
.fixedSize()
Spacer()
}
if context.state.nodesOnline >= 100 {
Text("100+ online")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize()
} else {
Text("\(context.state.nodesOnline) of \(context.state.totalNodes) online")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize()
}
Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%")
.font(.headline)
.fontWeight(.medium)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize()
Text("\(String(format: "Airtime: %.2f", context.state.airtime))%")
.font(.headline)
.fontWeight(.medium)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize()
Spacer()
}
DynamicIslandExpandedRegion(.center) {
VStack(alignment: .center, spacing: 0) {
BatteryIcon(batteryLevel: Int32(context.state.batteryLevel), font: .title, color: .accentColor)
if context.state.batteryLevel == 0 {
Text("< 1%")
.font(.title3)
.foregroundColor(.gray)
.fixedSize()
} else if context.state.batteryLevel < 101 {
Text(String(context.state.batteryLevel) + "%")
.font(.title3)
.foregroundColor(.gray)
.fixedSize()
} else {
Text("PWD")
.font(.title3)
.foregroundColor(.gray)
}
}
}
DynamicIslandExpandedRegion(.trailing, priority: 1) {
TimerView(timerRange: context.state.timerRange)
.tint(Color("LightIndigo"))
}
DynamicIslandExpandedRegion(.trailing, priority: 1) {
HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
Spacer()
Text("Packets")
.font(.callout)
.fontWeight(.medium)
.foregroundStyle(.primary)
.padding(.bottom, 10)
.fixedSize()
Spacer()
}
Text("Sent: \(context.state.sentPackets)")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize()
Text("Received: \(context.state.receivedPackets)")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize()
Text("Dupe / Bad \(context.state.badReceivedPackets)")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize()
}
DynamicIslandExpandedRegion(.bottom) {
Text(context.attributes.name)
.font(context.attributes.name.count > 14 ? .callout : .title3)
.fontWeight(.semibold)
.foregroundStyle(.tint)
Text("Last Heard: \(Date().formatted())")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.foregroundStyle(.tint)
.fixedSize()
}
@ -95,85 +115,63 @@ struct WidgetsLiveActivity: Widget {
.contentMargins(.trailing, 32, for: .expanded)
.contentMargins([.leading, .top, .bottom], 6, for: .compactLeading)
.contentMargins(.all, 6, for: .minimal)
.widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)"))
.widgetURL(URL(string: "meshtastic:///bluetooth"))
}
}
}
struct WidgetsLiveActivity_Previews: PreviewProvider {
static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G")
static let state = MeshActivityAttributes.ContentState(
timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9)
static var previews: some View {
attributes
.previewContext(state, viewKind: .dynamicIsland(.compact))
.previewDisplayName("Compact")
attributes
.previewContext(state, viewKind: .dynamicIsland(.minimal))
.previewDisplayName("Minimal")
attributes
.previewContext(state, viewKind: .dynamicIsland(.expanded))
.previewDisplayName("Expanded")
attributes
.previewContext(state, viewKind: .content)
.previewDisplayName("Notification")
}
}
//struct WidgetsLiveActivity_Previews: PreviewProvider {
// static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G")
// static let state = MeshActivityAttributes.ContentState(
// timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9)
//
// static var previews: some View {
// attributes
// .previewContext(state, viewKind: .dynamicIsland(.compact))
// .previewDisplayName("Compact")
// attributes
// .previewContext(state, viewKind: .dynamicIsland(.minimal))
// .previewDisplayName("Minimal")
// attributes
// .previewContext(state, viewKind: .dynamicIsland(.expanded))
// .previewDisplayName("Expanded")
// attributes
// .previewContext(state, viewKind: .content)
// .previewDisplayName("Notification")
// }
//}
struct LiveActivityView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.isLuminanceReduced) var isLuminanceReduced
var nodeName: String
// var connected: Bool
var uptimeSeconds: UInt32
var channelUtilization: Float
var airtime: Float
var batteryLevel: UInt32
var nodes: Int
var nodesOnline: Int
var sentPackets: UInt32
var receivedPackets: UInt32
var badReceivedPackets: UInt32
var nodesOnline: UInt32
var totalNodes: UInt32
var timerRange: ClosedRange<Date>
var body: some View {
HStack {
Spacer()
Image(colorScheme == .light ? "m-logo-black" : "m-logo-white")
.resizable()
.clipShape(ContainerRelativeShape())
.opacity(isLuminanceReduced ? 0.5 : 1.0)
.aspectRatio(contentMode: .fit)
.frame(width: 65)
.frame(minWidth: 25, idealWidth: 45, maxWidth: 55)
Spacer()
NodeInfoView(nodeName: nodeName, timerRange: timerRange, channelUtilization: channelUtilization, airtime: airtime, batteryLevel: batteryLevel, nodes: nodes, nodesOnline: nodesOnline)
NodeInfoView(isLuminanceReduced: _isLuminanceReduced, nodeName: nodeName, uptimeSeconds: uptimeSeconds, channelUtilization: channelUtilization, airtime: airtime, sentPackets: sentPackets, receivedPackets: receivedPackets, badReceivedPackets: badReceivedPackets, nodesOnline: nodesOnline, totalNodes: totalNodes, timerRange: timerRange)
Spacer()
VStack {
BatteryIcon(batteryLevel: Int32(batteryLevel), font: .title, color: .secondary)
if batteryLevel == 0 {
Text("< 1%")
.font(.headline)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
} else if batteryLevel < 101 {
Text(String(batteryLevel) + "%")
.font(.headline)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
} else {
Text("Plugged In")
.font(.headline)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
}
}
}
.tint(.primary)
.padding([.leading, .top, .bottom])
.padding(.trailing, 32)
.padding(.trailing, 25)
.activityBackgroundTint(colorScheme == .light ? Color("LiveActivityBackground") : Color("AccentColorDimmed"))
.activitySystemActionForegroundColor(.primary)
}
@ -183,12 +181,15 @@ struct NodeInfoView: View {
@Environment(\.isLuminanceReduced) var isLuminanceReduced
var nodeName: String
var timerRange: ClosedRange<Date>
var uptimeSeconds: UInt32
var channelUtilization: Float
var airtime: Float
var batteryLevel: UInt32
var nodes: Int
var nodesOnline: Int
var sentPackets: UInt32
var receivedPackets: UInt32
var badReceivedPackets: UInt32
var nodesOnline: UInt32
var totalNodes: UInt32
var timerRange: ClosedRange<Date>
var body: some View {
VStack(alignment: .leading, spacing: 0) {
@ -196,24 +197,45 @@ struct NodeInfoView: View {
.font(nodeName.count > 14 ? .callout : .title3)
.fontWeight(.semibold)
.foregroundStyle(.tint)
Text("\(String(format: "Ch. Util: %.2f", channelUtilization))%")
.font(.headline)
Text("\(String(format: "Ch. Util: %.2f", channelUtilization))% \(String(format: "Airtime: %.2f", airtime))%")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
Text("\(String(format: "Airtime: %.2f", airtime))%")
.font(.headline)
Text("Packets Sent: \(sentPackets)")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
// Text("\(String(format: "Connected: %d of %d online", nodesOnline, nodes))")
// .font(.callout)
// .fontWeight(.medium)
// .foregroundStyle(.secondary)
// .opacity(isLuminanceReduced ? 0.8 : 1.0)
// .fixedSize()
Text("Packets Received: \(receivedPackets)")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
Text("Dupe / Bad Packets: \(badReceivedPackets)")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
if totalNodes >= 100 {
Text("\(String(format: "Connected: %d nodes online", nodesOnline))")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
} else {
Text("\(String(format: "Connected: %d of %d nodes online", nodesOnline, totalNodes))")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.8 : 1.0)
.fixedSize()
}
let now = Date()
Text("Last Heard: \(now.formatted())")
.font(.caption)
@ -255,8 +277,9 @@ struct TimerView: View {
var body: some View {
VStack(alignment: .center) {
Text("NEXT UPDATE")
.font(.caption)
Text("UPDATE IN")
.font(.caption2)
.allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.opacity(isLuminanceReduced ? 0.5 : 1.0)
@ -268,10 +291,12 @@ struct TimerView: View {
.fontWeight(.semibold)
.foregroundStyle(.tint)
Image(systemName: "timer")
.symbolRenderingMode(.multicolor)
.resizable()
.foregroundStyle(.secondary)
.frame(width: 30, height: 30)
.opacity(isLuminanceReduced ? 0.5 : 1.0)
.offset(y: -5)
}
}
}