Merge pull request #131 from meshtastic/remove_analog_sensors

Remove analog sensors
This commit is contained in:
Garth Vander Houwen 2022-08-01 08:56:11 -07:00 committed by GitHub
commit 96eef069f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 279 additions and 295 deletions

View file

@ -145,6 +145,7 @@
DD86D4102881D16900BAEB7A /* WriteCsvFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteCsvFile.swift; sourceTree = "<group>"; };
DD882F5C2772E4640005BF05 /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = "<group>"; };
DD8EBF42285058FA00426DCA /* DisplayConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayConfig.swift; sourceTree = "<group>"; };
DD8ED9C328978D9D00B3B0AB /* MeshtasticDataModel v 5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModel v 5.xcdatamodel"; sourceTree = "<group>"; };
DD90860A26F645B700DC5189 /* Meshtastic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Meshtastic.entitlements; sourceTree = "<group>"; };
DD90860B26F684AF00DC5189 /* BatteryIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryIcon.swift; sourceTree = "<group>"; };
DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = "<group>"; };
@ -1080,12 +1081,13 @@
DD9D8F2D2764403B00080993 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DD8ED9C328978D9D00B3B0AB /* MeshtasticDataModel v 5.xcdatamodel */,
DD619373285CC7D600E59241 /* MeshtasticDataModel v 4.xcdatamodel */,
DDB2CC6F27F3F0AC009C5FCC /* MeshtasticDataModel v 3.xcdatamodel */,
DD45C77427BD4EF80011784F /* MeshtasticDataModel v2.xcdatamodel */,
DD9D8F2E2764403B00080993 /* CoreDataSample.xcdatamodel */,
);
currentVersion = DD619373285CC7D600E59241 /* MeshtasticDataModel v 4.xcdatamodel */;
currentVersion = DD8ED9C328978D9D00B3B0AB /* MeshtasticDataModel v 5.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -35,8 +35,30 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
}
} else {
// Create Device Telemetry Header
csvString = "Battery Level, Voltage, Channel Utilization, Airtime, Timestamp"
// Create Environment Telemetry Header
csvString = "Temperature, Relative Humidity, Barometric Pressure, Gas Resistance, Voltage, Current"
for dm in telemetry{
if dm.metricsType == 0 {
csvString += "\n"
csvString += String("\(dm.temperature)°")
csvString += ", "
csvString += String(dm.relativeHumidity)
csvString += ", "
csvString += String(dm.barometricPressure)
csvString += ", "
csvString += String(dm.gasResistance)
csvString += ", "
csvString += String(dm.voltage)
csvString += ", "
csvString += String(dm.current)
csvString += ", "
csvString += dm.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? "Unknown Age"
}
}
}
return csvString

View file

@ -723,25 +723,17 @@ func moduleConfig (config: ModuleConfig, meshlogging: Bool, context:NSManagedObj
newTelemetryConfig.deviceUpdateInterval = 0
newTelemetryConfig.environmentUpdateInterval = 0
newTelemetryConfig.environmentMeasurementEnabled = false
newTelemetryConfig.environmentSensorType = 0
newTelemetryConfig.environmentScreenEnabled = false
newTelemetryConfig.environmentDisplayFahrenheit = false
newTelemetryConfig.environmentRecoveryInterval = 0
newTelemetryConfig.environmentReadErrorCountThreshold = 0
} else {
newTelemetryConfig.deviceUpdateInterval = Int32(config.telemetry.deviceUpdateInterval)
newTelemetryConfig.environmentUpdateInterval = Int32(config.telemetry.environmentUpdateInterval)
newTelemetryConfig.environmentMeasurementEnabled = config.telemetry.environmentMeasurementEnabled
newTelemetryConfig.environmentSensorType = Int32(config.telemetry.environmentSensorType.rawValue)
newTelemetryConfig.environmentScreenEnabled = config.telemetry.environmentScreenEnabled
newTelemetryConfig.environmentDisplayFahrenheit = config.telemetry.environmentDisplayFahrenheit
newTelemetryConfig.environmentRecoveryInterval = Int32(config.telemetry.environmentRecoveryInterval)
newTelemetryConfig.environmentReadErrorCountThreshold = Int32(config.telemetry.environmentReadErrorCountThreshold)
}
fetchedNode[0].telemetryConfig = newTelemetryConfig
} else {
@ -751,23 +743,16 @@ func moduleConfig (config: ModuleConfig, meshlogging: Bool, context:NSManagedObj
fetchedNode[0].telemetryConfig?.deviceUpdateInterval = 0
fetchedNode[0].telemetryConfig?.environmentUpdateInterval = 0
fetchedNode[0].telemetryConfig?.environmentMeasurementEnabled = false
fetchedNode[0].telemetryConfig?.environmentSensorType = 0
fetchedNode[0].telemetryConfig?.environmentScreenEnabled = false
fetchedNode[0].telemetryConfig?.environmentDisplayFahrenheit = false
fetchedNode[0].telemetryConfig?.environmentRecoveryInterval = 0
fetchedNode[0].telemetryConfig?.environmentReadErrorCountThreshold = 0
} else {
fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(config.telemetry.deviceUpdateInterval)
fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(config.telemetry.environmentUpdateInterval)
fetchedNode[0].telemetryConfig?.environmentMeasurementEnabled = config.telemetry.environmentMeasurementEnabled
fetchedNode[0].telemetryConfig?.environmentSensorType = Int32(config.telemetry.environmentSensorType.rawValue)
fetchedNode[0].telemetryConfig?.environmentScreenEnabled = config.telemetry.environmentScreenEnabled
fetchedNode[0].telemetryConfig?.environmentDisplayFahrenheit = config.telemetry.environmentDisplayFahrenheit
fetchedNode[0].telemetryConfig?.environmentRecoveryInterval = Int32(config.telemetry.environmentRecoveryInterval)
fetchedNode[0].telemetryConfig?.environmentReadErrorCountThreshold = Int32(config.telemetry.environmentReadErrorCountThreshold)
}
}

View file

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>MeshtasticDataModel v 4.xcdatamodel</string>
<string>MeshtasticDataModel v 5.xcdatamodel</string>
</dict>
</plist>

View file

@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21G72" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<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="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<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="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" 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"/>
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" 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"/>
<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="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="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="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="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="fromUser" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
<relationship name="toUser" 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="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="bitrate" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="errorCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="firmwareVersion" attributeType="String"/>
<attribute name="hasGps" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="maxChannels" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="messageTimeoutMsec" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<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="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastHeard" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Nullify" 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="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
<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="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
<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="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="num"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
<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="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" 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="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="satsInView" 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="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 32" defaultValueString="0" 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="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="num" attributeType="Integer 64" defaultValueString="0" 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="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="num" attributeType="Integer 64" 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="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="relativeHumidity" 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="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="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="longName" attributeType="String"/>
<attribute name="macaddr" optional="YES" attributeType="Binary"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="shortName" attributeType="String"/>
<attribute name="team" optional="YES" 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="(toUser.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 isEmoji == false AND admin = false"/>
</fetchedProperty>
</entity>
<elements>
<element name="CannedMessageConfigEntity" positionX="45" positionY="144" width="128" height="209"/>
<element name="DeviceConfigEntity" positionX="45" positionY="144" width="128" height="104"/>
<element name="DisplayConfigEntity" positionX="54" positionY="153" width="128" height="104"/>
<element name="ExternalNotificationConfigEntity" positionX="63" positionY="162" width="128" height="149"/>
<element name="LoRaConfigEntity" positionX="45" positionY="144" width="128" height="119"/>
<element name="MessageEntity" positionX="-36" positionY="63" width="128" height="230"/>
<element name="MyInfoEntity" positionX="-18" positionY="81" width="128" height="209"/>
<element name="NodeInfoEntity" positionX="-63" positionY="-18" width="128" height="299"/>
<element name="PositionConfigEntity" positionX="63" positionY="162" width="128" height="149"/>
<element name="PositionEntity" positionX="-54" positionY="54" width="128" height="119"/>
<element name="RangeTestConfigEntity" positionX="72" positionY="171" width="128" height="104"/>
<element name="SerialConfigEntity" positionX="54" positionY="153" width="128" height="164"/>
<element name="TelemetryConfigEntity" positionX="72" positionY="171" width="128" height="134"/>
<element name="TelemetryEntity" positionX="160" positionY="192" width="128" height="209"/>
<element name="UserEntity" positionX="0" positionY="144" width="128" height="230"/>
</elements>
</model>

View file

@ -473,31 +473,11 @@ struct ModuleConfig {
/// Enable/Disable the telemetry measurement module on-device display
var environmentScreenEnabled: Bool = false
///
/// Sometimes sensor reads can fail.
/// If this happens, we will retry a configurable number of attempts,
/// each attempt will be delayed by the minimum required refresh rate for that sensor
var environmentReadErrorCountThreshold: UInt32 = 0
///
/// Sometimes we can end up with more than read_error_count_threshold failures.
/// In this case, we will stop trying to read from the sensor for a while.
/// Wait this long until trying to read from the sensor again
var environmentRecoveryInterval: UInt32 = 0
///
/// We'll always read the sensor in Celsius, but sometimes we might want to
/// display the results in Fahrenheit as a "user preference".
var environmentDisplayFahrenheit: Bool = false
///
/// Specify the sensor type
var environmentSensorType: TelemetrySensorType = .notSet
///
/// Specify the peferred GPIO Pin for sensor readings
var environmentSensorPin: UInt32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
@ -1184,11 +1164,7 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me
2: .standard(proto: "environment_update_interval"),
3: .standard(proto: "environment_measurement_enabled"),
4: .standard(proto: "environment_screen_enabled"),
5: .standard(proto: "environment_read_error_count_threshold"),
6: .standard(proto: "environment_recovery_interval"),
7: .standard(proto: "environment_display_fahrenheit"),
8: .standard(proto: "environment_sensor_type"),
9: .standard(proto: "environment_sensor_pin"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -1201,11 +1177,7 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.environmentUpdateInterval) }()
case 3: try { try decoder.decodeSingularBoolField(value: &self.environmentMeasurementEnabled) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.environmentScreenEnabled) }()
case 5: try { try decoder.decodeSingularUInt32Field(value: &self.environmentReadErrorCountThreshold) }()
case 6: try { try decoder.decodeSingularUInt32Field(value: &self.environmentRecoveryInterval) }()
case 7: try { try decoder.decodeSingularBoolField(value: &self.environmentDisplayFahrenheit) }()
case 8: try { try decoder.decodeSingularEnumField(value: &self.environmentSensorType) }()
case 9: try { try decoder.decodeSingularUInt32Field(value: &self.environmentSensorPin) }()
default: break
}
}
@ -1224,21 +1196,9 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me
if self.environmentScreenEnabled != false {
try visitor.visitSingularBoolField(value: self.environmentScreenEnabled, fieldNumber: 4)
}
if self.environmentReadErrorCountThreshold != 0 {
try visitor.visitSingularUInt32Field(value: self.environmentReadErrorCountThreshold, fieldNumber: 5)
}
if self.environmentRecoveryInterval != 0 {
try visitor.visitSingularUInt32Field(value: self.environmentRecoveryInterval, fieldNumber: 6)
}
if self.environmentDisplayFahrenheit != false {
try visitor.visitSingularBoolField(value: self.environmentDisplayFahrenheit, fieldNumber: 7)
}
if self.environmentSensorType != .notSet {
try visitor.visitSingularEnumField(value: self.environmentSensorType, fieldNumber: 8)
}
if self.environmentSensorPin != 0 {
try visitor.visitSingularUInt32Field(value: self.environmentSensorPin, fieldNumber: 9)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -1247,11 +1207,7 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me
if lhs.environmentUpdateInterval != rhs.environmentUpdateInterval {return false}
if lhs.environmentMeasurementEnabled != rhs.environmentMeasurementEnabled {return false}
if lhs.environmentScreenEnabled != rhs.environmentScreenEnabled {return false}
if lhs.environmentReadErrorCountThreshold != rhs.environmentReadErrorCountThreshold {return false}
if lhs.environmentRecoveryInterval != rhs.environmentRecoveryInterval {return false}
if lhs.environmentDisplayFahrenheit != rhs.environmentDisplayFahrenheit {return false}
if lhs.environmentSensorType != rhs.environmentSensorType {return false}
if lhs.environmentSensorPin != rhs.environmentSensorPin {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View file

@ -29,49 +29,29 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
/// No external telemetry sensor explicitly set
case notSet // = 0
///
/// Moderate accuracy temperature
case dht11 // = 1
///
/// High accuracy temperature
case ds18B20 // = 2
///
/// Moderate accuracy temperature and humidity
case dht12 // = 3
///
/// Moderate accuracy temperature and humidity
case dht21 // = 4
///
/// Moderate accuracy temperature and humidity
case dht22 // = 5
///
/// High accuracy temperature, pressure, humidity
case bme280 // = 6
case bme280 // = 1
///
/// High accuracy temperature, pressure, humidity, and air resistance
case bme680 // = 7
case bme680 // = 2
///
/// Very high accuracy temperature
case mcp9808 // = 8
///
/// Moderate accuracy temperature and humidity
case shtc3 // = 9
case mcp9808 // = 3
///
/// Moderate accuracy current and voltage
case ina260 // = 10
case ina260 // = 4
///
/// Moderate accuracy current and voltage
case ina219 // = 11
case ina219 // = 5
///
/// High accuracy temperature and pressure
case bmp280 // = 6
case UNRECOGNIZED(Int)
init() {
@ -81,17 +61,12 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
init?(rawValue: Int) {
switch rawValue {
case 0: self = .notSet
case 1: self = .dht11
case 2: self = .ds18B20
case 3: self = .dht12
case 4: self = .dht21
case 5: self = .dht22
case 6: self = .bme280
case 7: self = .bme680
case 8: self = .mcp9808
case 9: self = .shtc3
case 10: self = .ina260
case 11: self = .ina219
case 1: self = .bme280
case 2: self = .bme680
case 3: self = .mcp9808
case 4: self = .ina260
case 5: self = .ina219
case 6: self = .bmp280
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -99,17 +74,12 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
var rawValue: Int {
switch self {
case .notSet: return 0
case .dht11: return 1
case .ds18B20: return 2
case .dht12: return 3
case .dht21: return 4
case .dht22: return 5
case .bme280: return 6
case .bme680: return 7
case .mcp9808: return 8
case .shtc3: return 9
case .ina260: return 10
case .ina219: return 11
case .bme280: return 1
case .bme680: return 2
case .mcp9808: return 3
case .ina260: return 4
case .ina219: return 5
case .bmp280: return 6
case .UNRECOGNIZED(let i): return i
}
}
@ -122,17 +92,12 @@ extension TelemetrySensorType: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [TelemetrySensorType] = [
.notSet,
.dht11,
.ds18B20,
.dht12,
.dht21,
.dht22,
.bme280,
.bme680,
.mcp9808,
.shtc3,
.ina260,
.ina219,
.bmp280,
]
}
@ -285,17 +250,12 @@ extension Telemetry.OneOf_Variant: @unchecked Sendable {}
extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "NotSet"),
1: .same(proto: "DHT11"),
2: .same(proto: "DS18B20"),
3: .same(proto: "DHT12"),
4: .same(proto: "DHT21"),
5: .same(proto: "DHT22"),
6: .same(proto: "BME280"),
7: .same(proto: "BME680"),
8: .same(proto: "MCP9808"),
9: .same(proto: "SHTC3"),
10: .same(proto: "INA260"),
11: .same(proto: "INA219"),
1: .same(proto: "BME280"),
2: .same(proto: "BME680"),
3: .same(proto: "MCP9808"),
4: .same(proto: "INA260"),
5: .same(proto: "INA219"),
6: .same(proto: "BMP280"),
]
}

View file

@ -86,14 +86,10 @@ struct TelemetryLog: View {
Text("Environment Metrics")
.font(.title)
let sensor = SensorTypes(rawValue: Int(node.telemetryConfig?.environmentSensorType ?? 0))
let tempReadingType = (!(node.telemetryConfig?.environmentDisplayFahrenheit ?? true)) ? "°C" : "°F"
if sensor == SensorTypes.bme280 ||
sensor == SensorTypes.bme680 ||
sensor == SensorTypes.shtc3 ||
sensor == SensorTypes.mcp9808 {
if tel.temperature > 0 {
Image(systemName: "thermometer")
.font(.callout)
@ -104,9 +100,7 @@ struct TelemetryLog: View {
.font(.callout)
}
if sensor == SensorTypes.bme280 ||
sensor == SensorTypes.bme680 ||
sensor == SensorTypes.shtc3 {
if tel.relativeHumidity > 0 {
Image(systemName: "humidity")
.font(.callout)
@ -117,8 +111,7 @@ struct TelemetryLog: View {
.font(.callout)
}
if sensor == SensorTypes.bme280 ||
sensor == SensorTypes.bme680 {
if tel.barometricPressure > 0 {
Image(systemName: "barometer")
.font(.callout)
@ -129,7 +122,7 @@ struct TelemetryLog: View {
.font(.callout)
}
if sensor == SensorTypes.bme680 {
if tel.gasResistance > 0 {
Image(systemName: "aqi.medium")
.font(.callout)
@ -140,8 +133,7 @@ struct TelemetryLog: View {
.font(.callout)
}
if sensor == SensorTypes.ina219 ||
sensor == SensorTypes.ina260 {
if tel.current > 0 {
Image(systemName: "directcurrent")
.font(.callout)
@ -242,7 +234,6 @@ struct TelemetryLog: View {
} else if tel.metricsType == 1 {
// Environment Metrics
let sensor = SensorTypes(rawValue: Int(node.telemetryConfig?.environmentSensorType ?? 0))
let tempReadingType = (!(node.telemetryConfig?.environmentDisplayFahrenheit ?? true)) ? "°C" : "°F"
@ -260,10 +251,7 @@ struct TelemetryLog: View {
HStack {
if sensor == SensorTypes.bme280 ||
sensor == SensorTypes.bme680 ||
sensor == SensorTypes.shtc3 ||
sensor == SensorTypes.mcp9808 {
if tel.temperature > 0 {
Image(systemName: "thermometer")
.font(.callout)
@ -277,9 +265,7 @@ struct TelemetryLog: View {
HStack {
if sensor == SensorTypes.bme280 ||
sensor == SensorTypes.bme680 ||
sensor == SensorTypes.shtc3 {
if tel.relativeHumidity > 0 {
Image(systemName: "humidity")
.font(.callout)
@ -291,8 +277,7 @@ struct TelemetryLog: View {
}
}
if sensor == SensorTypes.ina219 ||
sensor == SensorTypes.ina260 {
if tel.current > 0 {
HStack {
@ -317,8 +302,7 @@ struct TelemetryLog: View {
}
}
if sensor == SensorTypes.bme280 ||
sensor == SensorTypes.bme680 {
if tel.barometricPressure > 0 {
HStack {
@ -332,7 +316,7 @@ struct TelemetryLog: View {
}
}
if sensor == SensorTypes.bme680 {
if tel.gasResistance > 0 {
HStack {

View file

@ -6,74 +6,6 @@
//
import SwiftUI
enum SensorTypes: Int, CaseIterable, Identifiable {
/// No external telemetry sensor explicitly set
case notSet = 0
/// High accuracy temperature, pressure, humidity
case bme280 = 6
/// High accuracy temperature, pressure, humidity, and air resistance
case bme680 = 7
/// Very high accuracy temperature
case mcp9808 = 8
/// Moderate accuracy temperature and humidity
case shtc3 = 9
/// Moderate accuracy current and voltage
case ina260 = 10
/// Moderate accuracy current and voltage
case ina219 = 11
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .notSet:
return "Not Set"
case .bme280:
return "BME280 - Temp, pressure & humidity"
case .bme680:
return "BME680 - Temp, pressure, humidity & air resistance"
case .mcp9808:
return "MCP9808 - Temperature"
case .shtc3:
return "SHTC3 - Temperature & humidity"
case .ina260:
return "INA260 - Current & voltage"
case .ina219:
return "INA219 - Current & voltage"
}
}
}
func protoEnumValue() -> TelemetrySensorType {
switch self {
case .notSet:
return TelemetrySensorType.notSet
case .bme280:
return TelemetrySensorType.bme280
case .bme680:
return TelemetrySensorType.bme680
case .mcp9808:
return TelemetrySensorType.mcp9808
case .shtc3:
return TelemetrySensorType.shtc3
case .ina260:
return TelemetrySensorType.ina260
case .ina219:
return TelemetrySensorType.ina219
}
}
}
// Default of 0 is off
enum ErrorRecoveryIntervals: Int, CaseIterable, Identifiable {
@ -188,11 +120,8 @@ struct TelemetryConfig: View {
@State var deviceUpdateInterval = 0
@State var environmentUpdateInterval = 0
@State var environmentMeasurementEnabled = false
@State var environmentSensorType = 0
@State var environmentScreenEnabled = false
@State var environmentDisplayFahrenheit = false
@State var environmentRecoveryInterval = 0
@State var environmentReadErrorCountThreshold = 0
var body: some View {
@ -222,6 +151,9 @@ struct TelemetryConfig: View {
}
Section(header: Text("Sensor Options")) {
Text("I2C Connected sensors will be detected automatically. Supported sensors are BMP280, BME280, BME680, MCP9808, INA219 and INA260.")
.font(.caption)
Toggle(isOn: $environmentMeasurementEnabled) {
@ -229,13 +161,6 @@ struct TelemetryConfig: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Picker("Sensor", selection: $environmentSensorType ) {
ForEach(SensorTypes.allCases) { st in
Text(st.description)
}
}
.pickerStyle(DefaultPickerStyle())
Toggle(isOn: $environmentScreenEnabled) {
Label("Show on device screen", systemImage: "display")
@ -247,37 +172,6 @@ struct TelemetryConfig: View {
Label("Display Fahrenheit", systemImage: "thermometer")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
Section(header: Text("Errors")) {
Picker("Error Count Threshold", selection: $environmentReadErrorCountThreshold) {
ForEach(0..<101) {
if $0 == 0 {
Text("Unset")
} else if $0 % 5 == 0 {
Text("\($0)")
}
}
}
.pickerStyle(DefaultPickerStyle())
Text("Sometimes sensor reads can fail. If this happens, we will retry a configurable number of attempts, each attempt will be delayed by the minimum required refresh rate for that sensor")
.font(.caption)
Picker("Error Recovery Interval", selection: $environmentRecoveryInterval ) {
ForEach(ErrorRecoveryIntervals.allCases) { eri in
Text(eri.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Sometimes we can end up with more failures than our error count threshold. In this case, we will stop trying to read from the sensor for a while. Wait this long until trying to read from the sensor again")
.font(.caption)
}
}
.disabled(bleManager.connectedPeripheral == nil)
@ -306,11 +200,8 @@ struct TelemetryConfig: View {
tc.deviceUpdateInterval = UInt32(deviceUpdateInterval)
tc.environmentUpdateInterval = UInt32(environmentUpdateInterval)
tc.environmentMeasurementEnabled = environmentMeasurementEnabled
tc.environmentSensorType = SensorTypes(rawValue: environmentSensorType)!.protoEnumValue()
tc.environmentScreenEnabled = environmentScreenEnabled
tc.environmentDisplayFahrenheit = environmentDisplayFahrenheit
tc.environmentRecoveryInterval = UInt32(environmentRecoveryInterval)
tc.environmentReadErrorCountThreshold = UInt32(environmentReadErrorCountThreshold)
let adminMessageId = bleManager.saveTelemetryModuleConfig(config: tc, fromUser: node!.user!, toUser: node!.user!, wantResponse: true)
@ -338,16 +229,11 @@ struct TelemetryConfig: View {
if self.initialLoad{
self.bleManager.context = context
self.deviceUpdateInterval = Int(node!.telemetryConfig?.deviceUpdateInterval ?? 0)
self.environmentUpdateInterval = Int(node!.telemetryConfig?.environmentUpdateInterval ?? 0)
self.environmentMeasurementEnabled = node!.telemetryConfig?.environmentMeasurementEnabled ?? false
self.environmentSensorType = Int(node!.telemetryConfig?.environmentSensorType ?? 0)
self.environmentScreenEnabled = node!.telemetryConfig?.environmentScreenEnabled ?? false
self.environmentDisplayFahrenheit = node!.telemetryConfig?.environmentDisplayFahrenheit ?? false
self.environmentRecoveryInterval = Int(node!.telemetryConfig?.environmentRecoveryInterval ?? 0)
self.environmentReadErrorCountThreshold = Int(node!.telemetryConfig?.environmentReadErrorCountThreshold ?? 0)
self.hasChanges = false
self.initialLoad = false
}
@ -373,13 +259,6 @@ struct TelemetryConfig: View {
if newEnvEnabled != node!.telemetryConfig!.environmentMeasurementEnabled { hasChanges = true }
}
}
.onChange(of: environmentSensorType) { newEnvSensorType in
if node != nil && node!.telemetryConfig != nil {
if newEnvSensorType != node!.telemetryConfig!.environmentSensorType { hasChanges = true }
}
}
.onChange(of: environmentScreenEnabled) { newEnvScreenEnabled in
if node!.telemetryConfig != nil {
@ -394,20 +273,6 @@ struct TelemetryConfig: View {
if newEnvDisplayF != node!.telemetryConfig!.environmentDisplayFahrenheit { hasChanges = true }
}
}
.onChange(of: environmentRecoveryInterval) { newEnvRecoveryInterval in
if node != nil && node!.telemetryConfig != nil {
if newEnvRecoveryInterval != node!.telemetryConfig!.environmentRecoveryInterval { hasChanges = true }
}
}
.onChange(of: environmentReadErrorCountThreshold) { newEnvReadErrorCountThreshold in
if node != nil && node!.telemetryConfig != nil {
if newEnvReadErrorCountThreshold != node!.telemetryConfig!.environmentReadErrorCountThreshold { hasChanges = true }
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}