Merge pull request #341 from meshtastic/2.1.4_Working_Changes

2.1.4 working changes
This commit is contained in:
Garth Vander Houwen 2023-03-29 11:12:57 -07:00 committed by GitHub
commit f2d17d496a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1429 additions and 596 deletions

View file

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

View file

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

View file

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

View file

@ -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 &amp;&amp; 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>

View file

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

View file

@ -1,3 +1,10 @@
//
// PersistenceEntityExtenstion.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 11/28/21.
//
import CoreData
import CoreLocation
import MapKit

View file

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

View file

@ -1,6 +1,6 @@
//
// UserEntityExtension.swift
// MeshtasticApple
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 6/3/22.
//

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -77,7 +77,7 @@ struct NodeMap: View {
waypoints: Array(waypoints),
mapViewType: mapType,
userTrackingMode: userTrackingMode,
showNodeHistory: meshMapShowNodeHistory,
showNodeHistory: meshMapShowNodeHistory,
showRouteLines: meshMapShowRouteLines,
customMapOverlay: self.customMapOverlay
)

View file

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

View file

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -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"="收到否认";