mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #341 from meshtastic/2.1.4_Working_Changes
2.1.4 working changes
This commit is contained in:
commit
f2d17d496a
29 changed files with 1429 additions and 596 deletions
|
|
@ -108,6 +108,9 @@
|
|||
DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; };
|
||||
DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC3B273283F411B00AC321C /* LastHeardText.swift */; };
|
||||
DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; };
|
||||
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; };
|
||||
DDC94FC229CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; };
|
||||
DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */; };
|
||||
DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DDCDC6CD29481FCC004C1DDA /* Localizable.strings */; };
|
||||
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */; };
|
||||
DDD3BBD5292D763200D609B3 /* MeshtasticTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */; };
|
||||
|
|
@ -282,6 +285,9 @@
|
|||
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = "<group>"; };
|
||||
DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = "<group>"; };
|
||||
DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
|
||||
DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevel.swift; sourceTree = "<group>"; };
|
||||
DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV10.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RtttlConfig.swift; sourceTree = "<group>"; };
|
||||
DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV3.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDCDC6CC29481FCC004C1DDA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
DDCDC6CE294821AD004C1DDA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
|
|
@ -455,6 +461,7 @@
|
|||
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */,
|
||||
DD2160AE28C5552500C17253 /* MQTTConfig.swift */,
|
||||
DD41582928585C32009B0E59 /* RangeTestConfig.swift */,
|
||||
DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */,
|
||||
DD6193782863875F00E59241 /* SerialConfig.swift */,
|
||||
DD415827285859C4009B0E59 /* TelemetryConfig.swift */,
|
||||
);
|
||||
|
|
@ -677,6 +684,7 @@
|
|||
DDDE59FC29AF163D00490C6C /* Widgets.swift */,
|
||||
DDDE5A0029AF163E00490C6C /* Info.plist */,
|
||||
DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */,
|
||||
DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */,
|
||||
);
|
||||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -899,8 +907,10 @@
|
|||
DDC4D568275499A500A4208E /* Persistence.swift in Sources */,
|
||||
DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */,
|
||||
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
|
||||
DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */,
|
||||
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
|
||||
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
|
||||
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */,
|
||||
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */,
|
||||
DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */,
|
||||
DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */,
|
||||
|
|
@ -1003,6 +1013,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DDC94FC229CE063B0082EA6E /* BatteryLevel.swift in Sources */,
|
||||
DDDE5A1129AFE69700490C6C /* MeshActivityAttributes.swift in Sources */,
|
||||
DDDE59FB29AF163D00490C6C /* WidgetsLiveActivity.swift in Sources */,
|
||||
DDDE59FD29AF163D00490C6C /* Widgets.swift in Sources */,
|
||||
|
|
@ -1188,7 +1199,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.1.3;
|
||||
MARKETING_VERSION = 2.1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1222,7 +1233,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.1.3;
|
||||
MARKETING_VERSION = 2.1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1469,6 +1480,7 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */,
|
||||
DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */,
|
||||
DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */,
|
||||
DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */,
|
||||
|
|
@ -1479,7 +1491,7 @@
|
|||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */;
|
||||
currentVersion = DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */;
|
||||
name = Meshtastic.xcdatamodeld;
|
||||
path = Meshtastic/Meshtastic.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -279,6 +279,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
|
||||
if nodeInfo.position.longitudeI > 0 || nodeInfo.position.latitudeI > 0 && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) {
|
||||
let position = PositionEntity(context: context)
|
||||
position.latest = true
|
||||
position.seqNo = Int32(nodeInfo.position.seqNumber)
|
||||
position.latitudeI = nodeInfo.position.latitudeI
|
||||
position.longitudeI = nodeInfo.position.longitudeI
|
||||
|
|
@ -486,6 +487,9 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
|
||||
}
|
||||
|
||||
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getRingtoneResponse(adminMessage.getRingtoneResponse) {
|
||||
let ringtone = adminMessage.getRingtoneResponse
|
||||
upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: Int64(packet.from), context: context)
|
||||
} else {
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Admin App \(try! packet.decoded.jsonString())")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV9.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV10.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,313 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<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" optional="YES" attributeType="Boolean" 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="psk" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
|
||||
<fetchedProperty name="allPrivateMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="channel == $FETCH_SOURCE.index && toUser == nil AND isEmoji == false"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</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="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"/>
|
||||
<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"/>
|
||||
<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="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="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="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="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="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="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<fetchedProperty name="tapbacks" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="messageId"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<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="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="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="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="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<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="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="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="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
|
||||
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
|
||||
<relationship name="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="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="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="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="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="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
|
||||
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="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"/>
|
||||
<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="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="shortName" attributeType="String"/>
|
||||
<attribute name="userId" attributeType="String"/>
|
||||
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
|
||||
<fetchedProperty name="adminMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND isEmoji == false AND admin = true"/>
|
||||
</fetchedProperty>
|
||||
<fetchedProperty name="allMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false"/>
|
||||
</fetchedProperty>
|
||||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
//
|
||||
// Persistence.swift
|
||||
// CoreDataSample
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 11/28/21.
|
||||
// Copyright(c) Garth Vander Houwen 11/28/21.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
//
|
||||
// PersistenceEntityExtenstion.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 11/28/21.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
|
|
|
|||
|
|
@ -535,19 +535,27 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu
|
|||
let newPositionConfig = PositionConfigEntity(context: context)
|
||||
newPositionConfig.smartPositionEnabled = config.positionBroadcastSmartEnabled
|
||||
newPositionConfig.deviceGpsEnabled = config.gpsEnabled
|
||||
newPositionConfig.rxGpio = Int32(config.rxGpio)
|
||||
newPositionConfig.txGpio = Int32(config.txGpio)
|
||||
newPositionConfig.fixedPosition = config.fixedPosition
|
||||
newPositionConfig.gpsUpdateInterval = Int32(config.gpsUpdateInterval)
|
||||
newPositionConfig.gpsAttemptTime = Int32(config.gpsAttemptTime)
|
||||
newPositionConfig.positionBroadcastSeconds = Int32(config.positionBroadcastSecs)
|
||||
newPositionConfig.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs)
|
||||
newPositionConfig.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance)
|
||||
newPositionConfig.positionFlags = Int32(config.positionFlags)
|
||||
fetchedNode[0].positionConfig = newPositionConfig
|
||||
} else {
|
||||
fetchedNode[0].positionConfig?.smartPositionEnabled = config.positionBroadcastSmartEnabled
|
||||
fetchedNode[0].positionConfig?.deviceGpsEnabled = config.gpsEnabled
|
||||
fetchedNode[0].positionConfig?.rxGpio = Int32(config.rxGpio)
|
||||
fetchedNode[0].positionConfig?.txGpio = Int32(config.txGpio)
|
||||
fetchedNode[0].positionConfig?.fixedPosition = config.fixedPosition
|
||||
fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.gpsUpdateInterval)
|
||||
fetchedNode[0].positionConfig?.gpsAttemptTime = Int32(config.gpsAttemptTime)
|
||||
fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.positionBroadcastSecs)
|
||||
fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs)
|
||||
fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance)
|
||||
fetchedNode[0].positionConfig?.positionFlags = Int32(config.positionFlags)
|
||||
}
|
||||
do {
|
||||
|
|
@ -699,6 +707,45 @@ func upsertExternalNotificationModuleConfigPacket(config: Meshtastic.ModuleConfi
|
|||
}
|
||||
}
|
||||
|
||||
func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.ringtone.config %@", comment: "RTTTL Ringtone config received: %@"), String(nodeNum))
|
||||
MeshLogger.log("⛰️ \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else {
|
||||
return
|
||||
}
|
||||
// Found a node, save RTTTL Config
|
||||
if !fetchedNode.isEmpty {
|
||||
if fetchedNode[0].rtttlConfig == nil {
|
||||
let newRtttlConfig = RTTTLConfigEntity(context: context)
|
||||
newRtttlConfig.ringtone = ringtone
|
||||
fetchedNode[0].rtttlConfig = newRtttlConfig
|
||||
} else {
|
||||
fetchedNode[0].rtttlConfig?.ringtone = ringtone
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated RTTTL Ringtone Config for node number: \(String(nodeNum))")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data RtttlConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save RTTTL Ringtone Config")
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data RtttlConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, nodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.mqtt.config %@", comment: "MQTT module config received: %@"), String(nodeNum))
|
||||
|
|
@ -719,7 +766,7 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no
|
|||
let newMQTTConfig = MQTTConfigEntity(context: context)
|
||||
newMQTTConfig.enabled = config.enabled
|
||||
newMQTTConfig.address = config.address
|
||||
newMQTTConfig.address = config.username
|
||||
newMQTTConfig.username = config.username
|
||||
newMQTTConfig.password = config.password
|
||||
newMQTTConfig.encryptionEnabled = config.encryptionEnabled
|
||||
newMQTTConfig.jsonEnabled = config.jsonEnabled
|
||||
|
|
@ -727,7 +774,7 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no
|
|||
} else {
|
||||
fetchedNode[0].mqttConfig?.enabled = config.enabled
|
||||
fetchedNode[0].mqttConfig?.address = config.address
|
||||
fetchedNode[0].mqttConfig?.address = config.username
|
||||
fetchedNode[0].mqttConfig?.username = config.username
|
||||
fetchedNode[0].mqttConfig?.password = config.password
|
||||
fetchedNode[0].mqttConfig?.encryptionEnabled = config.encryptionEnabled
|
||||
fetchedNode[0].mqttConfig?.jsonEnabled = config.jsonEnabled
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// UserEntityExtension.swift
|
||||
// MeshtasticApple
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 6/3/22.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -354,6 +354,14 @@ struct Config {
|
|||
/// (Re)define GPS_TX_PIN for your board.
|
||||
var txGpio: UInt32 = 0
|
||||
|
||||
///
|
||||
/// The minimum distance in meters traveled (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled
|
||||
var broadcastSmartMinimumDistance: UInt32 = 0
|
||||
|
||||
///
|
||||
/// The minumum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled
|
||||
var broadcastSmartMinimumIntervalSecs: UInt32 = 0
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
///
|
||||
|
|
@ -1669,6 +1677,8 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
7: .standard(proto: "position_flags"),
|
||||
8: .standard(proto: "rx_gpio"),
|
||||
9: .standard(proto: "tx_gpio"),
|
||||
10: .standard(proto: "broadcast_smart_minimum_distance"),
|
||||
11: .standard(proto: "broadcast_smart_minimum_interval_secs"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -1686,6 +1696,8 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }()
|
||||
case 8: try { try decoder.decodeSingularUInt32Field(value: &self.rxGpio) }()
|
||||
case 9: try { try decoder.decodeSingularUInt32Field(value: &self.txGpio) }()
|
||||
case 10: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumDistance) }()
|
||||
case 11: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumIntervalSecs) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -1719,6 +1731,12 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if self.txGpio != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.txGpio, fieldNumber: 9)
|
||||
}
|
||||
if self.broadcastSmartMinimumDistance != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.broadcastSmartMinimumDistance, fieldNumber: 10)
|
||||
}
|
||||
if self.broadcastSmartMinimumIntervalSecs != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.broadcastSmartMinimumIntervalSecs, fieldNumber: 11)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -1732,6 +1750,8 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if lhs.positionFlags != rhs.positionFlags {return false}
|
||||
if lhs.rxGpio != rhs.rxGpio {return false}
|
||||
if lhs.txGpio != rhs.txGpio {return false}
|
||||
if lhs.broadcastSmartMinimumDistance != rhs.broadcastSmartMinimumDistance {return false}
|
||||
if lhs.broadcastSmartMinimumIntervalSecs != rhs.broadcastSmartMinimumIntervalSecs {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1587,6 +1587,10 @@ struct NodeInfo {
|
|||
/// Clears the value of `deviceMetrics`. Subsequent reads from it will return its default value.
|
||||
mutating func clearDeviceMetrics() {self._deviceMetrics = nil}
|
||||
|
||||
///
|
||||
/// local channel index we heard that node on. Only populated if its not the default channel.
|
||||
var channel: UInt32 = 0
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
|
@ -3181,6 +3185,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
4: .same(proto: "snr"),
|
||||
5: .standard(proto: "last_heard"),
|
||||
6: .standard(proto: "device_metrics"),
|
||||
7: .same(proto: "channel"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -3195,6 +3200,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
case 4: try { try decoder.decodeSingularFloatField(value: &self.snr) }()
|
||||
case 5: try { try decoder.decodeSingularFixed32Field(value: &self.lastHeard) }()
|
||||
case 6: try { try decoder.decodeSingularMessageField(value: &self._deviceMetrics) }()
|
||||
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.channel) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -3223,6 +3229,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
try { if let v = self._deviceMetrics {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
|
||||
} }()
|
||||
if self.channel != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.channel, fieldNumber: 7)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -3233,6 +3242,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
if lhs.snr != rhs.snr {return false}
|
||||
if lhs.lastHeard != rhs.lastHeard {return false}
|
||||
if lhs._deviceMetrics != rhs._deviceMetrics {return false}
|
||||
if lhs.channel != rhs.channel {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,20 @@ struct Connect: View {
|
|||
@State var presentingSwitchPreferredPeripheral = false
|
||||
@State var selectedPeripherialId = ""
|
||||
|
||||
init () {
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.getNotificationSettings(completionHandler: { (settings) in
|
||||
if settings.authorizationStatus == .notDetermined {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
|
||||
if success {
|
||||
print("Notifications are all set!")
|
||||
} else if let error = error {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
var body: some View {
|
||||
|
||||
NavigationStack {
|
||||
|
|
@ -90,7 +104,7 @@ struct Connect: View {
|
|||
#endif
|
||||
}
|
||||
} label: {
|
||||
Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play")
|
||||
Label("mesh.live.activity", systemImage: liveActivityStarted ? "stop" : "play")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -279,18 +293,9 @@ struct Connect: View {
|
|||
.onAppear(perform: {
|
||||
self.bleManager.context = context
|
||||
self.bleManager.userSettings = userSettings
|
||||
|
||||
// Ask for notification permission
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
|
||||
if success {
|
||||
print("Notifications are all set!")
|
||||
} else if let error = error {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
#if canImport(ActivityKit)
|
||||
#if canImport(ActivityKit)
|
||||
func startNodeActivity() {
|
||||
if #available(iOS 16.2, *) {
|
||||
liveActivityStarted = true
|
||||
|
|
@ -330,29 +335,7 @@ struct Connect: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
func postNotification() {
|
||||
let timerSeconds = 60
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Mesh Live Activity Over"
|
||||
content.body = "Your timed mesh live activity is over."
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(timerSeconds), repeats: false)
|
||||
let uuidString = UUID().uuidString
|
||||
let request = UNNotificationRequest(identifier: uuidString,
|
||||
content: content, trigger: trigger)
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.add(request) { (error) in
|
||||
if error != nil {
|
||||
// Handle any errors.
|
||||
print("Error posting local notification: \(error?.localizedDescription ?? "no description")")
|
||||
} else {
|
||||
print("Posted local notification.")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
func didDismissSheet() {
|
||||
bleManager.disconnectPeripheral(reconnect: false)
|
||||
|
|
|
|||
|
|
@ -12,45 +12,51 @@ func degreesToRadians(_ number: Double) -> Double {
|
|||
}
|
||||
|
||||
struct MapViewSwiftUI: UIViewRepresentable {
|
||||
|
||||
|
||||
var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void
|
||||
var onWaypointEdit: (_ waypointId: Int ) -> Void
|
||||
let mapView = MKMapView()
|
||||
let lineColors: [UIColor] = [UIColor.systemIndigo, UIColor.yellow, UIColor.white, UIColor.red, UIColor.purple, UIColor.orange, UIColor.magenta, UIColor.lightGray, UIColor.green, UIColor.gray, UIColor.systemMint, UIColor.darkGray, UIColor.cyan, UIColor.brown, UIColor.blue, UIColor.black, UIColor.systemPink,
|
||||
UIColor.systemTeal]
|
||||
// Parameters
|
||||
let positions: [PositionEntity]
|
||||
let waypoints: [WaypointEntity]
|
||||
let mapViewType: MKMapType
|
||||
let userTrackingMode: MKUserTrackingMode
|
||||
let showNodeHistory: Bool
|
||||
let showRouteLines: Bool
|
||||
let colors: [UIColor] = [UIColor.systemIndigo, UIColor.orange, UIColor.green, UIColor.brown, UIColor.purple, UIColor.systemMint, UIColor.cyan, UIColor.magenta, UIColor.systemPink, UIColor.blue]
|
||||
|
||||
@AppStorage("meshMapRecentering") private var recenter: Bool = false
|
||||
// Offline Maps
|
||||
// make this view dependent on the UserDefault that is updated when importing a new map file
|
||||
// Offline Map Tiles
|
||||
@AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0
|
||||
@State private var loadedLastUpdatedLocalMapFile = 0
|
||||
var customMapOverlay: CustomMapOverlay?
|
||||
@State private var presentCustomMapOverlayHash: CustomMapOverlay?
|
||||
|
||||
|
||||
func makeUIView(context: Context) -> MKMapView {
|
||||
// Map View Parameters
|
||||
mapView.mapType = mapViewType
|
||||
mapView.addAnnotations(waypoints)
|
||||
// Do the initial map centering
|
||||
let latest = positions
|
||||
.filter { $0.latest == true }
|
||||
.sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 }
|
||||
let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003)
|
||||
let center = LocationHelper.currentLocation
|
||||
let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation
|
||||
let region = MKCoordinateRegion(center: center, span: span)
|
||||
mapView.addAnnotations(showNodeHistory ? positions : latest)
|
||||
mapView.setRegion(region, animated: true)
|
||||
// Set user (phone gps) tracking options
|
||||
let latest = positions.filter { $0.latest == true }
|
||||
mapView.setUserTrackingMode(userTrackingMode, animated: true)
|
||||
if userTrackingMode == MKUserTrackingMode.none {
|
||||
if latest.count == 1 {
|
||||
mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: false)
|
||||
} else {
|
||||
mapView.fitAllAnnotations()
|
||||
}
|
||||
mapView.showsUserLocation = false
|
||||
} else {
|
||||
mapView.showsUserLocation = true
|
||||
}
|
||||
mapView.addAnnotations(showNodeHistory ? positions : latest)
|
||||
// Other MKMapView Settings
|
||||
mapView.preferredConfiguration.elevationStyle = .realistic// .flat
|
||||
mapView.isPitchEnabled = true
|
||||
|
|
@ -60,14 +66,14 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
mapView.showsBuildings = true
|
||||
mapView.showsScale = true
|
||||
mapView.showsTraffic = true
|
||||
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
// Show the default always visible compass and the mac only controls
|
||||
mapView.showsCompass = true
|
||||
mapView.showsZoomControls = true
|
||||
mapView.showsPitchControl = true
|
||||
#else
|
||||
|
||||
|
||||
#if os(iOS)
|
||||
// Hide the default compass that only appears when you are not going north and instead always show the compass in the bottom right corner of the map
|
||||
mapView.showsCompass = false
|
||||
|
|
@ -78,19 +84,21 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
compassButton.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true
|
||||
compassButton.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -25).isActive = true
|
||||
#endif
|
||||
|
||||
|
||||
#endif
|
||||
mapView.delegate = context.coordinator
|
||||
return mapView
|
||||
}
|
||||
|
||||
|
||||
func updateUIView(_ mapView: MKMapView, context: Context) {
|
||||
|
||||
|
||||
mapView.mapType = mapViewType
|
||||
|
||||
|
||||
if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile {
|
||||
mapView.removeOverlays(mapView.overlays)
|
||||
if self.customMapOverlay != nil {
|
||||
|
||||
|
||||
let fileManager = FileManager.default
|
||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path
|
||||
|
|
@ -109,12 +117,15 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
// DispatchQueue.main.async {
|
||||
let latest = positions
|
||||
.filter { $0.latest == true }
|
||||
.sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 }
|
||||
|
||||
let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count)
|
||||
print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)")
|
||||
mapView.removeAnnotations(mapView.annotations)
|
||||
mapView.addAnnotations(waypoints)
|
||||
if showRouteLines {
|
||||
// Remove all existing PolyLine Overlays
|
||||
for overlay in mapView.overlays {
|
||||
|
|
@ -125,7 +136,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
var lineIndex = 0
|
||||
for position in latest {
|
||||
|
||||
let nodePositions = positions.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 }
|
||||
let nodePositions = positions.filter { $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 }
|
||||
let lineCoords = nodePositions.map ({
|
||||
(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate!
|
||||
|
|
@ -134,47 +145,36 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))"
|
||||
mapView.addOverlay(polyline)
|
||||
lineIndex += 1
|
||||
// There are 10 colors for lines, start over if we are at index 10
|
||||
if lineIndex > 9 {
|
||||
// There are 18 colors for lines, start over if we are at index 17
|
||||
if lineIndex > 17 {
|
||||
lineIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let annotationCount = waypoints.count + positions.count
|
||||
if annotationCount != mapView.annotations.count {
|
||||
mapView.removeAnnotations(mapView.annotations)
|
||||
mapView.addAnnotations(waypoints)
|
||||
mapView.setUserTrackingMode(userTrackingMode, animated: true)
|
||||
|
||||
if userTrackingMode == MKUserTrackingMode.none {
|
||||
mapView.showsUserLocation = false
|
||||
mapView.addAnnotations(showNodeHistory ? positions : latest)
|
||||
if recenter {
|
||||
if showRouteLines || showNodeHistory {
|
||||
mapView.fit(annotations: showNodeHistory ? positions : positions, andShow: false)
|
||||
} else {
|
||||
mapView.fitAllAnnotations()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Centering Done by tracking mode
|
||||
mapView.addAnnotations(showNodeHistory ? positions : latest)
|
||||
mapView.showsUserLocation = true
|
||||
if userTrackingMode == MKUserTrackingMode.none {
|
||||
mapView.showsUserLocation = false
|
||||
mapView.addAnnotations(showNodeHistory ? positions : latest)
|
||||
if recenter {
|
||||
mapView.fit(annotations:showNodeHistory || showRouteLines ? positions : latest, andShow: false)
|
||||
}
|
||||
} else {
|
||||
// Centering Done by tracking mode
|
||||
mapView.addAnnotations(showNodeHistory ? positions : latest)
|
||||
mapView.showsUserLocation = true
|
||||
}
|
||||
}
|
||||
mapView.setUserTrackingMode(userTrackingMode, animated: true)
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
func makeCoordinator() -> MapCoordinator {
|
||||
return Coordinator(self)
|
||||
}
|
||||
|
||||
|
||||
final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate {
|
||||
|
||||
|
||||
var parent: MapViewSwiftUI
|
||||
var longPressRecognizer = UILongPressGestureRecognizer()
|
||||
|
||||
|
||||
init(_ parent: MapViewSwiftUI) {
|
||||
self.parent = parent
|
||||
super.init()
|
||||
|
|
@ -184,16 +184,16 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
self.longPressRecognizer.delegate = self
|
||||
self.parent.mapView.addGestureRecognizer(longPressRecognizer)
|
||||
}
|
||||
|
||||
|
||||
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
||||
|
||||
|
||||
switch annotation {
|
||||
case let positionAnnotation as PositionEntity:
|
||||
let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0)
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID )
|
||||
annotationView.tag = -1
|
||||
annotationView.canShowCallout = true
|
||||
|
||||
|
||||
if positionAnnotation.latest {
|
||||
annotationView.markerTintColor = .systemRed
|
||||
annotationView.displayPriority = .required
|
||||
|
|
@ -216,7 +216,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
let distanceFormatter = MKDistanceFormatter()
|
||||
subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n"
|
||||
if positionAnnotation.nodePosition?.metadata != nil {
|
||||
|
||||
|
||||
if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client ||
|
||||
DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute ||
|
||||
DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient {
|
||||
|
|
@ -230,7 +230,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
} else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor {
|
||||
annotationView.glyphImage = UIImage(systemName: "sensor")
|
||||
}
|
||||
|
||||
|
||||
let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3))
|
||||
if pf.contains(.Satsinview) {
|
||||
subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n"
|
||||
|
|
@ -239,7 +239,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n"
|
||||
}
|
||||
if pf.contains(.Heading) {
|
||||
|
||||
|
||||
if parent.userTrackingMode != MKUserTrackingMode.followWithHeading {
|
||||
annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading))))
|
||||
subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n"
|
||||
|
|
@ -255,7 +255,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
}
|
||||
subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n"
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
// node metadata is nil
|
||||
annotationView.glyphImage = UIImage(systemName: "flipphone")
|
||||
|
|
@ -316,23 +316,23 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
|
||||
// Only Allow Edit for waypoint annotations with a id
|
||||
if view.tag > 0 {
|
||||
parent.onWaypointEdit(view.tag)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) {
|
||||
|
||||
|
||||
if gesture.state != UIGestureRecognizer.State.ended {
|
||||
return
|
||||
} else if gesture.state != UIGestureRecognizer.State.began {
|
||||
|
||||
|
||||
// Screen Position - CGPoint
|
||||
let location = longPressRecognizer.location(in: self.parent.mapView)
|
||||
|
||||
|
||||
// Map Coordinate - CLLocationCoordinate2D
|
||||
let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView)
|
||||
let annotation = MKPointAnnotation()
|
||||
|
|
@ -343,9 +343,9 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
parent.onLongPress(coordinate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
||||
|
||||
|
||||
if let tileOverlay = overlay as? MKTileOverlay {
|
||||
return MKTileOverlayRenderer(tileOverlay: tileOverlay)
|
||||
} else {
|
||||
|
|
@ -354,26 +354,26 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
let titleString = routePolyline.title ?? "None-0"
|
||||
let index = Int(titleString.components(separatedBy: "-").last ?? "0")
|
||||
let renderer = MKPolylineRenderer(polyline: routePolyline)
|
||||
renderer.strokeColor = parent.colors[index ?? 0]
|
||||
renderer.lineWidth = 5
|
||||
renderer.strokeColor = parent.lineColors[index ?? 0]
|
||||
renderer.lineWidth = 8
|
||||
return renderer
|
||||
}
|
||||
return MKOverlayRenderer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// is supposed to be located in the folder with the map name
|
||||
public struct DefaultTile: Hashable {
|
||||
let tileName: String
|
||||
let tileType: String
|
||||
|
||||
|
||||
public init(tileName: String, tileType: String) {
|
||||
self.tileName = tileName
|
||||
self.tileType = tileType
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct CustomMapOverlay: Equatable, Hashable {
|
||||
let mapName: String
|
||||
let tileType: String
|
||||
|
|
@ -381,7 +381,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
var minimumZoomLevel: Int?
|
||||
var maximumZoomLevel: Int?
|
||||
let defaultTile: DefaultTile?
|
||||
|
||||
|
||||
public init(
|
||||
mapName: String,
|
||||
tileType: String,
|
||||
|
|
@ -397,7 +397,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
self.maximumZoomLevel = maximumZoomLevel
|
||||
self.defaultTile = defaultTile
|
||||
}
|
||||
|
||||
|
||||
public init?(
|
||||
mapName: String?,
|
||||
tileType: String,
|
||||
|
|
@ -417,15 +417,15 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
self.defaultTile = defaultTile
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class CustomMapOverlaySource: MKTileOverlay {
|
||||
|
||||
|
||||
// requires folder: tiles/{mapName}/z/y/y,{tileType}
|
||||
private var parent: MapViewSwiftUI
|
||||
private let mapName: String
|
||||
private let tileType: String
|
||||
private let defaultTile: DefaultTile?
|
||||
|
||||
|
||||
public init(
|
||||
parent: MapViewSwiftUI,
|
||||
mapName: String,
|
||||
|
|
@ -438,7 +438,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
self.defaultTile = defaultTile
|
||||
super.init(urlTemplate: "")
|
||||
}
|
||||
|
||||
|
||||
public override func url(forTilePath path: MKTileOverlayPath) -> URL {
|
||||
if let tileUrl = Bundle.main.url(
|
||||
forResource: "\(path.y)",
|
||||
|
|
@ -460,31 +460,4 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// public struct Overlay {
|
||||
//
|
||||
// public static func == (lhs: MapViewSwiftUI.Overlay, rhs: MapViewSwiftUI.Overlay) -> Bool {
|
||||
// // maybe to use in the future for comparison of full array
|
||||
// lhs.shape.coordinate.latitude == rhs.shape.coordinate.latitude &&
|
||||
// lhs.shape.coordinate.longitude == rhs.shape.coordinate.longitude &&
|
||||
// lhs.fillColor == rhs.fillColor
|
||||
// }
|
||||
//
|
||||
// var shape: MKOverlay
|
||||
// var fillColor: UIColor?
|
||||
// var strokeColor: UIColor?
|
||||
// var lineWidth: CGFloat
|
||||
//
|
||||
// public init(
|
||||
// shape: MKOverlay,
|
||||
// fillColor: UIColor? = nil,
|
||||
// strokeColor: UIColor? = nil,
|
||||
// lineWidth: CGFloat = 0
|
||||
// ) {
|
||||
// self.shape = shape
|
||||
// self.fillColor = fillColor
|
||||
// self.strokeColor = strokeColor
|
||||
// self.lineWidth = lineWidth
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ struct ChannelMessageList: View {
|
|||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) {
|
||||
if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false, smartPosition: false) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
|
|
@ -339,7 +339,7 @@ struct ChannelMessageList: View {
|
|||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) {
|
||||
if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false, smartPosition: false) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ struct UserMessageList: View {
|
|||
let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp))
|
||||
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
|
||||
if ackDate >= sixMonthsAgo! {
|
||||
Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss a"))").foregroundColor(.gray)
|
||||
Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss.SSSS a"))").foregroundColor(.gray)
|
||||
} else {
|
||||
Text("unknown.age").font(.caption2).foregroundColor(.gray)
|
||||
}
|
||||
|
|
@ -328,7 +328,7 @@ struct UserMessageList: View {
|
|||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(destNum: user.num, wantResponse: true) {
|
||||
if bleManager.sendPosition(destNum: user.num, wantResponse: true, smartPosition: false) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
|
|
@ -345,7 +345,7 @@ struct UserMessageList: View {
|
|||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(destNum: user.num, wantResponse: true) {
|
||||
if bleManager.sendPosition(destNum: user.num, wantResponse: true, smartPosition: false) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ struct DeviceMetricsLog: View {
|
|||
ForEach(node.telemetries?.reversed() as? [TelemetryEntity] ?? [], id: \.self) { (dm: TelemetryEntity) in
|
||||
if dm.metricsType == 0 {
|
||||
GridRow {
|
||||
if dm.batteryLevel == 0 {
|
||||
if dm.batteryLevel == 111 {
|
||||
Text("USB")
|
||||
.font(.caption)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ struct NodeDetail: View {
|
|||
if node.positions?.count ?? 0 > 0 {
|
||||
ZStack {
|
||||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) }
|
||||
let lastTenThousand = Array(positionArray.prefix(10000))
|
||||
// let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) }
|
||||
ZStack {
|
||||
MapViewSwiftUI(onLongPress: { coord in
|
||||
waypointCoordinate = coord
|
||||
|
|
@ -71,7 +72,7 @@ struct NodeDetail: View {
|
|||
editingWaypoint = wpId
|
||||
presentingWaypointForm = true
|
||||
}
|
||||
}, positions: todaysPositions, waypoints: Array(waypoints),
|
||||
}, positions: lastTenThousand, waypoints: Array(waypoints),
|
||||
mapViewType: mapType,
|
||||
userTrackingMode: MKUserTrackingMode.none,
|
||||
showNodeHistory: meshMapShowNodeHistory,
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ struct NodeMap: View {
|
|||
waypoints: Array(waypoints),
|
||||
mapViewType: mapType,
|
||||
userTrackingMode: userTrackingMode,
|
||||
showNodeHistory: meshMapShowNodeHistory,
|
||||
showNodeHistory: meshMapShowNodeHistory,
|
||||
showRouteLines: meshMapShowRouteLines,
|
||||
customMapOverlay: self.customMapOverlay
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ struct DisplayConfig: View {
|
|||
@State var screenCarouselInterval = 0
|
||||
@State var gpsFormat = 0
|
||||
@State var compassNorthTop = false
|
||||
@State var wakeOnTapOrMotion = false
|
||||
@State var flipScreen = false
|
||||
@State var oledType = 0
|
||||
@State var displayMode = 0
|
||||
|
|
@ -72,7 +73,14 @@ struct DisplayConfig: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("The compass heading on the screen outside of the circle will always point north.")
|
||||
.font(.caption)
|
||||
|
||||
|
||||
Toggle(isOn: $wakeOnTapOrMotion) {
|
||||
Label("Wake Screen on tap or motion", systemImage: "gyroscope")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Requires that there be an accelerometer on your device.")
|
||||
.font(.caption)
|
||||
|
||||
Toggle(isOn: $flipScreen) {
|
||||
|
||||
Label("Flip Screen", systemImage: "pip.swap")
|
||||
|
|
@ -151,6 +159,7 @@ struct DisplayConfig: View {
|
|||
dc.screenOnSecs = UInt32(screenOnSeconds)
|
||||
dc.autoScreenCarouselSecs = UInt32(screenCarouselInterval)
|
||||
dc.compassNorthTop = compassNorthTop
|
||||
dc.wakeOnTapOrMotion = wakeOnTapOrMotion
|
||||
dc.flipScreen = flipScreen
|
||||
dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue()
|
||||
dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue()
|
||||
|
|
@ -202,6 +211,11 @@ struct DisplayConfig: View {
|
|||
if newCompassNorthTop != node!.displayConfig!.compassNorthTop { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: wakeOnTapOrMotion) { newWakeOnTapOrMotion in
|
||||
if node != nil && node!.displayConfig != nil {
|
||||
if newWakeOnTapOrMotion != node!.displayConfig!.wakeOnTapOrMotion { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: gpsFormat) { newGpsFormat in
|
||||
if node != nil && node!.displayConfig != nil {
|
||||
if newGpsFormat != node!.displayConfig!.gpsFormat { hasChanges = true }
|
||||
|
|
@ -229,6 +243,7 @@ struct DisplayConfig: View {
|
|||
self.screenOnSeconds = Int(node?.displayConfig?.screenOnSeconds ?? 0)
|
||||
self.screenCarouselInterval = Int(node?.displayConfig?.screenCarouselInterval ?? 0)
|
||||
self.compassNorthTop = node?.displayConfig?.compassNorthTop ?? false
|
||||
self.wakeOnTapOrMotion = node?.displayConfig?.wakeOnTapOrMotion ?? false
|
||||
self.flipScreen = node?.displayConfig?.flipScreen ?? false
|
||||
self.oledType = Int(node?.displayConfig?.oledType ?? 0)
|
||||
self.displayMode = Int(node?.displayConfig?.displayMode ?? 0)
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@ struct MQTTConfig: View {
|
|||
.onChange(of: address, perform: { _ in
|
||||
let totalBytes = address.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 30 {
|
||||
let firstNBytes = Data(username.utf8.prefix(30))
|
||||
if totalBytes > 62 {
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
address = maxBytesString
|
||||
|
|
|
|||
142
Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift
Normal file
142
Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
//
|
||||
// RingtoneConfig.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 3/25/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RtttlConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
var node: NodeInfoEntity?
|
||||
|
||||
@State private var isPresentingSaveConfirm: Bool = false
|
||||
@State var hasChanges = false
|
||||
@State var ringtone: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.rtttlConfig == nil {
|
||||
Text("RTTTL Ringtone config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setRtttLConfigValue()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
|
||||
HStack {
|
||||
Label("ringtone", systemImage: "music.quarternote.3")
|
||||
TextField("Ringtone Transfer Language", text: $ringtone, axis: .vertical)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: ringtone, perform: { _ in
|
||||
|
||||
let totalBytes = ringtone.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 228 {
|
||||
|
||||
let firstNBytes = Data(ringtone.utf8.prefix(228))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the ringtone back to the last place where it was the right size
|
||||
ringtone = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
Text("Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.rtttlConfig == nil)
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
.confirmationDialog(
|
||||
"are.you.sure",
|
||||
isPresented: $isPresentingSaveConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
let nodeName = node?.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")
|
||||
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
|
||||
Button(buttonText) {
|
||||
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if connectedNode != nil {
|
||||
let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
hasChanges = false
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
message: {
|
||||
Text("config.save.confirm")
|
||||
}
|
||||
.navigationTitle("ringtone.config")
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
|
||||
})
|
||||
.onAppear {
|
||||
self.bleManager.context = context
|
||||
setRtttLConfigValue()
|
||||
// Need to request a Rtttl Config from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && (node?.rtttlConfig == nil || node?.rtttlConfig?.ringtone?.count ?? 0 == 0) {
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestRtttlConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: ringtone) { newRingtone in
|
||||
if node != nil && node!.rtttlConfig != nil {
|
||||
if newRingtone != node!.rtttlConfig!.ringtone { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setRtttLConfigValue() {
|
||||
self.ringtone = node?.rtttlConfig?.ringtone ?? ""
|
||||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
|
@ -36,10 +36,14 @@ struct PositionConfig: View {
|
|||
|
||||
@State var smartPositionEnabled = true
|
||||
@State var deviceGpsEnabled = true
|
||||
@State var rxGpio = 0
|
||||
@State var txGpio = 0
|
||||
@State var fixedPosition = false
|
||||
@State var gpsUpdateInterval = 0
|
||||
@State var gpsAttemptTime = 0
|
||||
@State var positionBroadcastSeconds = 0
|
||||
@State var broadcastSmartMinimumDistance = 0
|
||||
@State var broadcastSmartMinimumIntervalSecs = 0
|
||||
@State var positionFlags = 3
|
||||
|
||||
/// Position Flags
|
||||
|
|
@ -98,54 +102,53 @@ struct PositionConfig: View {
|
|||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("Device GPS")) {
|
||||
Toggle(isOn: $deviceGpsEnabled) {
|
||||
Label("Device GPS Enabled", systemImage: "location")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
if deviceGpsEnabled {
|
||||
Picker("Update Interval", selection: $gpsUpdateInterval) {
|
||||
ForEach(GpsUpdateIntervals.allCases) { ui in
|
||||
Text(ui.description)
|
||||
}
|
||||
}
|
||||
Text("How often should we try to get a GPS position.")
|
||||
.font(.caption)
|
||||
Picker("Attempt Time", selection: $gpsAttemptTime) {
|
||||
ForEach(GpsAttemptTimes.allCases) { at in
|
||||
Text(at.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("How long should we try to get our position during each GPS Update Interval attempt?")
|
||||
.font(.caption)
|
||||
} else {
|
||||
Toggle(isOn: $fixedPosition) {
|
||||
Label("Fixed Position", systemImage: "location.square.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If enabled your current location will be set as a fixed position.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Position Packet")) {
|
||||
|
||||
Toggle(isOn: $smartPositionEnabled) {
|
||||
|
||||
Label("Smart Position Broadcast", systemImage: "location.fill.viewfinder")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Picker("Position Broadcast Interval", selection: $positionBroadcastSeconds) {
|
||||
ForEach(UpdateIntervals.allCases) { at in
|
||||
Text(at.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
Text("We should send our position this often (but only if it has changed significantly)")
|
||||
Text("The maximum interval that can elapse without a node sending a position")
|
||||
.font(.caption)
|
||||
|
||||
Toggle(isOn: $smartPositionEnabled) {
|
||||
|
||||
Label("Smart Position Broadcast", systemImage: "location.fill.viewfinder")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
if smartPositionEnabled {
|
||||
Picker("Minimum Broadcast Interval", selection: $broadcastSmartMinimumIntervalSecs) {
|
||||
ForEach(UpdateIntervals.allCases) { at in
|
||||
Text(at.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("The fastest that position updates will be sent if the minimum distance has been satisfied")
|
||||
.font(.caption)
|
||||
|
||||
Picker("Minimum Distance", selection: $broadcastSmartMinimumDistance) {
|
||||
ForEach(10..<151) {
|
||||
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
||||
if $0.isMultiple(of: 5) {
|
||||
Text("\($0)")
|
||||
.tag($0)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("The minimum distance change in meters to be considered for a smart position broadcast.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
Section(header: Text("Position Flags")) {
|
||||
|
||||
|
|
@ -209,6 +212,59 @@ struct PositionConfig: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
Section(header: Text("Device GPS")) {
|
||||
Toggle(isOn: $deviceGpsEnabled) {
|
||||
Label("Device GPS Enabled", systemImage: "location")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
if deviceGpsEnabled {
|
||||
Picker("Update Interval", selection: $gpsUpdateInterval) {
|
||||
ForEach(GpsUpdateIntervals.allCases) { ui in
|
||||
Text(ui.description)
|
||||
}
|
||||
}
|
||||
Text("How often should we try to get a GPS position.")
|
||||
.font(.caption)
|
||||
Picker("Attempt Time", selection: $gpsAttemptTime) {
|
||||
ForEach(GpsAttemptTimes.allCases) { at in
|
||||
Text(at.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("How long should we try to get our position during each GPS Update Interval attempt?")
|
||||
.font(.caption)
|
||||
|
||||
Picker("GPS Receive GPIO", selection: $rxGpio) {
|
||||
ForEach(0..<40) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
Picker("GPS Transmit GPIO", selection: $txGpio) {
|
||||
ForEach(0..<40) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
} else {
|
||||
Toggle(isOn: $fixedPosition) {
|
||||
Label("Fixed Position", systemImage: "location.square.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If enabled your current location will be set as a fixed position.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.positionConfig == nil)
|
||||
|
||||
|
|
@ -232,7 +288,7 @@ struct PositionConfig: View {
|
|||
Button(buttonText) {
|
||||
|
||||
if fixedPosition {
|
||||
_ = bleManager.sendPosition(destNum: node!.num, wantResponse: true)
|
||||
_ = bleManager.sendPosition(destNum: node!.num, wantResponse: true, smartPosition: false)
|
||||
}
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
|
||||
|
|
@ -244,6 +300,8 @@ struct PositionConfig: View {
|
|||
pc.gpsUpdateInterval = UInt32(gpsUpdateInterval)
|
||||
pc.gpsAttemptTime = UInt32(gpsAttemptTime)
|
||||
pc.positionBroadcastSecs = UInt32(positionBroadcastSeconds)
|
||||
pc.broadcastSmartMinimumIntervalSecs = UInt32(broadcastSmartMinimumIntervalSecs)
|
||||
pc.broadcastSmartMinimumDistance = UInt32(broadcastSmartMinimumDistance)
|
||||
var pf: PositionFlags = []
|
||||
if includeAltitude { pf.insert(.Altitude) }
|
||||
if includeAltitudeMsl { pf.insert(.AltitudeMsl) }
|
||||
|
|
@ -296,6 +354,16 @@ struct PositionConfig: View {
|
|||
if newDeviceGps != node!.positionConfig!.deviceGpsEnabled { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: rxGpio) { newRxGpio in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newRxGpio != node!.positionConfig!.rxGpio { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: txGpio) { newTxGpio in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newTxGpio != node!.positionConfig!.txGpio { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: gpsAttemptTime) { newGpsAttemptTime in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newGpsAttemptTime != node!.positionConfig!.gpsAttemptTime { hasChanges = true }
|
||||
|
|
@ -321,6 +389,16 @@ struct PositionConfig: View {
|
|||
if newPositionBroadcastSeconds != node!.positionConfig!.positionBroadcastSeconds { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: broadcastSmartMinimumIntervalSecs) { newBroadcastSmartMinimumIntervalSecs in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newBroadcastSmartMinimumIntervalSecs != node!.positionConfig!.broadcastSmartMinimumIntervalSecs { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: broadcastSmartMinimumDistance) { newBroadcastSmartMinimumDistance in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newBroadcastSmartMinimumDistance != node!.positionConfig!.broadcastSmartMinimumDistance { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: includeAltitude) { altFlag in
|
||||
let pf = PositionFlags(rawValue: self.positionFlags)
|
||||
let existingValue = pf.contains(.Altitude)
|
||||
|
|
@ -382,10 +460,14 @@ struct PositionConfig: View {
|
|||
|
||||
self.smartPositionEnabled = node?.positionConfig?.smartPositionEnabled ?? true
|
||||
self.deviceGpsEnabled = node?.positionConfig?.deviceGpsEnabled ?? true
|
||||
self.rxGpio = Int(node?.positionConfig?.rxGpio ?? 0)
|
||||
self.txGpio = Int(node?.positionConfig?.txGpio ?? 0)
|
||||
self.fixedPosition = node?.positionConfig?.fixedPosition ?? false
|
||||
self.gpsUpdateInterval = Int(node?.positionConfig?.gpsUpdateInterval ?? 30)
|
||||
self.gpsAttemptTime = Int(node?.positionConfig?.gpsAttemptTime ?? 30)
|
||||
self.positionBroadcastSeconds = Int(node?.positionConfig?.positionBroadcastSeconds ?? 900)
|
||||
self.broadcastSmartMinimumIntervalSecs = Int(node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 30)
|
||||
self.broadcastSmartMinimumDistance = Int(node?.positionConfig?.broadcastSmartMinimumDistance ?? 50)
|
||||
self.positionFlags = Int(node?.positionConfig?.positionFlags ?? 3)
|
||||
|
||||
let pf = PositionFlags(rawValue: self.positionFlags)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ struct Settings: View {
|
|||
case externalNotificationConfig
|
||||
case mqttConfig
|
||||
case rangeTestConfig
|
||||
case ringtoneConfig
|
||||
case serialConfig
|
||||
case telemetryConfig
|
||||
case meshLog
|
||||
|
|
@ -229,7 +230,14 @@ struct Settings: View {
|
|||
.symbolRenderingMode(.hierarchical)
|
||||
Text("range.test")
|
||||
}
|
||||
.tag(SettingsSidebar.rangeTestConfig)
|
||||
NavigationLink {
|
||||
RtttlConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "music.note.list")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("ringtone")
|
||||
}
|
||||
.tag(SettingsSidebar.ringtoneConfig)
|
||||
|
||||
NavigationLink {
|
||||
SerialConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ struct ShareChannels: View {
|
|||
.font(.headline)
|
||||
.padding(.bottom)
|
||||
Text("Primary Channel").font(.title2)
|
||||
Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled.")
|
||||
Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled. If you don't share your primary channel, the first channel will become the primary channel on the other network and will allow communication with your mesh on the group channel.")
|
||||
.font(.callout)
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
Text("Admin Channel").font(.title2)
|
||||
|
|
|
|||
78
Widgets/BatteryLevel.swift
Normal file
78
Widgets/BatteryLevel.swift
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// BatteryLevel.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright Garth Vander Houwen 3/24/23.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
struct BatteryIcon: View {
|
||||
var batteryLevel: Int32?
|
||||
var font: Font
|
||||
var color: Color
|
||||
|
||||
var body: some View {
|
||||
|
||||
if batteryLevel == 100 {
|
||||
|
||||
Image(systemName: "battery.100.bolt")
|
||||
.font(font)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else if batteryLevel! < 100 && batteryLevel! > 74 {
|
||||
|
||||
Image(systemName: "battery.75")
|
||||
.font(font)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else if batteryLevel! < 75 && batteryLevel! > 49 {
|
||||
|
||||
Image(systemName: "battery.50")
|
||||
.font(font)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else if batteryLevel! < 50 && batteryLevel! > 14 {
|
||||
|
||||
Image(systemName: "battery.25")
|
||||
.font(font)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else if batteryLevel! < 15 && batteryLevel! > 0 {
|
||||
|
||||
Image(systemName: "battery.0")
|
||||
.font(font)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
||||
} else if batteryLevel! == 0 {
|
||||
|
||||
Image(systemName: "battery.0")
|
||||
.font(font)
|
||||
.foregroundColor(.red)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else if batteryLevel! > 100 {
|
||||
|
||||
Image(systemName: "powerplug")
|
||||
.font(font)
|
||||
.foregroundColor(color)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BatteryIcon_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BatteryIcon(batteryLevel: 111, font: .title2, color: Color.accentColor)
|
||||
.previewLayout(.fixed(width: 75, height: 75))
|
||||
BatteryIcon(batteryLevel: 100, font: .title2, color: Color.accentColor)
|
||||
.previewLayout(.fixed(width: 75, height: 75))
|
||||
BatteryIcon(batteryLevel: 99, font: .title2, color: Color.accentColor)
|
||||
.previewLayout(.fixed(width: 75, height: 75))
|
||||
BatteryIcon(batteryLevel: 74, font: .title2, color: Color.accentColor)
|
||||
.previewLayout(.fixed(width: 75, height: 75))
|
||||
BatteryIcon(batteryLevel: 49, font: .title2, color: Color.accentColor)
|
||||
.previewLayout(.fixed(width: 75, height: 75))
|
||||
BatteryIcon(batteryLevel: 14, font: .title2, color: Color.accentColor)
|
||||
.previewLayout(.fixed(width: 75, height: 75))
|
||||
}
|
||||
}
|
||||
|
|
@ -20,22 +20,60 @@ struct WidgetsLiveActivity: Widget {
|
|||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
NodeInfoView(nodeName: context.attributes.name, timerRange: context.state.timerRange, channelUtilization: context.state.channelUtilization, airtime: context.state.airtime, batteryLevel: context.state.batteryLevel)
|
||||
.tint(Color("LightIndigo"))
|
||||
.padding(.top)
|
||||
Text("Network")
|
||||
.font(.headline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
.padding(.top, 10)
|
||||
Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%")
|
||||
.font(.headline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
Text("\(String(format: "Airtime: %.2f", context.state.airtime))%")
|
||||
.font(.headline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
Spacer()
|
||||
}
|
||||
// Expanded UI goes here. Compose the expanded UI through
|
||||
// various regions, like leading/trailing/center/bottom
|
||||
DynamicIslandExpandedRegion(.trailing, priority: 1) {
|
||||
HStack(alignment: .lastTextBaseline) {
|
||||
|
||||
Spacer()
|
||||
TimerView(timerRange: context.state.timerRange)
|
||||
.tint(Color("LightIndigo"))
|
||||
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("Plugged In")
|
||||
.font(.title3)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing, priority: 1) {
|
||||
TimerView(timerRange: context.state.timerRange)
|
||||
.tint(Color("LightIndigo"))
|
||||
|
||||
}
|
||||
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)
|
||||
.fixedSize()
|
||||
}
|
||||
|
||||
} compactLeading: {
|
||||
Image("logo-black")
|
||||
|
|
@ -65,7 +103,7 @@ struct WidgetsLiveActivity: Widget {
|
|||
|
||||
@available(iOS 16.2, *)
|
||||
struct WidgetsLiveActivity_Previews: PreviewProvider {
|
||||
static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "Meshtastic 8E6G")
|
||||
static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G")
|
||||
static let state = MeshActivityAttributes.ContentState(
|
||||
timerRange: Date.now...Date(timeIntervalSinceNow: 3600), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39)
|
||||
|
||||
|
|
@ -108,7 +146,31 @@ struct LiveActivityView: View {
|
|||
Spacer()
|
||||
NodeInfoView(nodeName: nodeName, timerRange: timerRange, channelUtilization: channelUtilization, airtime: airtime, batteryLevel: batteryLevel)
|
||||
Spacer()
|
||||
TimerView(timerRange: timerRange)
|
||||
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])
|
||||
|
|
@ -134,38 +196,47 @@ struct NodeInfoView: View {
|
|||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.tint)
|
||||
Text("\(String(format: "Ch. Util: %.2f", channelUtilization))%")
|
||||
.font(.subheadline)
|
||||
.font(.headline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
Text("\(String(format: "Airtime: %.2f", airtime))%")
|
||||
.font(.subheadline)
|
||||
.font(.headline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
if batteryLevel < 101 {
|
||||
Text("Battery Level: \(batteryLevel > 0 ? String(batteryLevel) : "< 1")%")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
} else {
|
||||
Text("Plugged In")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
let now = Date()
|
||||
Text("Last Heard: \(now.formatted())")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
HStack {
|
||||
|
||||
if timerRange.upperBound >= now {
|
||||
Text("Next Update:")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
Text(timerInterval: timerRange, countsDown: true)
|
||||
.monospacedDigit()
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.tint)
|
||||
} else {
|
||||
Text("Not Connected")
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.tint)
|
||||
}
|
||||
}
|
||||
Text(Date().formatted())
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -177,18 +248,11 @@ struct TimerView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
Text("NEXT")
|
||||
Text("NEXT UPDATE")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.5 : 1.0)
|
||||
.fixedSize()
|
||||
Text("UPDATE")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.5 : 1.0)
|
||||
.fixedSize()
|
||||
Text(timerInterval: timerRange, countsDown: true)
|
||||
.monospacedDigit()
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
@ -215,7 +279,6 @@ struct ExpandedTrailingView: View {
|
|||
|
||||
var body: some View {
|
||||
HStack(alignment: .lastTextBaseline) {
|
||||
|
||||
Spacer()
|
||||
TimerView(timerRange: timerInterval)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@
|
|||
"map.usertrackingmode.none"="None";
|
||||
"map.usertrackingmode.follow"="Follow";
|
||||
"map.usertrackingmode.followwithheading"="Follow with heading";
|
||||
"mesh.live.activity"="Mesh Live Activity";
|
||||
"mesh.log"="Mesh Log";
|
||||
"mesh.log.bluetooth.config %@"="Bluetooth Konfiguration empfangen: %@";
|
||||
"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@";
|
||||
|
|
@ -165,6 +166,7 @@
|
|||
"mesh.log.position.config %@"="Positions Konfiguration empfangen: %@";
|
||||
"mesh.log.position.received %@"="Positionspaket empfangen von Node: %@";
|
||||
"mesh.log.rangetest.config %@"="Range Test Modul konfiguration empfangen: %@";
|
||||
"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@";
|
||||
"mesh.log.routing.message %@ %@"="Routing empfangen für RequestID: %@ Ack Status: %@";
|
||||
"mesh.log.serial.config %@"="Serial Modul Konfiguration empfangen: %@";
|
||||
"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@";
|
||||
|
|
@ -212,6 +214,8 @@
|
|||
"reply"="Antworten";
|
||||
"received.ack"="Empfangsbestätigung";
|
||||
"received.ack.real"="Recipient Ack";
|
||||
"ringtone"="Ringtone";
|
||||
"ringtone.config"="Ringtone Config";
|
||||
"routing.acknowledged"="Bestätigt";
|
||||
"routing.noroute"="Keine Route";
|
||||
"routing.gotnak"="Negative Empfangsbestätigung empfangen";
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@
|
|||
"clear.log"="Clear Log";
|
||||
"close"="Close";
|
||||
"config.save.confirm"="After config values save the node will reboot.";
|
||||
"connected.radio"="Connected Radio";
|
||||
"communicating"="Communicating with device. .";
|
||||
"connected.radio"="Connected Radio";
|
||||
"connected"="Currently Connected";
|
||||
"connecting"="Connecting . .";
|
||||
"contacts"="Contacts";
|
||||
|
|
@ -144,6 +144,7 @@
|
|||
"map.usertrackingmode.follow"="Follow";
|
||||
"map.usertrackingmode.followwithheading"="Follow with heading";
|
||||
"map.usertrackingmode.none"="None";
|
||||
"mesh.live.activity"="Mesh Live Activity";
|
||||
"mesh.log"="Mesh Log";
|
||||
"mesh.log.bluetooth.config %@"="Bluetooth config received: %@";
|
||||
"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@";
|
||||
|
|
@ -165,6 +166,7 @@
|
|||
"mesh.log.position.config %@"="Positon config received: %@";
|
||||
"mesh.log.position.received %@"="Position Packet received from node: %@";
|
||||
"mesh.log.rangetest.config %@"="Range Test module config received: %@";
|
||||
"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@";
|
||||
"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@";
|
||||
"mesh.log.serial.config %@"="Serial module config received: %@";
|
||||
"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@";
|
||||
|
|
@ -212,6 +214,8 @@
|
|||
"reboot.node"="Reboot node?";
|
||||
"received.ack"="Received Ack";
|
||||
"received.ack.real"="Recipient Ack";
|
||||
"ringtone"="Ringtone";
|
||||
"ringtone.config"="Ringtone Config";
|
||||
"routing.acknowledged"="Acknowledged";
|
||||
"routing.noroute"="No Route";
|
||||
"routing.gotnak"="Received a negative acknowledgment";
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@
|
|||
"map.usertrackingmode.none"="None";
|
||||
"map.usertrackingmode.follow"="Follow";
|
||||
"map.usertrackingmode.followwithheading"="Follow with heading";
|
||||
"mesh.live.activity"="Mesh Live Activity";
|
||||
"mesh.log"="Mesh 日志";
|
||||
"mesh.log.bluetooth.config %@"="Bluetooth config received: %@";
|
||||
"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@";
|
||||
|
|
@ -165,6 +166,7 @@
|
|||
"mesh.log.position.config %@"="Positon config received: %@";
|
||||
"mesh.log.position.received %@"="Position Packet received from node: %@";
|
||||
"mesh.log.rangetest.config %@"="Range Test module config received: %@";
|
||||
"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@";
|
||||
"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@";
|
||||
"mesh.log.serial.config %@"="Serial module config received: %@";
|
||||
"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@";
|
||||
|
|
@ -212,6 +214,8 @@
|
|||
"reboot.node"="重启节点?";
|
||||
"received.ack"="收到确认";
|
||||
"received.ack.real"="收件人确认";
|
||||
"ringtone"="Ringtone";
|
||||
"ringtone.config"="Ringtone Config";
|
||||
"routing.acknowledged"="确认";
|
||||
"routing.noroute"="找不到目标";
|
||||
"routing.gotnak"="收到否认";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue