mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #427 from meshtastic/2.2.13_Working_Changes
2.2.13 Working Changes - Looks great, I approve
This commit is contained in:
commit
66f363d03a
33 changed files with 924 additions and 101 deletions
|
|
@ -100,6 +100,8 @@
|
|||
DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA0B6B1294CDC55001356EC /* Channels.swift */; };
|
||||
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */; };
|
||||
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */; };
|
||||
DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAB580C2B0DAA9E00147258 /* Routes.swift */; };
|
||||
DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */; };
|
||||
DDAD49ED2AFB39DC00B4425D /* MeshMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */; };
|
||||
DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5226EB1DF10058C060 /* BLEManager.swift */; };
|
||||
DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */; };
|
||||
|
|
@ -314,6 +316,9 @@
|
|||
DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = "<group>"; };
|
||||
DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = "<group>"; };
|
||||
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = "<group>"; };
|
||||
DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV20.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDAB580C2B0DAA9E00147258 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routes.swift; sourceTree = "<group>"; };
|
||||
DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMap.swift; sourceTree = "<group>"; };
|
||||
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = "<group>"; };
|
||||
DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothConfig.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -472,10 +477,26 @@
|
|||
DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */,
|
||||
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */,
|
||||
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */,
|
||||
DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */,
|
||||
);
|
||||
path = CoreData;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DD2100802B0E676E00F2F116 /* Routes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD2100832B0E67AD00F2F116 /* RouteMap */,
|
||||
);
|
||||
path = Routes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DD2100832B0E67AD00F2F116 /* RouteMap */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = RouteMap;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DD47E3CA26F0E50300029299 /* Nodes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -503,9 +524,11 @@
|
|||
DD4A911C2708C57100501B7E /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD2100802B0E676E00F2F116 /* Routes */,
|
||||
DD97E96728EFE9A00056DDA4 /* About.swift */,
|
||||
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */,
|
||||
DD4A911D2708C65400501B7E /* AppSettings.swift */,
|
||||
DDAB580C2B0DAA9E00147258 /* Routes.swift */,
|
||||
DDA0B6B1294CDC55001356EC /* Channels.swift */,
|
||||
DDD6EEAE29BC024700383354 /* Firmware.swift */,
|
||||
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */,
|
||||
|
|
@ -1208,6 +1231,7 @@
|
|||
DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */,
|
||||
DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */,
|
||||
DD5E523A298EFA5300D21B61 /* TelemetryWeather.swift in Sources */,
|
||||
DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */,
|
||||
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */,
|
||||
DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */,
|
||||
DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */,
|
||||
|
|
@ -1223,6 +1247,7 @@
|
|||
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */,
|
||||
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */,
|
||||
DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */,
|
||||
DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */,
|
||||
DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */,
|
||||
DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */,
|
||||
DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */,
|
||||
|
|
@ -1435,7 +1460,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.12;
|
||||
MARKETING_VERSION = 2.2.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1469,7 +1494,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.12;
|
||||
MARKETING_VERSION = 2.2.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1591,7 +1616,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.12;
|
||||
MARKETING_VERSION = 2.2.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1624,7 +1649,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.12;
|
||||
MARKETING_VERSION = 2.2.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1735,6 +1760,7 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */,
|
||||
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */,
|
||||
DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */,
|
||||
DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */,
|
||||
|
|
@ -1755,7 +1781,7 @@
|
|||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */;
|
||||
currentVersion = DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */;
|
||||
name = Meshtastic.xcdatamodeld;
|
||||
path = Meshtastic/Meshtastic.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable {
|
|||
}
|
||||
|
||||
enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
|
||||
case fiveSeconds = 5
|
||||
case tenSeconds = 10
|
||||
case fifteenSeconds = 15
|
||||
case thirtySeconds = 30
|
||||
|
|
@ -97,8 +96,6 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
|
|||
var id: Int { self.rawValue }
|
||||
var description: String {
|
||||
switch self {
|
||||
case .fiveSeconds:
|
||||
return "interval.five.seconds".localized
|
||||
case .tenSeconds:
|
||||
return "interval.ten.seconds".localized
|
||||
case .fifteenSeconds:
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ enum ScreenOnIntervals: Int, CaseIterable, Identifiable {
|
|||
enum ScreenCarouselIntervals: Int, CaseIterable, Identifiable {
|
||||
|
||||
case off = 0
|
||||
case fifteenSeconds = 15
|
||||
case thirtySeconds = 30
|
||||
case oneMinute = 60
|
||||
case fiveMinutes = 300
|
||||
|
|
@ -84,6 +85,8 @@ enum ScreenCarouselIntervals: Int, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
case .off:
|
||||
return "off".localized
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
case .oneMinute:
|
||||
|
|
@ -168,3 +171,29 @@ enum DisplayModes: Int, CaseIterable, Identifiable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default of 0 is metric
|
||||
enum Units: Int, CaseIterable, Identifiable {
|
||||
|
||||
case metric = 0
|
||||
case imperial = 1
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var description: String {
|
||||
switch self {
|
||||
case .metric:
|
||||
return "Metric"
|
||||
case .imperial:
|
||||
return "Imperial"
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Config.DisplayConfig.DisplayUnits {
|
||||
|
||||
switch self {
|
||||
case .metric:
|
||||
return Config.DisplayConfig.DisplayUnits.metric
|
||||
case .imperial:
|
||||
return Config.DisplayConfig.DisplayUnits.imperial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
42
Meshtastic/Extensions/CoreData/LocationEntityExtension.swift
Normal file
42
Meshtastic/Extensions/CoreData/LocationEntityExtension.swift
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// LocationEntityExtension.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright (c) Garth Vander Houwen 11/21/23.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
||||
extension LocationEntity {
|
||||
|
||||
var latitude: Double? {
|
||||
|
||||
let d = Double(latitudeI)
|
||||
if d == 0 {
|
||||
return 0
|
||||
}
|
||||
return d / 1e7
|
||||
}
|
||||
|
||||
var longitude: Double? {
|
||||
|
||||
let d = Double(longitudeI)
|
||||
if d == 0 {
|
||||
return 0
|
||||
}
|
||||
return d / 1e7
|
||||
}
|
||||
|
||||
var locationCoordinate: CLLocationCoordinate2D? {
|
||||
if latitudeI != 0 && longitudeI != 0 {
|
||||
let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
|
||||
return coord
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -37,5 +37,13 @@ extension UIColor {
|
|||
private func add(_ value: CGFloat, toComponent: CGFloat) -> CGFloat {
|
||||
return max(0, min(1, toComponent + value))
|
||||
}
|
||||
|
||||
|
||||
static var random: UIColor {
|
||||
return UIColor(
|
||||
red: .random(in: 0...1),
|
||||
green: .random(in: 0...1),
|
||||
blue: .random(in: 0...1),
|
||||
alpha: 1.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ extension UserDefaults {
|
|||
case enableRangeTest
|
||||
case meshtasticUsername
|
||||
case preferredPeripheralId
|
||||
case preferredPeripheralNum
|
||||
case provideLocation
|
||||
case provideLocationInterval
|
||||
case mapLayer
|
||||
|
|
@ -53,6 +54,14 @@ extension UserDefaults {
|
|||
UserDefaults.standard.set(newValue, forKey: "preferredPeripheralId")
|
||||
}
|
||||
}
|
||||
static var preferredPeripheralNum: Int {
|
||||
get {
|
||||
UserDefaults.standard.integer(forKey: "preferredPeripheralNum")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "preferredPeripheralNum")
|
||||
}
|
||||
}
|
||||
static var provideLocation: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "provideLocation")
|
||||
|
|
|
|||
|
|
@ -486,6 +486,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let myInfo = myInfoPacket(myInfo: decodedInfo.myInfo, peripheralId: self.connectedPeripheral.id, context: context!)
|
||||
|
||||
if myInfo != nil {
|
||||
UserDefaults.preferredPeripheralNum = Int(myInfo!.myNodeNum)
|
||||
connectedPeripheral.num = myInfo!.myNodeNum
|
||||
connectedPeripheral.name = myInfo?.bleName ?? "unknown".localized
|
||||
connectedPeripheral.longName = myInfo?.bleName ?? "unknown".localized
|
||||
|
|
@ -611,7 +612,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
}
|
||||
case .neighborinfoApp:
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
|
||||
if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) {
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App App UNHANDLED \(neighborInfo)")
|
||||
}
|
||||
case .UNRECOGNIZED:
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())")
|
||||
case .max:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV19.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV20.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="23A344" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23C5047e" 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"/>
|
||||
|
|
@ -290,7 +290,7 @@
|
|||
<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" maxCount="1" deletionRule="Nullify" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
|
||||
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Nullify" 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=""/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,378 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23C5047e" 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" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="psk" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
|
||||
<fetchedProperty name="allPrivateMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="channel == $FETCH_SOURCE.index && toUser == nil AND isEmoji == false"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="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="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="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
|
||||
</entity>
|
||||
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="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="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="detection" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="environment" optional="YES" attributeType="Boolean" defaultValueString="NO" 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"/>
|
||||
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
|
||||
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
|
||||
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
|
||||
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
|
||||
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
|
||||
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
|
||||
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
|
||||
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
|
||||
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
|
||||
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
|
||||
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
|
||||
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
|
||||
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
|
||||
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
|
||||
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
|
||||
<relationship name="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="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="num"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="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="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="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="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"/>
|
||||
<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="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="shortName" attributeType="String"/>
|
||||
<attribute name="userId" attributeType="String"/>
|
||||
<attribute name="vip" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<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"/>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -256,10 +256,10 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else {
|
||||
return
|
||||
}
|
||||
/// Don't save the same position over and over.
|
||||
/// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one.
|
||||
if mutablePositions.count > 0 {
|
||||
let mostRecent = mutablePositions.lastObject as! PositionEntity
|
||||
if mostRecent.latitudeI == position.latitudeI && mostRecent.longitudeI == position.longitudeI {
|
||||
if mostRecent.coordinate.distance(from: position.coordinate) < 10 {
|
||||
mutablePositions.remove(mostRecent)
|
||||
}
|
||||
}
|
||||
|
|
@ -413,6 +413,7 @@ func upsertDisplayConfigPacket(config: Meshtastic.Config.DisplayConfig, nodeNum:
|
|||
newDisplayConfig.flipScreen = config.flipScreen
|
||||
newDisplayConfig.oledType = Int32(config.oled.rawValue)
|
||||
newDisplayConfig.displayMode = Int32(config.displaymode.rawValue)
|
||||
newDisplayConfig.units = Int32(config.units.rawValue)
|
||||
newDisplayConfig.headingBold = config.headingBold
|
||||
fetchedNode[0].displayConfig = newDisplayConfig
|
||||
|
||||
|
|
@ -425,6 +426,7 @@ func upsertDisplayConfigPacket(config: Meshtastic.Config.DisplayConfig, nodeNum:
|
|||
fetchedNode[0].displayConfig?.flipScreen = config.flipScreen
|
||||
fetchedNode[0].displayConfig?.oledType = Int32(config.oled.rawValue)
|
||||
fetchedNode[0].displayConfig?.displayMode = Int32(config.displaymode.rawValue)
|
||||
fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue)
|
||||
fetchedNode[0].displayConfig?.headingBold = config.headingBold
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ struct ChannelMessageList: View {
|
|||
@State private var deleteMessageId: Int64 = 0
|
||||
@State private var replyMessageId: Int64 = 0
|
||||
@State private var sendPositionWithMessage: Bool = false
|
||||
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
|
@ -39,7 +40,7 @@ struct ChannelMessageList: View {
|
|||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach( channel.allPrivateMessages ) { (message: MessageEntity) in
|
||||
let currentUser: Bool = (bleManager.connectedPeripheral?.num ?? -1 == message.fromUser?.num ? true : false)
|
||||
let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false)
|
||||
if message.replyID > 0 {
|
||||
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ struct Messages: View {
|
|||
}
|
||||
if UserDefaults.preferredPeripheralId.count > 0 {
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? -1))
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(UserDefaults.preferredPeripheralNum))
|
||||
do {
|
||||
guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ struct UserList: View {
|
|||
Spacer()
|
||||
if user.vip {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
if user.messageList.count > 0 {
|
||||
if lastMessageDay == currentDay {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ struct UserMessageList: View {
|
|||
LazyVStack {
|
||||
ForEach( user.messageList ) { (message: MessageEntity) in
|
||||
if user.num != bleManager.connectedPeripheral?.num ?? -1 {
|
||||
let currentUser: Bool = (bleManager.connectedPeripheral?.num ?? 0 == message.fromUser?.num ?? -1 ? true : false)
|
||||
let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false)
|
||||
|
||||
if message.replyID > 0 {
|
||||
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct MapSettingsForm: View {
|
|||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(header: Text("Map Options")) {
|
||||
Picker(selection: $mapLayer, label: Text("")) {
|
||||
|
|
@ -80,6 +80,7 @@ struct MapSettingsForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
Button {
|
||||
|
|
@ -95,5 +96,6 @@ Spacer()
|
|||
}
|
||||
.presentationDetents([.fraction(0.45), .fraction(0.65)])
|
||||
.presentationDragIndicator(.visible)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,18 +61,16 @@ struct NodeMapSwiftUI: View {
|
|||
let nodeColor = UIColor(hex: UInt32(node.num))
|
||||
/// Route Lines
|
||||
if showRouteLines {
|
||||
if showRouteLines {
|
||||
let gradient = LinearGradient(
|
||||
colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)],
|
||||
startPoint: .leading, endPoint: .trailing
|
||||
)
|
||||
let dashed = StrokeStyle(
|
||||
lineWidth: 3,
|
||||
lineCap: .round, lineJoin: .round, dash: [10, 10]
|
||||
)
|
||||
MapPolyline(coordinates: lineCoords)
|
||||
.stroke(gradient, style: dashed)
|
||||
}
|
||||
let gradient = LinearGradient(
|
||||
colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)],
|
||||
startPoint: .leading, endPoint: .trailing
|
||||
)
|
||||
let dashed = StrokeStyle(
|
||||
lineWidth: 3,
|
||||
lineCap: .round, lineJoin: .round, dash: [10, 10]
|
||||
)
|
||||
MapPolyline(coordinates: lineCoords)
|
||||
.stroke(gradient, style: dashed)
|
||||
}
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
|
|
@ -125,7 +123,7 @@ struct NodeMapSwiftUI: View {
|
|||
.opacity(0.8)
|
||||
.presentationCompactAdaptation(.popover)
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
Image(systemName: "flipphone")
|
||||
.symbolEffect(.pulse.byLayer)
|
||||
|
|
@ -142,7 +140,7 @@ struct NodeMapSwiftUI: View {
|
|||
.opacity(0.8)
|
||||
.presentationCompactAdaptation(.popover)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
} else {
|
||||
if showNodeHistory {
|
||||
|
|
@ -155,7 +153,7 @@ struct NodeMapSwiftUI: View {
|
|||
.clipShape(Circle())
|
||||
.rotationEffect(headingDegrees)
|
||||
.frame(width: 16, height: 16)
|
||||
|
||||
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color(UIColor(hex: UInt32(node.num))))
|
||||
|
|
@ -225,6 +223,8 @@ struct NodeMapSwiftUI: View {
|
|||
}
|
||||
}
|
||||
.onChange(of: node) {
|
||||
isLookingAround = false
|
||||
isShowingAltitude = false
|
||||
mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
position = .automatic
|
||||
|
|
@ -282,8 +282,8 @@ struct NodeMapSwiftUI: View {
|
|||
showWaypoints = !showWaypoints
|
||||
}
|
||||
}) {
|
||||
Image(systemName: showWaypoints ? "signpost.right.and.left.fill" : "signpost.right.and.left")
|
||||
.padding(.vertical, 5)
|
||||
Image(systemName: showWaypoints ? "signpost.right.and.left.fill" : "signpost.right.and.left")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
@ -319,13 +319,13 @@ struct NodeMapSwiftUI: View {
|
|||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
/// Hide non fuctional catalyst controls
|
||||
// MapZoomStepper(scope: mapScope)
|
||||
// .mapControlVisibility(.visible)
|
||||
// MapPitchSlider(scope: mapScope)
|
||||
// .mapControlVisibility(.visible)
|
||||
#endif
|
||||
// MapZoomStepper(scope: mapScope)
|
||||
// .mapControlVisibility(.visible)
|
||||
// MapPitchSlider(scope: mapScope)
|
||||
// .mapControlVisibility(.visible)
|
||||
#endif
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ struct PositionAltitudeChart: View {
|
|||
var body: some View {
|
||||
let nodePositions = Array(node.positions!) as! [PositionEntity]
|
||||
let data = nodePositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) }
|
||||
HStack {
|
||||
GroupBox(label: Label("Altitude", systemImage: "mountain.2")) {
|
||||
|
||||
Chart(data, id: \.time) {
|
||||
LineMark(
|
||||
x: .value("Time", $0.time),
|
||||
|
|
@ -56,7 +57,6 @@ struct PositionAltitudeChart: View {
|
|||
}
|
||||
.chartXAxis(.visible)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(UIColor.secondarySystemBackground))
|
||||
.opacity(/*@START_MENU_TOKEN@*/0.8/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,32 @@ struct PositionPopover: View {
|
|||
var position: PositionEntity
|
||||
var popover: Bool = true
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
var delay: Double = 0
|
||||
@State private var scale: CGFloat = 0.5
|
||||
var body: some View {
|
||||
// Node Color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(position.nodePosition?.user?.num ?? 0))), circleSize: 65)
|
||||
.padding(.trailing, 5)
|
||||
ZStack {
|
||||
|
||||
if position.nodePosition?.isOnline ?? false {
|
||||
Circle()
|
||||
.fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5)))
|
||||
.foregroundStyle(Color(nodeColor.lighter()).opacity(0.3))
|
||||
.scaleEffect(scale)
|
||||
.animation(
|
||||
Animation.easeInOut(duration: 0.6)
|
||||
.repeatForever().delay(delay), value: scale
|
||||
)
|
||||
.onAppear {
|
||||
self.scale = 1
|
||||
}
|
||||
.frame(width: 90, height: 90)
|
||||
}
|
||||
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 65)
|
||||
}
|
||||
|
||||
Text(position.nodePosition?.user?.longName ?? "Unknown")
|
||||
.font(.largeTitle)
|
||||
}
|
||||
|
|
@ -129,7 +150,7 @@ struct PositionPopover: View {
|
|||
if position.nodePosition != nil {
|
||||
if position.nodePosition?.user?.vip ?? false {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(.yellow)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.largeTitle)
|
||||
.padding(.bottom, 5)
|
||||
|
|
@ -137,7 +158,7 @@ struct PositionPopover: View {
|
|||
if position.nodePosition?.hasEnvironmentMetrics ?? false {
|
||||
Image(systemName: "cloud.sun.rain")
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.font(.largeTitle)
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import MapKit
|
|||
import CoreLocation
|
||||
|
||||
struct WaypointForm: View {
|
||||
|
||||
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State var waypoint: WaypointEntity
|
||||
|
|
@ -27,9 +27,9 @@ struct WaypointForm: View {
|
|||
@State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours
|
||||
@State private var locked: Bool = false
|
||||
@State private var lockedTo: Int64 = 0
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
NavigationStack {
|
||||
if editMode {
|
||||
Text((waypoint.id > 0) ? "Editing Waypoint" : "Create Waypoint")
|
||||
.font(.largeTitle)
|
||||
|
|
@ -341,7 +341,7 @@ struct WaypointForm: View {
|
|||
}
|
||||
}
|
||||
.padding(.top)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
|
|
@ -352,7 +352,7 @@ struct WaypointForm: View {
|
|||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -381,9 +381,11 @@ struct WaypointForm: View {
|
|||
expires = false
|
||||
expire = Date.now.addingTimeInterval(60 * 480)
|
||||
icon = "📍"
|
||||
latitude = waypoint.coordinate.latitude
|
||||
latitude = waypoint.coordinate.latitude
|
||||
longitude = waypoint.coordinate.longitude
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.75)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ struct NodeDetail: View {
|
|||
NavigationLink {
|
||||
EnvironmentMetricsLog(node: node)
|
||||
} label: {
|
||||
Image(systemName: "chart.xyaxis.line")
|
||||
Image(systemName: "cloud.sun.rain")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ struct NodeListItem: View {
|
|||
VStack(alignment: .leading) {
|
||||
CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65)
|
||||
.padding(.trailing, 5)
|
||||
BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
|
|
@ -33,7 +32,7 @@ struct NodeListItem: View {
|
|||
if node.user?.vip ?? false {
|
||||
Spacer()
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
}
|
||||
if connected {
|
||||
|
|
@ -81,8 +80,30 @@ struct NodeListItem: View {
|
|||
HStack {
|
||||
let preset = ModemPresets(rawValue: Int(modemPreset))
|
||||
LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor)
|
||||
|
||||
if node.hasPositions {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.callout)
|
||||
|
||||
}
|
||||
if node.hasEnvironmentMetrics {
|
||||
Image(systemName: "cloud.sun.rain")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.callout)
|
||||
}
|
||||
if node.hasDetectionSensorMetrics {
|
||||
Image(systemName: "sensor")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
.padding(.top, 3)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ struct MeshMap: View {
|
|||
var delay: Double = 0
|
||||
@State private var scale: CGFloat = 0.5
|
||||
|
||||
/// "time >= %@ && nodePosition != nil && latest == true"
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
|
||||
predicate: NSPredicate(format: "time >= %@ && nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -30, to: Date())! as NSDate), animation: .none)
|
||||
predicate: NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -30, to: Date())! as NSDate), animation: .none)
|
||||
private var positions: FetchedResults<PositionEntity>
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
|
|
@ -53,6 +54,10 @@ struct MeshMap: View {
|
|||
format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
), animation: .none)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
|
||||
predicate: NSPredicate(format: "enabled == true", ""), animation: .none)
|
||||
private var routes: FetchedResults<RouteEntity>
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
|
@ -122,7 +127,39 @@ struct MeshMap: View {
|
|||
selectedPosition = (selectedPosition == position ? nil : position)
|
||||
}
|
||||
}
|
||||
/// Route Lines
|
||||
/// Routes
|
||||
ForEach(Array(routes), id: \.id) { route in
|
||||
let routeLocations = Array(route.locations!) as! [LocationEntity]
|
||||
let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in
|
||||
return loc.locationCoordinate ?? LocationHelper.DefaultLocation
|
||||
})
|
||||
Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(.green))
|
||||
.strokeBorder(.white, lineWidth: 3)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(.black))
|
||||
.strokeBorder(.white, lineWidth: 3)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
let dashed = StrokeStyle(
|
||||
lineWidth: 3,
|
||||
lineCap: .round, lineJoin: .round, dash: [7, 10]
|
||||
)
|
||||
MapPolyline(coordinates: routeCoords)
|
||||
.stroke(Color(UIColor(hex: UInt32(route.color))), style: dashed)
|
||||
|
||||
}
|
||||
/// Node Route Lines
|
||||
if showRouteLines {
|
||||
let nodePositions = Array(position.nodePosition!.positions!) as! [PositionEntity]
|
||||
let routeCoords = nodePositions.compactMap({(pos) -> CLLocationCoordinate2D in
|
||||
|
|
@ -172,6 +209,19 @@ struct MeshMap: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.mapScope(mapScope)
|
||||
.mapStyle(mapStyle)
|
||||
.mapControls {
|
||||
MapScaleView(scope: mapScope)
|
||||
.mapControlVisibility(.automatic)
|
||||
MapUserLocationButton(scope: mapScope)
|
||||
.mapControlVisibility(showUserLocation ? .visible : .hidden)
|
||||
MapPitchToggle(scope: mapScope)
|
||||
.mapControlVisibility(.automatic)
|
||||
MapCompass(scope: mapScope)
|
||||
.mapControlVisibility(.automatic)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.onTapGesture(count: 1, perform: { location in
|
||||
newWaypointCoord = reader.convert(location , from: .local)
|
||||
})
|
||||
|
|
@ -184,23 +234,10 @@ struct MeshMap: View {
|
|||
editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480)
|
||||
editingWaypoint!.id = 0
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.mapScope(mapScope)
|
||||
.mapStyle(mapStyle)
|
||||
.mapControls {
|
||||
MapScaleView(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
if showUserLocation {
|
||||
MapUserLocationButton(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
}
|
||||
MapPitchToggle(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
MapCompass(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
|
||||
.sheet(item: $selectedPosition) { selection in
|
||||
PositionPopover(position: selection, popover: false)
|
||||
.padding()
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ struct Channels: View {
|
|||
Picker("Key Size", selection: $channelKeySize) {
|
||||
Text("Empty").tag(0)
|
||||
Text("Default").tag(-1)
|
||||
Text("1 bit").tag(1)
|
||||
Text("1 byte").tag(1)
|
||||
Text("128 bit").tag(16)
|
||||
Text("192 bit").tag(24)
|
||||
Text("256 bit").tag(32)
|
||||
|
|
|
|||
|
|
@ -143,8 +143,11 @@ struct DeviceConfig: View {
|
|||
) {
|
||||
Button("Erase all device and app data?", role: .destructive) {
|
||||
if bleManager.sendNodeDBReset(fromUser: node!.user!, toUser: node!.user!) {
|
||||
bleManager.disconnectPeripheral()
|
||||
clearCoreDataDatabase(context: context)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
bleManager.disconnectPeripheral()
|
||||
clearCoreDataDatabase(context: context)
|
||||
}
|
||||
|
||||
} else {
|
||||
print("NodeDB Reset Failed")
|
||||
}
|
||||
|
|
@ -165,8 +168,10 @@ struct DeviceConfig: View {
|
|||
) {
|
||||
Button("Factory reset your device and app? ", role: .destructive) {
|
||||
if bleManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!) {
|
||||
bleManager.disconnectPeripheral()
|
||||
clearCoreDataDatabase(context: context)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
bleManager.disconnectPeripheral()
|
||||
clearCoreDataDatabase(context: context)
|
||||
}
|
||||
} else {
|
||||
print("Factory Reset Failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ struct DisplayConfig: View {
|
|||
@State var flipScreen = false
|
||||
@State var oledType = 0
|
||||
@State var displayMode = 0
|
||||
@State var units = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
|
@ -125,6 +126,15 @@ struct DisplayConfig: View {
|
|||
Text("The format used to display GPS coordinates on the device screen.")
|
||||
.font(.caption)
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
Picker("Display Units", selection: $units ) {
|
||||
ForEach(Units.allCases) { un in
|
||||
Text(un.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("Units displayed on the device screen")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.displayConfig == nil)
|
||||
|
|
@ -160,6 +170,7 @@ struct DisplayConfig: View {
|
|||
dc.flipScreen = flipScreen
|
||||
dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue()
|
||||
dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue()
|
||||
dc.units = Units(rawValue: units)!.protoEnumValue()
|
||||
|
||||
let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
if adminMessageId > 0 {
|
||||
|
|
@ -233,6 +244,11 @@ struct DisplayConfig: View {
|
|||
if newDisplayMode != node!.displayConfig!.displayMode { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: units) { newUnits in
|
||||
if node != nil && node!.displayConfig != nil {
|
||||
if newUnits != node!.displayConfig!.units { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
func setDisplayValues() {
|
||||
self.gpsFormat = Int(node?.displayConfig?.gpsFormat ?? 0)
|
||||
|
|
@ -243,6 +259,7 @@ struct DisplayConfig: View {
|
|||
self.flipScreen = node?.displayConfig?.flipScreen ?? false
|
||||
self.oledType = Int(node?.displayConfig?.oledType ?? 0)
|
||||
self.displayMode = Int(node?.displayConfig?.displayMode ?? 0)
|
||||
self.units = Int(node?.displayConfig?.units ?? 0)
|
||||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,6 +184,12 @@ struct LoRaConfig: View {
|
|||
.scrollDismissesKeyboard(.immediately)
|
||||
.focused($focusedField, equals: .frequencyOverride)
|
||||
}
|
||||
HStack {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.foregroundColor(.accentColor)
|
||||
Stepper("\(txPower)db Transmit Power", value: $txPower, in: 1...30, step: 1)
|
||||
.padding(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.loRaConfig == nil)
|
||||
|
|
@ -214,6 +220,7 @@ struct LoRaConfig: View {
|
|||
lc.modemPreset = ModemPresets(rawValue: modemPreset)!.protoEnumValue()
|
||||
lc.usePreset = usePreset
|
||||
lc.txEnabled = txEnabled
|
||||
lc.txPower = Int32(txPower)
|
||||
lc.channelNum = UInt32(channelNum)
|
||||
lc.bandwidth = UInt32(bandwidth)
|
||||
lc.codingRate = UInt32(codingRate)
|
||||
|
|
@ -302,6 +309,11 @@ struct LoRaConfig: View {
|
|||
if newOverrideFrequency != node!.loRaConfig!.overrideFrequency { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: txPower) { newTxPower in
|
||||
if node != nil && node!.loRaConfig != nil {
|
||||
if newTxPower != node!.loRaConfig!.txPower { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
func setLoRaValues() {
|
||||
self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3)
|
||||
|
|
|
|||
195
Meshtastic/Views/Settings/Routes.swift
Normal file
195
Meshtastic/Views/Settings/Routes.swift
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
//
|
||||
// Routes.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 11/21/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import MapKit
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct Routes: View {
|
||||
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@State private var selectedRoute: RouteEntity?
|
||||
@State private var importing = false
|
||||
@State private var isShowingBadFileAlert = false
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "enabled", ascending: false), NSSortDescriptor(key: "name", ascending: true), NSSortDescriptor(key: "date", ascending: false)], animation: .default)
|
||||
|
||||
var routes: FetchedResults<RouteEntity>
|
||||
var body: some View {
|
||||
//NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
NavigationStack {
|
||||
Button("Import Route") {
|
||||
importing = true
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
|
||||
.alert(isPresented: $isShowingBadFileAlert) {
|
||||
Alert(title: Text("Not a valid route file"), message: Text("Your route file must have both Latitude and Longitude."), dismissButton: .default(Text("OK")))
|
||||
}
|
||||
.fileImporter(
|
||||
isPresented: $importing,
|
||||
allowedContentTypes: [.commaSeparatedText],
|
||||
allowsMultipleSelection: false
|
||||
) { result in
|
||||
do {
|
||||
guard let selectedFile: URL = try result.get().first else { return }
|
||||
guard selectedFile.startAccessingSecurityScopedResource() else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
guard let fileContent = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return }
|
||||
let routeName = selectedFile.lastPathComponent.dropLast(4)
|
||||
let lines = fileContent.components(separatedBy: "\n")
|
||||
let headers = lines.first?.components(separatedBy: ",")
|
||||
var latIndex = -1
|
||||
var longIndex = -1
|
||||
for index in headers!.indices {
|
||||
print("\(index): \( headers![index])")
|
||||
if headers![index].trimmingCharacters(in: .whitespaces) == "Latitude" {
|
||||
latIndex = index
|
||||
} else if headers![index].trimmingCharacters(in: .whitespaces) == "Longitude" {
|
||||
longIndex = index
|
||||
}
|
||||
}
|
||||
if latIndex >= 0 && longIndex >= 0 {
|
||||
let newRoute = RouteEntity(context: context)
|
||||
newRoute.name = String(routeName)
|
||||
newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max)
|
||||
newRoute.color = Int64(UIColor.random.hex)
|
||||
newRoute.date = Date()
|
||||
newRoute.enabled = true
|
||||
var newLocations = [LocationEntity]()
|
||||
lines.dropFirst().forEach { line in
|
||||
let data = line.components(separatedBy: ",")
|
||||
if data.count > 1 {
|
||||
let latitude = latIndex >= 0 ? data[latIndex].trimmingCharacters(in: .whitespaces) : "0"
|
||||
let longitude = longIndex >= 0 ? data[longIndex].trimmingCharacters(in: .whitespaces) : "0"
|
||||
let loc = LocationEntity(context: context)
|
||||
loc.latitudeI = Int32((Double(latitude) ?? 0) * 1e7)
|
||||
loc.longitudeI = Int32((Double(longitude) ?? 0) * 1e7)
|
||||
newLocations.append(loc)
|
||||
print("Longitude: \(longitude) Latitude: \(latitude)")
|
||||
}
|
||||
}
|
||||
newRoute.locations? = NSOrderedSet(array: newLocations)
|
||||
do {
|
||||
try context.save()
|
||||
} catch let error as NSError {
|
||||
print("Error: \(error.localizedDescription)")
|
||||
isShowingBadFileAlert = true
|
||||
}
|
||||
} else {
|
||||
isShowingBadFileAlert = true
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("error: \(error)") // to do deal with errors
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("CSV Import Error")
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
List(routes, id: \.self, selection: $selectedRoute) { route in
|
||||
let routeColor = Color(UIColor(hex: route.color >= 0 ? UInt32(route.color) : 0))
|
||||
Label {
|
||||
VStack (alignment: .leading) {
|
||||
Text("\(route.name ?? "No Name Route")")
|
||||
.padding(.top)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text("\(route.date?.formatted() ?? "Unknown Time")")
|
||||
.padding(.bottom)
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
if route.notes?.count ?? 0 > 0 {
|
||||
Text("\(route.notes ?? "")")
|
||||
.padding(.bottom)
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
} icon: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(routeColor)
|
||||
.frame(width: 40, height: 40)
|
||||
.padding(.top)
|
||||
if route.enabled {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.padding(.top)
|
||||
.foregroundColor(routeColor.isLight() ? .black : .white)
|
||||
}
|
||||
}
|
||||
}
|
||||
.swipeActions {
|
||||
Button(role: .destructive) {
|
||||
context.delete(route)
|
||||
do {
|
||||
try context.save()
|
||||
} catch let error as NSError {
|
||||
print("Error: \(error.localizedDescription)")
|
||||
}
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
.navigationTitle("Route List")
|
||||
// } detail: {
|
||||
|
||||
VStack {
|
||||
if selectedRoute != nil {
|
||||
let locationArray = selectedRoute?.locations?.array as? [LocationEntity] ?? []
|
||||
let lineCoords = locationArray.compactMap({(location) -> CLLocationCoordinate2D in
|
||||
return location.locationCoordinate ?? LocationHelper.DefaultLocation
|
||||
})
|
||||
|
||||
Map() {
|
||||
Annotation("Start", coordinate: lineCoords.first ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(.green))
|
||||
.strokeBorder(.white, lineWidth: 3)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
Annotation("Finish", coordinate: lineCoords.last ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(.black))
|
||||
.strokeBorder(.white, lineWidth: 3)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
let dashed = StrokeStyle(
|
||||
lineWidth: 3,
|
||||
lineCap: .round, lineJoin: .round, dash: [7, 10]
|
||||
)
|
||||
MapPolyline(coordinates: lineCoords)
|
||||
.stroke(Color(UIColor(hex: UInt32(selectedRoute?.color ?? 0))), style: dashed)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}.navigationTitle(" \(selectedRoute?.name ?? "Unknown Route") \(selectedRoute?.locations?.count ?? 0) points")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,10 +13,11 @@ struct Settings: View {
|
|||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default)
|
||||
private var nodes: FetchedResults<NodeInfoEntity>
|
||||
@State private var selectedNode: Int = 0
|
||||
@State private var connectedNodeNum: Int = 0
|
||||
@State private var preferredNodeNum: Int = 0
|
||||
@State private var selection: SettingsSidebar = .about
|
||||
enum SettingsSidebar {
|
||||
case appSettings
|
||||
case routes
|
||||
case shareChannels
|
||||
case userConfig
|
||||
case loraConfig
|
||||
|
|
@ -57,7 +58,18 @@ struct Settings: View {
|
|||
Text("app.settings")
|
||||
}
|
||||
.tag(SettingsSidebar.appSettings)
|
||||
let node = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
NavigationLink {
|
||||
Routes()
|
||||
} label: {
|
||||
Image(systemName: "road.lanes.curved.right")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("routes")
|
||||
}
|
||||
.tag(SettingsSidebar.routes)
|
||||
}
|
||||
|
||||
let node = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0 ? true : false
|
||||
if !(node?.deviceConfig?.isManaged ?? false) {
|
||||
Section("Configure") {
|
||||
|
|
@ -84,8 +96,8 @@ struct Settings: View {
|
|||
.onChange(of: selectedNode) { newValue in
|
||||
if selectedNode > 0 {
|
||||
let node = nodes.first(where: { $0.num == newValue })
|
||||
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
|
||||
let connectedNode = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
preferredNodeNum = Int(connectedNode?.num ?? 0)// Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
|
||||
if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil && node?.metadata == nil {
|
||||
let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context)
|
||||
if adminMessageId > 0 {
|
||||
|
|
@ -100,14 +112,14 @@ struct Settings: View {
|
|||
}
|
||||
Section("radio.configuration") {
|
||||
NavigationLink {
|
||||
ShareChannels(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
ShareChannels(node: nodes.first(where: { $0.num == preferredNodeNum }))
|
||||
} label: {
|
||||
Image(systemName: "qrcode")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("share.channels")
|
||||
}
|
||||
.tag(SettingsSidebar.shareChannels)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
.disabled(selectedNode > 0 && selectedNode != preferredNodeNum)
|
||||
NavigationLink {
|
||||
UserConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
|
|
@ -125,14 +137,14 @@ struct Settings: View {
|
|||
}
|
||||
.tag(SettingsSidebar.loraConfig)
|
||||
NavigationLink {
|
||||
Channels(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
Channels(node: nodes.first(where: { $0.num == preferredNodeNum }))
|
||||
} label: {
|
||||
Image(systemName: "fibrechannel")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("channels")
|
||||
}
|
||||
.tag(SettingsSidebar.channelConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
.disabled(selectedNode > 0 && selectedNode != preferredNodeNum)
|
||||
NavigationLink {
|
||||
BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
|
|
@ -257,7 +269,7 @@ struct Settings: View {
|
|||
}
|
||||
.tag(SettingsSidebar.meshLog)
|
||||
NavigationLink {
|
||||
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
let connectedNode = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
AdminMessageList(user: connectedNode?.user)
|
||||
} label: {
|
||||
Image(systemName: "building.columns")
|
||||
|
|
@ -266,24 +278,24 @@ struct Settings: View {
|
|||
}
|
||||
.tag(SettingsSidebar.adminMessageLog)
|
||||
}
|
||||
Section(header: Text("Firmware")) {
|
||||
NavigationLink {
|
||||
Firmware(node: nodes.first(where: { $0.num == connectedNodeNum }))
|
||||
} label: {
|
||||
Image(systemName: "arrow.up.arrow.down.square")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("Firmware Updates")
|
||||
}
|
||||
.tag(SettingsSidebar.about)
|
||||
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
|
||||
}
|
||||
// Section(header: Text("Firmware")) {
|
||||
// NavigationLink {
|
||||
// Firmware(node: nodes.first(where: { $0.num == preferredNodeNum }))
|
||||
// } label: {
|
||||
// Image(systemName: "arrow.up.arrow.down.square")
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// Text("Firmware Updates")
|
||||
// }
|
||||
// .tag(SettingsSidebar.about)
|
||||
// .disabled(selectedNode > 0 && selectedNode != preferredNodeNum)
|
||||
// }
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
self.connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
|
||||
self.preferredNodeNum = UserDefaults.preferredPeripheralNum// Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
|
||||
selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@
|
|||
"received.ack.real"="Recipient Ack";
|
||||
"ringtone"="Ringtone";
|
||||
"ringtone.config"="Ringtone Config";
|
||||
"routes"="Routes";
|
||||
"routing.acknowledged"="Bestätigt";
|
||||
"routing.noroute"="Keine Route";
|
||||
"routing.gotnak"="Negative Empfangsbestätigung empfangen";
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@
|
|||
"received.ack.real"="Recipient Ack";
|
||||
"ringtone"="Ringtone";
|
||||
"ringtone.config"="Ringtone Config";
|
||||
"routes"="Routes";
|
||||
"routing.acknowledged"="Acknowledged";
|
||||
"routing.noroute"="No Route";
|
||||
"routing.gotnak"="Received a negative acknowledgment";
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@
|
|||
"received.ack.real"="Odbiorca potwierdzenia";
|
||||
"ringtone"="Dzwonek";
|
||||
"ringtone.config"="Konfiguracja dzwonka";
|
||||
"routes"="Routes";
|
||||
"routing.acknowledged"="Potwierdzono";
|
||||
"routing.noroute"="Brak trasy";
|
||||
"routing.gotnak"="Otrzymano negatywne potwierdzenie";
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@
|
|||
"received.ack.real"="收件人确认";
|
||||
"ringtone"="铃声";
|
||||
"ringtone.config"="铃声设置";
|
||||
"routes"="Routes";
|
||||
"routing.acknowledged"="确认";
|
||||
"routing.noroute"="找不到目标";
|
||||
"routing.gotnak"="收到否认";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue