Merge pull request #579 from meshtastic/2.3.3_Working_Changes

2.3.3 working changes
This commit is contained in:
Garth Vander Houwen 2024-04-03 14:23:51 -07:00 committed by GitHub
commit 76bcfa28be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 1042 additions and 247 deletions

View file

@ -374,6 +374,7 @@
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = "<group>"; };
DD964FC32974767D007C176F /* MapViewFitExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewFitExtension.swift; sourceTree = "<group>"; };
DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = "<group>"; };
DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV32.xcdatamodel; sourceTree = "<group>"; };
DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = "<group>"; };
DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = "<group>"; };
@ -1610,7 +1611,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.3.2;
MARKETING_VERSION = 2.3.3;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1644,7 +1645,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.3.2;
MARKETING_VERSION = 2.3.3;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1766,7 +1767,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.2;
MARKETING_VERSION = 2.3.3;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1799,7 +1800,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.2;
MARKETING_VERSION = 2.3.3;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1910,6 +1911,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */,
DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */,
DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */,
DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */,
@ -1942,7 +1944,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */;
currentVersion = DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -1,4 +1,5 @@
{
"originHash" : "e9855e3a299c14a10f11ee0b8f29e4170b09548533939361223a0f50e7caac8c",
"pins" : [
{
"identity" : "cocoamqtt",
@ -46,5 +47,5 @@
}
}
],
"version" : 2
"version" : 3
}

View file

@ -56,11 +56,12 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable {
case twoHundredMiles = 321869
case fiveHundredMiles = 804672
case oneThousandMiles = 1609000
case fifteenHundredMiles = 2414016
case twentyFiveHundredMiles = 4023360
var id: Double { self.rawValue }
var description: String {
let distanceFormatter = MKDistanceFormatter()
return "up to \(distanceFormatter.string(fromDistance: Double(self.rawValue))) away"
return String.localizedStringWithFormat("nodelist.filter.distance %@".localized, distanceFormatter.string(fromDistance: Double(self.rawValue)))
}
}

View file

@ -14,7 +14,7 @@ extension MyInfoEntity {
}
var unreadMessages: Int {
let unreadMessages = messageList.filter{ ($0 as AnyObject).read == false }
let unreadMessages = messageList.filter{ ($0 as AnyObject).read == false && ($0 as AnyObject).isEmoji == false }
return unreadMessages.count
}
}

View file

@ -14,13 +14,12 @@ extension PositionEntity {
static func allPositionsFetchRequest() -> NSFetchRequest<PositionEntity> {
let request: NSFetchRequest<PositionEntity> = PositionEntity.fetchRequest()
request.fetchLimit = 200
//request.fetchBatchSize = 1
request.fetchLimit = 100
request.returnsObjectsAsFaults = false
request.includesSubentities = true
request.returnsDistinctResults = true
request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate)
let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true")
let pointOfInterest = LocationHelper.currentLocation

View file

@ -8,13 +8,26 @@
import Foundation
extension URL {
func regularFileAllocatedSize() throws -> UInt64 {
let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys)
guard resourceValues.isRegularFile ?? false else {
return 0
func regularFileAllocatedSize() throws -> UInt64 {
let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys)
guard resourceValues.isRegularFile ?? false else {
return 0
}
return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0)
}
subscript(queryParam: String) -> String? {
guard let url = URLComponents(string: self.absoluteString) else { return nil }
if let parameters = url.queryItems {
return parameters.first(where: { $0.name == queryParam })?.value
} else if let paramPairs = url.fragment?.components(separatedBy: "?").last?.components(separatedBy: "&") {
for pair in paramPairs where pair.contains(queryParam) {
return pair.components(separatedBy: "=").last
}
return nil
} else {
return nil
}
}
return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0)
}
}

View file

@ -759,19 +759,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].storeForwardConfig?.enabled == true {
wantStoreAndForwardPackets = true;
}
if fetchedNodeInfo.count == 1 {
if !(fetchedNodeInfo[0].user?.vip ?? false) {
fetchedNodeInfo[0].user?.vip = true
do {
try context!.save()
} catch {
context!.rollback()
let nsError = error as NSError
print("💥 Core Data error. Error: \(nsError)")
}
}
}
} catch {
print("Failed to find a node info for the connected node")
@ -1317,18 +1304,36 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return 0
}
public func saveChannelSet(base64UrlString: String) -> Bool {
public func saveChannelSet(base64UrlString: String, addChannels: Bool = false) -> Bool {
if isConnected {
// Before we get started delete the existing channels from the myNodeInfo
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num))
tryClearExistingChannels()
var i: Int32 = 0
// Before we get started delete the existing channels from the myNodeInfo
if !addChannels {
tryClearExistingChannels()
} else {
// We are trying to add a channel so lets get the last index
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num))
do {
let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as? [MyInfoEntity] ?? []
if fetchedMyInfo.count == 1 {
if addChannels {
i = Int32(fetchedMyInfo[0].channels?.count ?? -1)
// Bail out if the index is negative or bigger than our max of 8
if i < 0 || i > 8 {
return false
}
}
}
} catch {
print("Failed to find a node MyInfo to save these channels to")
}
}
let decodedString = base64UrlString.base64urlToBase64()
if let decodedData = Data(base64Encoded: decodedString) {
do {
let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData)
var i: Int32 = 0
for cs in channelSet.settings {
var chan = Channel()
if i == 0 {
@ -1385,7 +1390,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
let logString = String.localizedStringWithFormat("mesh.log.lora.config.sent %@".localized, String(connectedPeripheral.num))
MeshLogger.log("📻 \(logString)")
}
return true
if self.connectedPeripheral != nil {
self.sendWantConfig()
return true
}
} catch {
return false
}
@ -1448,6 +1458,54 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return false
}
public func setFavoriteNode(node: NodeInfoEntity, connectedNodeNum: Int64) -> Bool {
var adminPacket = AdminMessage()
adminPacket.setFavoriteNode = UInt32(node.num)
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(connectedNodeNum)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected{
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
return true
}
return false
}
public func removeFavoriteNode(node: NodeInfoEntity, connectedNodeNum: Int64) -> Bool {
var adminPacket = AdminMessage()
adminPacket.removeFavoriteNode = UInt32(node.num)
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(connectedNodeNum)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected{
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
return true
}
return false
}
public func saveLicensedUser(ham: HamParameters, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
var adminPacket = AdminMessage()
adminPacket.setHamMode = ham

View file

@ -293,6 +293,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
} else {
let newUser = UserEntity(context: context)
newUser.num = Int64(nodeInfo.num)
newUser.numString = String(nodeInfo.num)
let userId = String(format:"%2X", nodeInfo.num)
newUser.userId = "!\(userId)"
let last4 = String(userId.suffix(4))
@ -357,6 +358,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
}
fetchedNode[0].user!.userId = nodeInfo.user.id
fetchedNode[0].user!.num = Int64(nodeInfo.num)
fetchedNode[0].user!.numString = String(nodeInfo.num)
fetchedNode[0].user!.longName = nodeInfo.user.longName
fetchedNode[0].user!.shortName = nodeInfo.user.shortName
fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed

View file

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

View file

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

View file

@ -18,6 +18,7 @@ struct MeshtasticAppleApp: App {
@State var saveChannels = false
@State var incomingUrl: URL?
@State var channelSettings: String?
@State var addChannels = false
@StateObject var appState = AppState.shared
var body: some Scene {
@ -26,7 +27,7 @@ struct MeshtasticAppleApp: App {
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(bleManager)
.sheet(isPresented: $saveChannels) {
SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", bleManager: bleManager)
SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
@ -36,9 +37,13 @@ struct MeshtasticAppleApp: App {
self.incomingUrl = userActivity.webpageURL
if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#")) != nil {
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
self.channelSettings = components.last!
guard let cs = components.last!.components(separatedBy: "?").first else {
return
}
self.channelSettings = cs
self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false
print("Add Channel \(self.addChannels)")
}
self.saveChannels = true
print("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")")

View file

@ -236,7 +236,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries)
}
if nodeInfoMessage.hasUser {
fetchedNode[0].user!.vip = nodeInfoMessage.isFavorite
/// Seeing Some crashes here ?
fetchedNode[0].user!.userId = nodeInfoMessage.user.id
fetchedNode[0].user!.num = Int64(nodeInfoMessage.num)

View file

@ -350,7 +350,7 @@ struct AdminMessage {
}
///
/// Clear fixed position coordinates and then set position.fixed_position = false
/// Clear fixed position coordinates and then set position.fixed_position = false
var removeFixedPosition: Bool {
get {
if case .removeFixedPosition(let v)? = payloadVariant {return v}
@ -547,7 +547,7 @@ struct AdminMessage {
/// Set fixed position data on the node and then set the position.fixed_position = true
case setFixedPosition(Position)
///
/// Clear fixed position coordinates and then set position.fixed_position = false
/// Clear fixed position coordinates and then set position.fixed_position = false
case removeFixedPosition(Bool)
///
/// Begins an edit transaction for config, module config, owner, and channel settings changes

View file

@ -255,7 +255,7 @@ extension MemberRole: CaseIterable {
#endif // swift(>=4.2)
///
/// Packets for the official ATAK Plugin
/// Packets for the official ATAK Plugin
struct TAKPacket {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for

View file

@ -226,14 +226,14 @@ struct Config {
///
/// Description: Broadcasts GPS position packets as priority.
/// Technical Details: Position Mesh packets will be prioritized higher and sent more frequently by default.
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
/// send position, and then sleep for position.position_broadcast_secs seconds.
case tracker // = 5
///
/// Description: Broadcasts telemetry packets as priority.
/// Technical Details: Telemetry Mesh packets will be prioritized higher and sent more frequently by default.
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
/// send environment telemetry, and then sleep for telemetry.environment_update_interval seconds.
case sensor // = 6
@ -249,12 +249,12 @@ struct Config {
/// Technical Details: Used for nodes that "only speak when spoken to"
/// Turns all of the routine broadcasts but allows for ad-hoc communication
/// Still rebroadcasts, but with local only rebroadcast mode (known meshes only)
/// Can be used for clandestine operation or to dramatically reduce airtime / power consumption
/// Can be used for clandestine operation or to dramatically reduce airtime / power consumption
case clientHidden // = 8
///
/// Description: Broadcasts location as message to default channel regularly for to assist with device recovery.
/// Technical Details: Used to automatically send a text message to the mesh
/// Technical Details: Used to automatically send a text message to the mesh
/// with the current position of the device on a frequent interval:
/// "I'm lost! Position: lat / long"
case lostAndFound // = 9

View file

@ -1565,8 +1565,8 @@ struct MeshPacket {
set {_uniqueStorage()._viaMqtt = newValue}
}
///
/// Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header.
///
/// Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header.
/// When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled.
var hopStart: UInt32 {
get {return _storage._hopStart}
@ -2606,7 +2606,7 @@ struct DeviceMetadata {
init() {}
}
///
///
/// A heartbeat message is sent to the node from the client to keep the connection alive.
/// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI.
struct Heartbeat {

View file

@ -370,7 +370,7 @@ struct Telemetry {
}
///
/// Power Metrics
/// Power Metrics
var powerMetrics: PowerMetrics {
get {
if case .powerMetrics(let v)? = variant {return v}
@ -392,7 +392,7 @@ struct Telemetry {
/// Air quality metrics
case airQualityMetrics(AirQualityMetrics)
///
/// Power Metrics
/// Power Metrics
case powerMetrics(PowerMetrics)
#if !swift(>=4.1)

View file

@ -9,6 +9,7 @@ struct ConnectedDevice: View {
var bluetoothOn: Bool
var deviceConnected: Bool
var name: String
var mqttProxyEnabled: Bool = false
var mqttProxyConnected: Bool = false
var phoneOnly: Bool = false
@ -18,11 +19,11 @@ struct ConnectedDevice: View {
if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly {
if bluetoothOn {
if deviceConnected && mqttProxyConnected {
if mqttProxyConnected {
if deviceConnected && (mqttProxyEnabled || mqttProxyConnected) {
if (mqttProxyConnected || mqttProxyEnabled) {
Image(systemName: "iphone.gen3.radiowaves.left.and.right.circle.fill")
.imageScale(.large)
.foregroundColor(.green)
.foregroundColor(mqttProxyConnected ? .green : .gray)
.symbolRenderingMode(.hierarchical)
}
}

View file

@ -85,7 +85,7 @@ func getRssiColor(rssi: Int32) -> Color {
if rssi > -115 {
/// Good
return .green
} else if rssi > -115 && rssi < -120 {
} else if rssi > -120 {
/// Fair
return .yellow
} else if rssi > -126 {

View file

@ -156,7 +156,10 @@ struct ChannelMessageList: View {
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?",
mqttProxyEnabled: channel.uplinkEnabled || channel.downlinkEnabled,
mqttProxyConnected: channel.uplinkEnabled || channel.downlinkEnabled ? bleManager.mqttProxyConnected : false
)
}
}
}

View file

@ -17,23 +17,20 @@ struct UserList: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State private var searchText = ""
@State private var viaLora = true
@State private var viaMqtt = true
@State private var isOnline = false
@State private var isFavorite = false
@State private var distanceFilter = false
@State private var maxDistance: Double = 800000
@State private var hopsAway: Int = -1
@State private var deviceRole: Int = -1
@State var isEditingFilters = false
var usersQuery: Binding<String> {
Binding {
searchText
} set: { newValue in
searchText = newValue
/// Case Insensitive Search Text Predicates
let searchPredicates = ["userId", "hwModel", "longName", "shortName"].map { property in
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
}
/// Create a compound predicate using each text search predicate as an OR
let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates)
users.nsPredicate = newValue.isEmpty ? nil : textSearchPredicate
}
}
@FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), NSSortDescriptor(key: "vip", ascending: false), NSSortDescriptor(key: "longName", ascending: true)],
sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false),
NSSortDescriptor(key: "userNode.favorite", ascending: false),
NSSortDescriptor(key: "longName", ascending: true)],
animation: .default)
private var users: FetchedResults<UserEntity>
@ -71,7 +68,7 @@ struct UserList: View {
Text(user.longName ?? "unknown".localized)
.font(.headline)
Spacer()
if user.vip {
if (user.userNode?.favorite ?? false) {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
@ -108,15 +105,29 @@ struct UserList: View {
.frame(height: 62)
.contextMenu {
Button {
user.vip = !user.vip
if node != nil && !(user.userNode?.favorite ?? false) {
let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num))
if success {
user.userNode?.favorite = !(user.userNode?.favorite ?? true)
print("Favorited a node")
}
} else {
let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num))
if success {
user.userNode?.favorite = !(user.userNode?.favorite ?? true)
print("Favorited a node")
}
}
context.refresh(user, mergeChanges: true)
do {
try context.save()
} catch {
context.rollback()
print("💥 Save User VIP Error")
print("💥 Save Node Favorite Error")
}
} label: {
Label(user.vip ? "Un-Favorite" : "Favorite", systemImage: user.vip ? "star.slash.fill" : "star.fill")
Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill")
}
Button {
user.mute = !user.mute
@ -156,9 +167,142 @@ struct UserList: View {
}
.listStyle(.plain)
.navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count - 1)))
.searchable(text: usersQuery, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact")
.sheet(isPresented: $isEditingFilters) {
NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole)
}
.onChange(of: searchText) { _ in
searchUserList()
}
.onChange(of: viaLora) { _ in
if !viaLora && !viaMqtt {
viaMqtt = true
}
searchUserList()
}
.onChange(of: viaMqtt) { _ in
if !viaLora && !viaMqtt {
viaLora = true
}
searchUserList()
}
.onChange(of: deviceRole) { _ in
searchUserList()
}
.onChange(of: hopsAway) { _ in
searchUserList()
}
.onChange(of: isOnline) { _ in
searchUserList()
}
.onChange(of: isFavorite) { _ in
searchUserList()
}
.onChange(of: maxDistance) { _ in
searchUserList()
}
.onChange(of: distanceFilter) { _ in
searchUserList()
}
.onAppear {
if self.bleManager.context == nil {
self.bleManager.context = context
}
searchUserList()
}
.safeAreaInset(edge: .bottom, alignment: .trailing) {
HStack {
Button(action: {
withAnimation {
isEditingFilters = !isEditingFilters
}
}) {
Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill")
.padding(.vertical, 5)
}
.tint(Color(UIColor.secondarySystemBackground))
.foregroundColor(.accentColor)
.buttonStyle(.borderedProminent)
}
.controlSize(.regular)
.padding(5)
}
.padding(.bottom, 5)
.searchable(text: $searchText, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact")
.disableAutocorrection(true)
.scrollDismissesKeyboard(.immediately)
}
}
private func searchUserList() {
/// Case Insensitive Search Text Predicates
let searchPredicates = ["userId", "numString", "hwModel", "longName", "shortName"].map { property in
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
}
/// Create a compound predicate using each text search preicate as an OR
let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates)
/// Create an array of predicates to hold our AND predicates
var predicates: [NSPredicate] = []
/// Mqtt
if !(viaLora && viaMqtt) {
if viaLora {
let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO")
predicates.append(loraPredicate)
} else {
let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES")
predicates.append(mqttPredicate)
}
}
/// Role
if deviceRole > -1 {
let rolePredicate = NSPredicate(format: "role == %i", Int32(deviceRole))
predicates.append(rolePredicate)
}
/// Hops Away
if hopsAway > 0 {
let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway))
predicates.append(hopsAwayPredicate)
}
/// Online
if isOnline {
let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
predicates.append(isOnlinePredicate)
}
/// Favorites
if isFavorite {
let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES")
predicates.append(isFavoritePredicate)
}
/// Distance
if distanceFilter {
let pointOfInterest = LocationHelper.currentLocation
if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude {
let D: Double = maxDistance * 1.1
let R: Double = 6371009
let meanLatitidue = pointOfInterest.latitude * .pi / 180
let deltaLatitude = D / R * 180 / .pi
let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi
let minLatitude: Double = pointOfInterest.latitude - deltaLatitude
let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude
let minLongitude: Double = pointOfInterest.longitude - deltaLongitude
let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude
let distancePredicate = NSPredicate(format: "(SUBQUERY(userNode.positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude,minLatitude, maxLatitude)
predicates.append(distancePredicate)
}
}
if predicates.count > 0 || !searchText.isEmpty {
if !searchText.isEmpty {
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates])
} else {
users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
}
} else {
users.nsPredicate = nil
}
}
}

View file

@ -93,7 +93,7 @@ struct MeshMapContent: MapContent {
}
/// Node History and Route Lines for favorites
if position.nodePosition?.user?.vip ?? false {
if position.nodePosition?.favorite ?? false {
if showRouteLines {
let nodePositions = Array(position.nodePosition!.positions!) as! [PositionEntity]
let routeCoords = nodePositions.compactMap({(pos) -> CLLocationCoordinate2D in
@ -112,7 +112,7 @@ struct MeshMapContent: MapContent {
}
if showNodeHistory {
ForEach(Array(position.nodePosition!.positions!) as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
if mappin.latest == false && mappin.nodePosition?.user?.vip ?? false {
if mappin.latest == false && mappin.nodePosition?.favorite ?? false {
let pf = PositionFlags(rawValue: Int(mappin.nodePosition?.metadata?.positionFlags ?? 771))
let headingDegrees = Angle.degrees(Double(mappin.heading))
Annotation("", coordinate: mappin.coordinate) {

View file

@ -128,13 +128,13 @@ struct NodeMapContent: MapContent {
}
}
}
// .tag(position.time)
.tag(position.time)
.annotationTitles(.automatic)
.annotationSubtitles(.automatic)
}
/// Node History
if showNodeHistory {
if position.latest == false && position.nodePosition?.user?.vip ?? false {
if position.latest == false && position.nodePosition?.favorite ?? false {
let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771))
let headingDegrees = Angle.degrees(Double(position.heading))
Annotation("", coordinate: position.coordinate) {

View file

@ -138,7 +138,7 @@ struct NodeMapSwiftUI: View {
}
}
}
.safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) {
.safeAreaInset(edge: .bottom, alignment: .trailing) {
HStack {
Button(action: {
withAnimation {

View file

@ -162,7 +162,7 @@ struct PositionPopover: View {
Spacer()
VStack (alignment: .center) {
if position.nodePosition != nil {
if position.nodePosition?.user?.vip ?? false {
if position.nodePosition?.favorite ?? false {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
.symbolRenderingMode(.hierarchical)

View file

@ -11,9 +11,11 @@ import SwiftUI
struct NodeListFilter: View {
@Environment(\.dismiss) private var dismiss
/// Filters
var filterTitle = "Node Filters"
@Binding var viaLora: Bool
@Binding var viaMqtt: Bool
@Binding var isOnline: Bool
@Binding var isFavorite: Bool
@Binding var distanceFilter: Bool
@Binding var maximumDistance: Double
@Binding var hopsAway: Int
@ -23,7 +25,7 @@ struct NodeListFilter: View {
NavigationStack {
Form {
Section(header: Text("Node Filters")) {
Section(header: Text(filterTitle)) {
Toggle(isOn: $viaLora) {
Label {
@ -48,7 +50,7 @@ struct NodeListFilter: View {
Toggle(isOn: $isOnline) {
Label {
Text("Online Only")
Text("Online")
} icon: {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
@ -58,29 +60,43 @@ struct NodeListFilter: View {
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.listRowSeparator(.visible)
// Toggle(isOn: $distanceFilter) {
//
// Label {
// Text("Distance")
// } icon: {
// Image(systemName: "map")
// }
// }
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
//
// .listRowSeparator(distanceFilter ? .hidden : .visible)
// if distanceFilter {
// HStack {
// Label("Show nodes", systemImage: "lines.measurement.horizontal")
// Picker("", selection: $maximumDistance) {
// ForEach(MeshMapDistances.allCases) { di in
// Text(di.description)
// .tag(di.id)
// }
// }
// .pickerStyle(DefaultPickerStyle())
// }
// }
Toggle(isOn: $isFavorite) {
Label {
Text("Favorites")
} icon: {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
.symbolRenderingMode(.hierarchical)
}
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.listRowSeparator(.visible)
Toggle(isOn: $distanceFilter) {
Label {
Text("Distance")
} icon: {
Image(systemName: "map")
}
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.listRowSeparator(distanceFilter ? .hidden : .visible)
if distanceFilter {
HStack {
Label("Show nodes", systemImage: "lines.measurement.horizontal")
Picker("", selection: $maximumDistance) {
ForEach(MeshMapDistances.allCases) { di in
Text(di.description)
.tag(di.id)
}
}
.pickerStyle(DefaultPickerStyle())
}
}
HStack {
Label("Hops Away", systemImage: "hare")
Picker("", selection: $hopsAway) {
@ -125,7 +141,7 @@ struct NodeListFilter: View {
.padding(.bottom)
#endif
}
.presentationDetents([.fraction(0.40), .fraction(0.50)])
.presentationDetents([.fraction(0.6), .fraction(0.75)])
.presentationDragIndicator(.visible)
}
}

View file

@ -31,7 +31,7 @@ struct NodeListItem: View {
Text(node.user?.longName ?? "unknown".localized)
.fontWeight(.medium)
.font(.headline)
if node.user?.vip ?? false {
if node.favorite {
Spacer()
Image(systemName: "star.fill")
.foregroundColor(.yellow)

View file

@ -148,7 +148,7 @@ struct MeshMap: View {
return
}
}
.safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) {
.safeAreaInset(edge: .bottom, alignment: .trailing) {
HStack {
Button(action: {
withAnimation {
@ -166,7 +166,6 @@ struct MeshMap: View {
.padding(5)
}
}
.navigationTitle("Mesh Map")
.navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})

View file

@ -20,6 +20,7 @@ struct NodeList: View {
@State private var viaLora = true
@State private var viaMqtt = true
@State private var isOnline = false
@State private var isFavorite = false
@State private var distanceFilter = false
@State private var maxDistance: Double = 800000
@State private var hopsAway: Int = -1
@ -33,7 +34,9 @@ struct NodeList: View {
@EnvironmentObject var bleManager: BLEManager
@FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "user.vip", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false)],
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
NSSortDescriptor(key: "lastHeard", ascending: false),
NSSortDescriptor(key: "user.longName", ascending: true)],
animation: .default)
var nodes: FetchedResults<NodeInfoEntity>
@ -49,19 +52,39 @@ struct NodeList: View {
connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num,
connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1))
.contextMenu {
if node.user != nil {
Button {
node.user!.vip = !node.user!.vip
context.refresh(node, mergeChanges: true)
do {
try context.save()
} catch {
context.rollback()
print("💥 Save User VIP Error")
Button {
if !node.favorite {
let success = bleManager.setFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum))
if success {
node.favorite = !node.favorite
do {
try context.save()
} catch {
context.rollback()
print("💥 Save Node Favorite Error")
}
print("Favorited a node")
}
} else {
let success = bleManager.removeFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum))
if success {
node.favorite = !node.favorite
do {
try context.save()
} catch {
context.rollback()
print("💥 Save Node Favorite Error")
}
print("Favorited a node")
}
} label: {
Label(node.user?.vip ?? false ? "Un-Favorite" : "Favorite", systemImage: node.user?.vip ?? false ? "star.slash.fill" : "star.fill")
}
} label: {
Label(node.favorite ? "Un-Favorite" : "Favorite", systemImage: node.favorite ? "star.slash.fill" : "star.fill")
}
if node.user != nil {
Button {
node.user!.mute = !node.user!.mute
context.refresh(node, mergeChanges: true)
@ -145,7 +168,7 @@ struct NodeList: View {
}
}
.sheet(isPresented: $isEditingFilters) {
NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole)
NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole)
}
.safeAreaInset(edge: .bottom, alignment: .trailing) {
HStack {
@ -263,6 +286,15 @@ struct NodeList: View {
.onChange(of: isOnline) { _ in
searchNodeList()
}
.onChange(of: isFavorite) { _ in
searchNodeList()
}
.onChange(of: maxDistance) { _ in
searchNodeList()
}
.onChange(of: distanceFilter) { _ in
searchNodeList()
}
.onAppear {
if self.bleManager.context == nil {
self.bleManager.context = context
@ -273,7 +305,7 @@ struct NodeList: View {
private func searchNodeList() {
/// Case Insensitive Search Text Predicates
let searchPredicates = ["user.userId", "user.hwModel", "user.longName", "user.shortName"].map { property in
let searchPredicates = ["user.userId", "user.numString", "user.hwModel", "user.longName", "user.shortName"].map { property in
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
}
/// Create a compound predicate using each text search preicate as an OR
@ -306,6 +338,11 @@ struct NodeList: View {
let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
predicates.append(isOnlinePredicate)
}
/// Favorites
if isFavorite {
let isFavoritePredicate = NSPredicate(format: "favorite == YES")
predicates.append(isFavoritePredicate)
}
/// Distance
if distanceFilter {
let pointOfInterest = LocationHelper.currentLocation
@ -320,15 +357,12 @@ struct NodeList: View {
let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude
let minLongitude: Double = pointOfInterest.longitude - deltaLongitude
let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude
let distancePredicate = NSPredicate(format: "(%lf <= (positions[first].longitudeI / 1e7))", minLongitude, maxLongitude,minLatitude, maxLatitude)
//let distancePredicate = NSPredicate(format: "(%lf <= (positions[LAST].longitudeI / 1e7)) AND ((positions[LAST].longitudeI / 1e7) <= %lf) AND (%lf <= (positions[LAST].latitudeI / 1e7)) AND ((positions[LAST].latitudeI / 1e7) <= %lf)", minLongitude, maxLongitude,minLatitude, maxLatitude)
//predicates.append(distancePredicate)
let distancePredicate = NSPredicate(format: "(SUBQUERY(positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude,minLatitude, maxLatitude)
predicates.append(distancePredicate)
}
}
if predicates.count > 0 || !searchText.isEmpty {
if !searchText.isEmpty {
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates])

View file

@ -32,21 +32,29 @@ struct Channels: View {
@State var hasChanges = false
@State var hasValidKey = true
@State private var isPresentingSaveConfirm: Bool = false
@State private var channelIndex: Int32 = 0
@State private var channelName = ""
@State private var channelKeySize = 16
@State private var channelKey = "AQ=="
@State private var channelRole = 0
@State private var uplink = false
@State private var downlink = false
@State private var positionPrecision = 32.0
@State private var preciseLocation = true
@State private var positionsEnabled = true
@State private var supportedVersion = true
@State var channelIndex: Int32 = 0
@State var channelName = ""
@State var channelKeySize = 16
@State var channelKey = "AQ=="
@State var channelRole = 0
@State var uplink = false
@State var downlink = false
@State var positionPrecision = 32.0
@State var preciseLocation = true
@State var positionsEnabled = true
@State var supportedVersion = true
@State var selectedChannel: ChannelEntity?
/// Minimum Version for granular position configuration
@State var minimumVersion = "2.2.24"
@FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
NSSortDescriptor(key: "lastHeard", ascending: false),
NSSortDescriptor(key: "user.longName", ascending: true)],
animation: .default)
var nodes: FetchedResults<NodeInfoEntity>
var body: some View {
@ -134,6 +142,8 @@ struct Channels: View {
.padding()
#endif
ChannelForm(channelIndex: $channelIndex, channelName: $channelName, channelKeySize: $channelKeySize, channelKey: $channelKey, channelRole: $channelRole, uplink: $uplink, downlink: $downlink, positionPrecision: $positionPrecision, preciseLocation: $preciseLocation, positionsEnabled: $positionsEnabled, hasChanges: $hasChanges, hasValidKey: $hasValidKey, supportedVersion: $supportedVersion)
.presentationDetents([.large])
.presentationDragIndicator(.visible)
.onAppear {
supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame
}
@ -150,26 +160,24 @@ struct Channels: View {
channel.settings.downlinkEnabled = downlink
channel.settings.moduleSettings.positionPrecision = UInt32(positionPrecision)
let newChannel = ChannelEntity(context: context)
newChannel.id = Int32(channel.index)
newChannel.index = Int32(channel.index)
newChannel.uplinkEnabled = channel.settings.uplinkEnabled
newChannel.downlinkEnabled = channel.settings.downlinkEnabled
newChannel.name = channel.settings.name
newChannel.role = Int32(channel.role.rawValue)
newChannel.psk = channel.settings.psk
newChannel.positionPrecision = Int32(positionPrecision)
selectedChannel!.role = Int32(channelRole)
selectedChannel!.index = channelIndex
selectedChannel!.name = channelName
selectedChannel!.psk = Data(base64Encoded: channelKey) ?? Data()
selectedChannel!.uplinkEnabled = uplink
selectedChannel!.downlinkEnabled = downlink
selectedChannel!.positionPrecision = Int32(positionPrecision)
guard let mutableChannels = node?.myInfo?.channels?.mutableCopy() as? NSMutableOrderedSet else {
return
}
if mutableChannels.contains(newChannel) {
mutableChannels.replaceObject(at: Int(newChannel.index), with: newChannel)
if mutableChannels.contains(selectedChannel as Any) {
mutableChannels.replaceObject(at: Int(channel.index), with: selectedChannel as Any)
} else {
mutableChannels.add(newChannel)
mutableChannels.add(selectedChannel as Any)
}
node!.myInfo!.channels = mutableChannels.copy() as? NSOrderedSet
context.refresh(newChannel, mergeChanges: true)
node?.myInfo?.channels = mutableChannels.copy() as? NSOrderedSet
context.refresh(selectedChannel!, mergeChanges: true)
do {
try context.save()
print("💾 Saved Channel: \(channel.settings.name)")
@ -179,24 +187,28 @@ struct Channels: View {
print("💥 Unresolved Core Data error in the channel editor. Error: \(nsError)")
}
} else {
if channelIndex <= node!.myInfo!.channels?.count ?? 0 {
guard let channelEntity = node!.myInfo!.channels?[Int(channelIndex)] as? ChannelEntity else {
return
}
let objects = channelEntity.allPrivateMessages
for object in objects {
context.delete(object)
}
context.delete(channelEntity)
do {
try context.save()
print("💾 Deleted Channel: \(channel.settings.name)")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Unresolved Core Data error in the channel editor. Error: \(nsError)")
guard let channelEntity = node?.myInfo?.channels?.first(where: { ($0 as! ChannelEntity).index == channelIndex }) else {
return
}
let objects = (channelEntity as! ChannelEntity).allPrivateMessages
for object in objects {
context.delete(object)
}
for node in nodes {
if node.channel == (channelEntity as AnyObject).index {
context.delete(node)
}
}
context.delete(channelEntity as! ChannelEntity)
do {
try context.save()
print("💾 Deleted Channel: \(channel.settings.name)")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Unresolved Core Data error in the channel editor. Error: \(nsError)")
}
}
let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!)
@ -227,8 +239,6 @@ struct Channels: View {
.padding(.bottom)
#endif
}
.presentationDetents([.fraction(0.85), .large])
.presentationDragIndicator(.visible)
}
if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil {
@ -249,7 +259,17 @@ struct Channels: View {
uplink = false
downlink = false
hasChanges = true
selectedChannel = ChannelEntity(context: context)
let newChannel = ChannelEntity(context: context)
newChannel.id = channelIndex
newChannel.index = channelIndex
newChannel.uplinkEnabled = uplink
newChannel.downlinkEnabled = downlink
newChannel.name = channelName
newChannel.role = Int32(channelRole)
newChannel.psk = Data(base64Encoded: channelKey) ?? Data()
newChannel.positionPrecision = Int32(positionPrecision)
selectedChannel = newChannel
} label: {
Label("Add Channel", systemImage: "plus.square")
@ -282,7 +302,6 @@ func firstMissingChannelIndex(_ indexes: [Int]) -> Int {
return indexes.count + 1
}
enum PositionPrecision: Int, CaseIterable, Identifiable {
case eleven = 11

View file

@ -105,6 +105,7 @@ struct ChannelForm: View {
)
.onChange(of: channelKey, perform: { _ in
let tempKey = Data(base64Encoded: channelKey) ?? Data()
if tempKey.count == channelKeySize || channelKeySize == -1{
hasValidKey = true
@ -245,7 +246,5 @@ struct ChannelForm: View {
}
}
}
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
}

View file

@ -289,7 +289,7 @@ struct MQTTConfig: View {
.navigationTitle("mqtt.config")
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", mqttProxyConnected: bleManager.mqttProxyConnected)
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", mqttProxyEnabled: self.enabled, mqttProxyConnected: bleManager.mqttProxyConnected)
})
.onChange(of: address) { newAddress in
if node != nil && node?.mqttConfig != nil {

View file

@ -11,22 +11,23 @@ struct SaveChannelQRCode: View {
@Environment(\.dismiss) private var dismiss
var channelSetLink: String
var addChannels: Bool = false
var bleManager: BLEManager
@State var connectedToDevice = false
var body: some View {
VStack {
Text("Save Channel Settings?")
Text("\(addChannels ? "Add" : "Replace all") Channels?")
.font(.title)
Text("These settings will replace the current LoRa Config and Channel Settings on your radio. After everything saves your device will reboot.")
Text("These settings will \(addChannels ? "add" : "replace all") channels. The current LoRa Config will be replaced. After everything saves your device will reboot.")
.foregroundColor(.gray)
.font(.callout)
.font(.title3)
.padding()
HStack {
Button {
let success = bleManager.saveChannelSet(base64UrlString: channelSetLink)
let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannels: addChannels)
if success {
dismiss()
}

View file

@ -13,7 +13,8 @@ import TipKit
struct Settings: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default)
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default)
private var nodes: FetchedResults<NodeInfoEntity>
@State private var selectedNode: Int = 0
@State private var preferredNodeNum: Int = 0
@ -106,16 +107,29 @@ struct Settings: View {
if selectedNode == 0 {
Text("Connect to a Node").tag(0)
}
ForEach(nodes) { node in
if node.num == bleManager.connectedPeripheral?.num ?? 0 {
Text("BLE Config: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
Label {
Text("BLE: \(node.user?.longName ?? "unknown".localized)")
} icon: {
Image(systemName: "antenna.radiowaves.left.and.right")
}
.tag(Int(node.num))
} else if node.metadata != nil {
Text("Remote Config: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
Label {
Text("Remote: \(node.user?.longName ?? "unknown".localized)")
} icon: {
Image(systemName: "av.remote")
}
.tag(Int(node.num))
} else if hasAdmin {
Text("Request Admin: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
Label {
Text("Request Admin: \(node.user?.longName ?? "unknown".localized)")
} icon: {
Image(systemName: "rectangle.and.hand.point.up.left")
}
.tag(Int(node.num))
}
}
}

View file

@ -46,6 +46,7 @@ struct ShareChannels: View {
@State var includeChannel5 = true
@State var includeChannel6 = true
@State var includeChannel7 = true
@State var replaceChannels = true
var node: NodeInfoEntity?
@State private var channelsUrl = "https://www.meshtastic.org/e/#"
var qrCodeImage = QrCodeImage()
@ -53,9 +54,9 @@ struct ShareChannels: View {
var body: some View {
if #available(iOS 17.0, macOS 14.0, *) {
VStack {
TipView(ShareChannelsTip(), arrowEdge: .bottom)
}
// VStack {
// TipView(ShareChannelsTip(), arrowEdge: .bottom)
// }
}
GeometryReader { bounds in
let smallest = min(bounds.size.width, bounds.size.height)
@ -191,6 +192,17 @@ struct ShareChannels: View {
let qrImage = qrCodeImage.generateQRCode(from: channelsUrl)
VStack {
if node != nil {
Toggle(isOn: $replaceChannels) {
Label(replaceChannels ? "Replace Channels" : "Add Channels", systemImage: replaceChannels ? "arrow.triangle.2.circlepath.circle" : "plus.app")
}
.tint(.accentColor)
.toggleStyle(.button)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.top)
.padding(.bottom)
ShareLink("Share QR Code & Link",
item: Image(uiImage: qrImage),
subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"),
@ -235,6 +247,7 @@ struct ShareChannels: View {
.onChange(of: includeChannel5) { _ in generateChannelSet() }
.onChange(of: includeChannel6) { _ in generateChannelSet() }
.onChange(of: includeChannel7) { _ in generateChannelSet() }
.onChange(of: replaceChannels) { _ in generateChannelSet() }
}
}
func generateChannelSet() {
@ -272,7 +285,7 @@ struct ShareChannels: View {
}
}
let settingsString = try! channelSet.serializedData().base64EncodedString()
channelsUrl = ("https://meshtastic.org/e/#" + settingsString.base64ToBase64url())
channelsUrl = ("https://meshtastic.org/e/#" + settingsString.base64ToBase64url() + (replaceChannels ? "" : "?add=true"))
}
}
}

View file

@ -28,6 +28,9 @@ SwiftUI client applications for iOS, iPadOS and macOS.
brew install swift-protobuf
```
- check out the latest protobuf commit from the master branch
```bash
git submodule update --init
```
- run:
```bash
./gen_proto.sh

View file

@ -239,6 +239,7 @@
"network.config"="Netzwerkeinstellungen";
"nodes"="Nodes";
"nodes %@"="Nodes (%@)";
"nodelist.filter.distance %@"="up to %@ away";
"no.nodes"="Keine Meshtastic Nodes gefunden";
"not.connected"="Kein Gerät verbunden";
"numbers.punctuation"="Ziffern und Interpunktion";

View file

@ -245,6 +245,8 @@
"network.config"="Network Config";
"nodes"="Nodes";
"nodes %@"="Nodes (%@)";
"nodelist.filter.distance %@"="up to %@ away";
"save.config %@"="Save Config for %@";
"no.nodes"="No Meshtastic Nodes Found";
"not.connected"="No device connected";
"numbers.punctuation"="Numbers and Punctuation";

View file

@ -219,6 +219,7 @@
"network.config"="Configuration du réseau";
"nodes"="Noeuds";
"nodes %@"="Noeuds (%@)";
"nodelist.filter.distance %@"="up to %@ away";
"no.nodes"="Aucun noeud Meshtastic trouvé";
"not.connected"="Aucun appareil connecté";
"numbers.punctuation"="Nombres and Ponctuation";

View file

@ -1,14 +1,14 @@
#!/bin/bash
# simple sanity checking for repo
if [ ! -d "../protobufs" ]; then
echo "Please check out the protobuf submodule by running: `git submodule update --init`"
if [ ! -d "./protobufs" ]; then
echo 'Please check out the protobuf submodule by running: `git submodule update --init`'
exit
fi
# simple sanity checking for executable
if [ ! -x "$(which protoc)" ]; then
echo "Please install swift-protobuf by running: brew install swift-protobuf"
echo 'Please install swift-protobuf by running: `brew install swift-protobuf`'
exit
fi

View file

@ -243,6 +243,7 @@
"network.config"="הגדרות רשת";
"nodes"="מכשירים";
"nodes %@"="מכשירים (%@)";
"nodelist.filter.distance %@"="up to %@ away";
"no.nodes"="לא נמצאו מכשירי משטסטיק";
"not.connected"="אין מכשיר מחובר";
"numbers.punctuation"="מספרים וסימני פיסוק ";

View file

@ -240,6 +240,7 @@
"network"="Sieć";
"network.config"="Konfiguracja sieci";
"nodes %@"="Węzły (%@)";
"nodelist.filter.distance %@"="up to %@ away";
"no.nodes"="Nie znaleziono węzłów Meshtastic";
"not.connected"="Brak podłączonych urządzeń";
"numbers.punctuation"="Cyfry i interpunkcja";

@ -1 +1 @@
Subproject commit dea3a82ef2accd25112b4ef1c6f8991b579740f4
Subproject commit e6b4c590e7c489306c9c44e3ad1fcf62a3efd288

View file

@ -239,6 +239,7 @@
"network.config"="网络配置";
"nodes"="节点";
"nodes %@"="节点 (%@)";
"nodelist.filter.distance %@"="up to %@ away";
"no.nodes"="未找到 Meshtastic 节点";
"not.connected"="未连接到电台";
"numbers.punctuation"="数字和标点符号";

View file

@ -238,6 +238,7 @@
"network.config"="網路設定";
"nodes"="中繼點";
"nodes %@"="中繼點 (%@)";
"nodelist.filter.distance %@"="up to %@ away";
"no.nodes"="未找到 Meshtastic 中繼點";
"not.connected"="未連接到電台";
"numbers.punctuation"="數字和標點符號";

View file

@ -23,9 +23,9 @@
"automatic.detection"="自動識別";
"battery.level"="電池電量";
"ble.name"="藍芽名稱";
"ble.connection.timeout %d %@"="嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該電台。";
"ble.errorcode.6 %@"="%@ 如果在首選電台的旁邊App 將會自動重連。";
"ble.errorcode.14 %@"="%@ 這個錯誤通常無法自動修復,你需要在系統設定的藍芽選項中忽略該電台並重新配對。";
"ble.connection.timeout %d %@"="嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該設備。";
"ble.errorcode.6 %@"="%@ 如果在首選裝置的旁邊App 將會自動重連。";
"ble.errorcode.14 %@"="%@ 這個錯誤通常無法自動修復,你需要在系統設定的藍芽選項中忽略該裝置並重新配對。";
"ble.errorcode.pin %@"="%@ 請再次嘗試連接並仔細檢查 PIN 碼。";
"bluetooth"="藍芽";
"bluetooth.off"="藍芽已關閉";
@ -60,22 +60,22 @@
"config.power.ls.secs"="Light Sleep Interval";
"config.power.min.wake.secs"="最小的喚醒間隔時間";
"config.power.saving"="省電模式";
"config.power.saving.description"="Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button.";
"config.power.saving.description"="將會盡可能的進入休眠,追蹤器模式和感測器模式將會包含在內";
"config.power.shutdown.on.power.loss"="失去電源後關機";
"config.power.shutdown.after.secs"="之後";
"config.power.wait.bluetooth.secs"="等待藍芽";
"config.ringtone"="RTTTL Ringtone";
"config.ringtone.title"="鈴聲";
"config.ringtone.label"="Ringtone Transfer Language";
"config.ringtone.description"="Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications.";
"config.ringtone.description"="支援外部通知的蜂鳴器所使用的 RTTTLRingtone Transfer Language鈴聲字串";
"config.module.paxcounter.settings"="PAX Counter";
"config.module.paxcounter.title"="PAX Counter Config";
"config.module.paxcounter.enabled.description"="When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be enabled for PAX counter to work.";
"config.module.paxcounter.updateinterval"="Update Interval";
"config.module.paxcounter.enabled.description"="啟用 PAX 計數器模組後,將使用 WiFi 和藍牙計算經過的人數。PAX 計數器需要同時啟用 WiFi 和藍牙才能正常運作";
"config.module.paxcounter.updateinterval"="更新間隔";
"config.module.paxcounter.updateinterval.description"="How often we can send a message to the mesh when people are detected.";
"config.save.confirm"="電台將會在設定儲存後重啟。";
"connected.radio"="已連接的電台";
"communicating"="與電台進行通訊中...";
"config.save.confirm"="裝置將會在設定儲存後重啟。";
"connected.radio"="已連接的裝置";
"communicating"="與裝置進行通訊中...";
"connected"="已連接";
"connecting"="連接中...";
"contacts"="聯絡人";
@ -86,21 +86,21 @@
"delete"="刪除";
"detection.sensor"="檢測感測器";
"device"="設備";
"device.config"="電台設定";
"device.config"="裝置設定";
"device.configuration"="設備設定";
"device.metrics.delete"="刪除所有電台指標??";
"device.metrics.log"="電台指標紀錄檔";
"device.role.client"="標準模式 - App 可以連接到電台進行收發操作,並且會自動轉發 Mesh 網路中其他中繼點的消息。";
"device.role.clientmute"="靜音模式 - 與標準模式類似App 可以連接到電台進行收發操作,但不會轉發 Mesh 網路中其他中繼點的消息。";
"device.metrics.delete"="刪除所有設備指標??";
"device.metrics.log"="設備指標紀錄檔";
"device.role.client"="標準模式 - App 可以連接到裝置進行收發操作,並且會自動轉發 Mesh 網路中其他中繼點的消息。";
"device.role.clientmute"="靜音模式 - 與標準模式類似App 可以連接到裝置進行收發操作,但不會轉發 Mesh 網路中其他中繼點的消息。";
"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption.";
"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\"";
"device.role.router"="纯路由模式 - 自動轉發 Mesh 網路中其他中繼點的消息中繼模式下螢幕會熄滅Wi-Fi 和藍芽將會進入睡眠模式App 將無法連接到電台進行收發操作。";
"device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼點的消息App 也可以連接到電台進行收發操作。";
"device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。";
"device.role.router"="纯路由模式 - 自動轉發 Mesh 網路中其他中繼點的消息中繼模式下螢幕會熄滅Wi-Fi 和藍芽將會進入睡眠模式App 將無法連接到裝置進行收發操作。";
"device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼節點的消息App 也可以連接到裝置進行收發操作。";
"device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。";
"device.role.tracker"="追蹤模式 - 用於作為 GPS 追蹤器。從該設備發送的定位數據包優先級較高,每兩分鐘廣播一次。智能位置廣播預設為關閉。";
"direct.messages"="聊天";
"dismiss.keyboard"="隱藏鍵盤";
"display"="螢幕(電台螢幕)";
"display"="螢幕(設備螢幕)";
"display.config"="螢幕設定";
"distance"="距離";
"disconnect"="斷開連接";
@ -112,7 +112,7 @@
"external.notification.config"="外部通知設定";
"finish"="完成";
"firmware.version"="韌體版本";
"firmware.version.unsupported"="檢測到不支援的韌體版本,無法連接到電台。";
"firmware.version.unsupported"="檢測到不支援的韌體版本,無法連接到裝置。";
"gas"="Gas";
"gas.resistance"="Gas Resistance";
"generate.qr.code"="生成QRcode";
@ -165,13 +165,13 @@
"interval.tyeight.hours"="四十八小时小時";
"interval.eventytwo.hours"="七十二小時";
"keyboard.type"="鍵盤類型";
"logging"="載中";
"logging"="載中";
"lora"="LoRa";
"lora.config"="LoRa 設定";
"map"="Mesh 地圖";
"map.centering"="中";
"map.centering"="中";
"map.tiles.delete"="刪除已緩存的地圖區塊";
"map.recentering"="自動重新中";
"map.recentering"="自動重新中";
"map.use.legacy"="Use Legacy Mesh Map";
"map.type"="地圖類型";
"map.usertrackingmode"="使用者跟隨模式";
@ -198,18 +198,18 @@
"mesh.log.mqtt.config %@"="MQTT module config received: %@";
"mesh.log.myinfo %@"="MyInfo received: %@";
"mesh.log.network.config %@"="收到網路設定: %@";
"mesh.log.nodeinfo.received %@"="收到中繼點訊息: %@";
"mesh.log.nodeinfo.received %@"="收到中繼點訊息: %@";
"mesh.log.paxcounter %@"="PAX Counter message received for: %@";
"mesh.log.position.config %@"="Positon config received: %@";
"mesh.log.position.received %@"="從中繼點接收到定位封包: %@";
"mesh.log.position.received %@"="從中繼點接收到定位封包: %@";
"mesh.log.rangetest.config %@"="收到拉距測試模組設定: %@";
"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 %@"="傳送iOS裝置的GPS定位封包到中繼點上: %@";
"mesh.log.sharelocation %@"="傳送iOS裝置的GPS定位封包到中繼點上: %@";
"mesh.log.storeforward.config %@"="Store & Forward module config received: %@";
"mesh.log.telemetry.config %@"="收到測模組設定: %@";
"mesh.log.telemetry.received %@"="收到測資料: %@";
"mesh.log.telemetry.config %@"="收到測模組設定: %@";
"mesh.log.telemetry.received %@"="收到測資料: %@";
"mesh.log.textmessage.received"="Message received from the text message app.";
"mesh.log.textmessage.send.failed %@"="訊息傳送失敗, 沒有正確連接到 %@";
"mesh.log.textmessage.sent %@ %@ %@"="傳送訊息 %@ 從 %@ 到 %@";
@ -233,10 +233,10 @@
"name"="名稱";
"network"="網路";
"network.config"="網路設定";
"nodes"="中繼點";
"nodes %@"="中繼點 (%@)";
"no.nodes"="未找到 Meshtastic 中繼點";
"not.connected"="未連接到電台";
"nodes"="中繼點";
"nodes %@"="中繼點 (%@)";
"no.nodes"="未找到 Meshtastic 中繼點";
"not.connected"="未連接到設備";
"numbers.punctuation"="數字和標點符號";
"off"="關閉";
"offline"="離線";
@ -245,17 +245,17 @@
"password"="密碼";
"pause"="暫停";
"phone.gps"="手機 GPS";
"phone.gps.interval.description"="電台通過手機獲得定位的時間間隔,但是向 Mesh 網路中更新定位的時間間隔由電台控制。";
"phone.gps.interval.description"="設備通過手機獲得定位的時間間隔,但是向 Mesh 網路中更新定位的時間間隔由裝置控制。";
"position"="定位";
"position.config"="定位設定";
"preferred.radio"="首選電台";
"radio.configuration"="電台設定";
"preferred.radio"="首選設備";
"radio.configuration"="設備設定";
"range.test"="拉距測試";
"range.test.blocked"="區塊範圍測試";
"range.test.config"="拉距測試設定";
"reply"="回復";
"reboot"="重新啟動";
"reboot.node"="重啟中繼點";
"reboot.node"="重啟中繼點";
"received.ack"="收到確認";
"received.ack.real"="收件人確認";
"resume"="恢復";
@ -272,7 +272,7 @@
"routing.nochannel"="没有頻道";
"routing.toolarge"="數據包過大";
"routing.noresponse"="無回應";
"routing.dutycyclelimit"="已達到物錢區域循環週期發射上限";
"routing.dutycyclelimit"="已達到目前區域循環週期發射上限";
"routing.badRequest"="錯誤請求";
"routing.notauthorized"="未授權";
"satellite"="衛星";
@ -291,7 +291,7 @@
"share.position"="分享位置";
"subscribed"="連接到 Mesh 網路";
"select.contact"="選擇聯絡人";
"select.node"="選擇中繼點";
"select.node"="選擇中繼點";
"select.menu.item"="從菜單選擇項目";
"set.region"="設定 LoRa 區域";
"standard"="標準";
@ -303,22 +303,22 @@
"storeforward.heartbeat"="發送心跳包";
"tapback"="響應";
"tapback.heart"="心";
"tapback.thumbsup"="豎大拇指";
"tapback.thumbsdown"="倒大拇指";
"tapback.thumbsup"="";
"tapback.thumbsdown"="倒";
"tapback.haha"="哈哈";
"tapback.exclamation"="驚嘆號";
"tapback.question"="問號";
"tapback.poop"="便便";
"telemetry"="測(傳感器)";
"telemetry.config"="遠側設定";
"telemetry"="測(傳感器)";
"telemetry.config"="遙測設定";
"timeout"="超時";
"timestamp"="時間戳記";
"tip.bluetooth.connect.title"="連接到 LoRa 電台";
"tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 電台的信息。您可以向左滑動斷開電台,長按查看統計訊息或開始即時活動。";
"tip.bluetooth.connect.title"="連接到 LoRa 設備";
"tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 裝置的信息。您可以向左滑動斷開裝置,長按查看統計訊息或開始即時活動。";
"tip.channels.create.title"="管理頻道";
"tip.channels.create.message"="現在 Mesh 上的資料會通過主通道發送。您可以設定輔助通道來建立由自己的金鑰保護的其他訊息組 [頻道設定提示](https://meshtastic.org/docs/configuration/radio/channels/)";
"tip.channels.share.title"="共享 Meshtastic 頻道";
"tip.channels.share.message"="在 Meshtastic 網路中最多有 8 個頻道。第一個頻道是主頻道,大多數活動都發生在這裡,也是必需的。如果您不共享主頻道,您的第一個共享頻道就會成為其他網路的主頻道。它會在其主頻道和您的輔助頻道上對話。名稱為 admin 的頻道可遠端控制中繼點。其他頻道用於私人群组,每個群組都有自己的密鑰。";
"tip.channels.share.message"="在 Meshtastic 網路中最多有 8 個頻道。第一個頻道是主頻道,大多數活動都發生在這裡,也是必需的。如果您不共享主頻道,您的第一個共享頻道就會成為其他網路的主頻道。它會在其主頻道和您的輔助頻道上對話。名稱為 admin 的頻道可遠端控制中繼點。其他頻道用於私人群组,每個群組都有自己的密鑰。";
"tip.messages.title"="消息";
"tip.messages.message"="您可以發送和接收1對1聊天和群聊。在任何訊息中您都可以長按查看可用的操作如複製、回復、拍一拍、刪除以及詳情。";
"twitter"="Twitter";