mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #401 from meshtastic/2.2.7_Working_Changes
2.2.7 working changes
This commit is contained in:
commit
2f20f82ea9
30 changed files with 1141 additions and 325 deletions
|
|
@ -16,6 +16,7 @@
|
|||
DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; };
|
||||
DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; };
|
||||
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */; };
|
||||
DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */; };
|
||||
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */; };
|
||||
DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B828CDA93900720036 /* SerialConfigEnums.swift */; };
|
||||
DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; };
|
||||
|
|
@ -67,6 +68,7 @@
|
|||
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; };
|
||||
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; };
|
||||
DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; };
|
||||
DD760AAE2ABAC706002C022E /* WaypointPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD760AAD2ABAC706002C022E /* WaypointPopover.swift */; };
|
||||
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; };
|
||||
DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */; };
|
||||
DD77093D2AA1AFA3007A8BF0 /* ChannelTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */; };
|
||||
|
|
@ -218,6 +220,7 @@
|
|||
DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = "<group>"; };
|
||||
DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionPopover.swift; sourceTree = "<group>"; };
|
||||
DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV15.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfigEnums.swift; sourceTree = "<group>"; };
|
||||
DD1925B828CDA93900720036 /* SerialConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfigEnums.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -227,6 +230,7 @@
|
|||
DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = "<group>"; };
|
||||
DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = "<group>"; };
|
||||
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = "<group>"; };
|
||||
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = "<group>"; };
|
||||
DD2E65252767A01F00E45FC5 /* NodeDetailOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetailOld.swift; sourceTree = "<group>"; };
|
||||
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -274,6 +278,7 @@
|
|||
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = "<group>"; };
|
||||
DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = "<group>"; };
|
||||
DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = "<group>"; };
|
||||
DD760AAD2ABAC706002C022E /* WaypointPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointPopover.swift; sourceTree = "<group>"; };
|
||||
DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = "<group>"; };
|
||||
DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = "<group>"; };
|
||||
DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -819,6 +824,8 @@
|
|||
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */,
|
||||
DDDB26472AACD6D1003AFCB7 /* NodeMapControl.swift */,
|
||||
DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */,
|
||||
DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */,
|
||||
DD760AAD2ABAC706002C022E /* WaypointPopover.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1069,6 +1076,7 @@
|
|||
DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */,
|
||||
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */,
|
||||
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */,
|
||||
DD760AAE2ABAC706002C022E /* WaypointPopover.swift in Sources */,
|
||||
DD5E5203298EE33B00D21B61 /* config.pb.swift in Sources */,
|
||||
DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */,
|
||||
DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */,
|
||||
|
|
@ -1106,6 +1114,7 @@
|
|||
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
|
||||
DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */,
|
||||
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
|
||||
DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */,
|
||||
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */,
|
||||
DDDB444229F8A88700EE2349 /* Double.swift in Sources */,
|
||||
DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */,
|
||||
|
|
@ -1410,7 +1419,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.6;
|
||||
MARKETING_VERSION = 2.2.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1444,7 +1453,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.6;
|
||||
MARKETING_VERSION = 2.2.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1566,7 +1575,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.6;
|
||||
MARKETING_VERSION = 2.2.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1599,7 +1608,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.6;
|
||||
MARKETING_VERSION = 2.2.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1710,6 +1719,7 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */,
|
||||
DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */,
|
||||
DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */,
|
||||
DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */,
|
||||
|
|
@ -1729,7 +1739,7 @@
|
|||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */;
|
||||
currentVersion = DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.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:
|
||||
|
|
|
|||
|
|
@ -18,3 +18,45 @@ extension CLLocationCoordinate2D {
|
|||
return from.distance(from: to)
|
||||
}
|
||||
}
|
||||
|
||||
extension [CLLocationCoordinate2D] {
|
||||
/// Get Convex Hull For an array of CLLocationCoordinate2D positions
|
||||
/// - Returns: A smaller CLLocationCoordinate2D array containing only the points necessary to create a convex hull polygon
|
||||
func getConvexHull() -> [CLLocationCoordinate2D] {
|
||||
/// X = longitude
|
||||
/// Y = latitude
|
||||
/// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product.
|
||||
/// Returns a positive value, if OAB makes a counter-clockwise turn,
|
||||
/// negative for clockwise turn, and zero if the points are collinear.
|
||||
func cross(P: CLLocationCoordinate2D, A: CLLocationCoordinate2D, B: CLLocationCoordinate2D) -> Double {
|
||||
let part1 = (A.longitude - P.longitude) * (B.latitude - P.latitude)
|
||||
let part2 = (A.latitude - P.latitude) * (B.longitude - P.longitude)
|
||||
return part1 - part2;
|
||||
}
|
||||
// Sort points lexicographically
|
||||
let points = self.sorted() {
|
||||
$0.longitude == $1.longitude ? $0.latitude < $1.latitude : $0.longitude < $1.longitude
|
||||
}
|
||||
// Build the lower hull
|
||||
var lower: [CLLocationCoordinate2D] = []
|
||||
for p in points {
|
||||
while lower.count >= 2 && cross(P: lower[lower.count - 2], A: lower[lower.count - 1], B: p) <= 0 {
|
||||
lower.removeLast()
|
||||
}
|
||||
lower.append(p)
|
||||
}
|
||||
// Build upper hull
|
||||
var upper: [CLLocationCoordinate2D] = []
|
||||
for p in points.reversed() {
|
||||
while upper.count >= 2 && cross(P: upper[upper.count-2], A: upper[upper.count-1], B: p) <= 0 {
|
||||
upper.removeLast()
|
||||
}
|
||||
upper.append(p)
|
||||
}
|
||||
// Last point of upper list is omitted because it is repeated at the
|
||||
// beginning of the lower list.
|
||||
upper.removeLast()
|
||||
// Concatenation of the lower and upper hulls gives the convex hull.
|
||||
return (upper + lower)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ extension UserDefaults {
|
|||
case meshMapRecentering
|
||||
case meshMapShowNodeHistory
|
||||
case meshMapShowRouteLines
|
||||
case enableMapConvexHull
|
||||
case enableMapTraffic
|
||||
case enableMapPointsOfInterest
|
||||
case enableOfflineMaps
|
||||
|
|
@ -98,6 +99,14 @@ extension UserDefaults {
|
|||
UserDefaults.standard.set(newValue, forKey: "meshMapShowRouteLines")
|
||||
}
|
||||
}
|
||||
static var enableMapConvexHull: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableMapConvexHull")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "enableMapConvexHull")
|
||||
}
|
||||
}
|
||||
static var enableMapTraffic: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableMapTraffic")
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
@Published var mqttProxyConnected: Bool = false
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
//public var locationHelper = LocationHelper.shared
|
||||
public var minimumVersion = "2.0.0"
|
||||
public var connectedVersion: String
|
||||
public var isConnecting: Bool = false
|
||||
|
|
@ -42,6 +43,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
var lastPosition: CLLocationCoordinate2D?
|
||||
let emptyNodeNum: UInt32 = 4294967295
|
||||
let mqttManager = MqttClientProxyManager.shared
|
||||
//var locationHelper = LocationHelper.shared
|
||||
var wantRangeTestPackets = false
|
||||
/* Meshtastic Service Details */
|
||||
var TORADIO_characteristic: CBCharacteristic!
|
||||
|
|
@ -69,7 +71,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
// Scan for nearby BLE devices using the Meshtastic BLE service ID
|
||||
func startScanning() {
|
||||
if isSwitchedOn {
|
||||
centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true])
|
||||
centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
|
||||
print("✅ Scanning Started")
|
||||
}
|
||||
}
|
||||
|
|
@ -486,18 +488,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
// Config
|
||||
if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil {
|
||||
|
||||
nowKnown = true
|
||||
localConfig(config: decodedInfo.config, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName)
|
||||
}
|
||||
// Module Config
|
||||
if decodedInfo.moduleConfig.isInitialized && !invalidVersion {
|
||||
|
||||
nowKnown = true
|
||||
moduleConfig(config: decodedInfo.moduleConfig, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName)
|
||||
|
||||
if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) {
|
||||
|
||||
if decodedInfo.moduleConfig.cannedMessage.enabled {
|
||||
_ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true)
|
||||
}
|
||||
|
|
@ -508,9 +506,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
nowKnown = true
|
||||
deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context!)
|
||||
connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion
|
||||
|
||||
let lastDotIndex = decodedInfo.metadata.firmwareVersion.lastIndex(of: ".")
|
||||
|
||||
if lastDotIndex == nil {
|
||||
invalidVersion = true
|
||||
connectedVersion = "0.0.0"
|
||||
|
|
@ -520,19 +516,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
connectedVersion = String(version.dropLast())
|
||||
appState.firmwareVersion = connectedVersion
|
||||
}
|
||||
|
||||
let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame
|
||||
|
||||
if !supportedVersion {
|
||||
invalidVersion = true
|
||||
lastConnectionError = "🚨" + "update.firmware".localized
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
// Log any other unknownApp calls
|
||||
if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") }
|
||||
|
||||
case .textMessageApp, .detectionSensorApp:
|
||||
textMessageAppPacket(packet: decodedInfo.packet, blockRangeTest: UserDefaults.blockRangeTest, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
|
||||
case .remoteHardwareApp:
|
||||
|
|
@ -844,24 +836,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return success
|
||||
}
|
||||
|
||||
public func sendPosition(destNum: Int64, wantResponse: Bool, smartPosition: Bool) -> Bool {
|
||||
public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool {
|
||||
var success = false
|
||||
let fromNodeNum = connectedPeripheral.num
|
||||
if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if smartPosition {
|
||||
if lastPosition != nil {
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral?.num ?? 0, context: context!)
|
||||
if connectedNode?.positionConfig?.smartPositionEnabled ?? false {
|
||||
if lastPosition!.distance(from: LocationHelper.currentLocation) < Double(connectedNode?.positionConfig?.broadcastSmartMinimumDistance ?? 50) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lastPosition = LocationHelper.currentLocation
|
||||
var positionPacket = Position()
|
||||
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
|
||||
|
|
@ -893,6 +873,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
success = true
|
||||
let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum))
|
||||
print(positionPacket)
|
||||
MeshLogger.log("📍 \(logString)")
|
||||
}
|
||||
return success
|
||||
|
|
@ -902,7 +883,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
if connectedPeripheral != nil {
|
||||
// Send a position out to the mesh if "share location with the mesh" is enabled in settings
|
||||
if UserDefaults.provideLocation {
|
||||
let _ = sendPosition(destNum: connectedPeripheral.num, wantResponse: false, smartPosition: true)
|
||||
let _ = sendPosition(destNum: connectedPeripheral.num, wantResponse: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import Foundation
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
|
||||
class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
|
||||
static let shared = LocationHelper()
|
||||
var locationManager = CLLocationManager()
|
||||
|
||||
//@Published var region = MKCoordinateRegion()
|
||||
@Published var authorizationStatus: CLAuthorizationStatus?
|
||||
override init() {
|
||||
super.init()
|
||||
|
|
@ -89,6 +92,13 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
|
|||
}
|
||||
}
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
// locationManager.stopUpdatingLocation()
|
||||
// locations.last.map {
|
||||
// region = MKCoordinateRegion(
|
||||
// center: $0.coordinate,
|
||||
// span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
||||
// )
|
||||
// }
|
||||
}
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
print("Location manager error: \(error.localizedDescription)")
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV18.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV19.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="22221.1" systemVersion="23A5337a" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="23A344" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
|
||||
|
|
@ -345,7 +345,7 @@
|
|||
<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"/>
|
||||
<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 != 1"/>
|
||||
</fetchedProperty>
|
||||
<fetchedProperty name="detectionSensorMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND portNum = 10"/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,376 @@
|
|||
<?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="">
|
||||
<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="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" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitude" optional="YES" 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 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<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"/>
|
||||
</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>
|
||||
|
|
@ -218,7 +218,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
position.latest = false
|
||||
}
|
||||
}
|
||||
|
||||
print("Incoming position message: \n \(positionMessage)")
|
||||
let position = PositionEntity(context: context)
|
||||
position.latest = true
|
||||
position.snr = packet.rxSnr
|
||||
|
|
|
|||
|
|
@ -186,6 +186,10 @@ struct Config {
|
|||
/// Clients should then limit available configuration and administrative options inside the user interface
|
||||
var isManaged: Bool = false
|
||||
|
||||
///
|
||||
/// Disables the triple-press of user button to enable or disable GPS
|
||||
var disableTripleClick: Bool = false
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
///
|
||||
|
|
@ -371,6 +375,10 @@ struct Config {
|
|||
/// The minimum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled
|
||||
var broadcastSmartMinimumIntervalSecs: UInt32 = 0
|
||||
|
||||
///
|
||||
/// (Re)define PIN_GPS_EN for your board.
|
||||
var gpsEnGpio: UInt32 = 0
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
///
|
||||
|
|
@ -951,6 +959,7 @@ struct Config {
|
|||
///
|
||||
/// Maximum number of hops. This can't be greater than 7.
|
||||
/// Default of 3
|
||||
/// Attempting to set a value > 7 results in the default
|
||||
var hopLimit: UInt32 = 0
|
||||
|
||||
///
|
||||
|
|
@ -1596,6 +1605,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
|
|||
7: .standard(proto: "node_info_broadcast_secs"),
|
||||
8: .standard(proto: "double_tap_as_button_press"),
|
||||
9: .standard(proto: "is_managed"),
|
||||
10: .standard(proto: "disable_triple_click"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -1613,6 +1623,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
|
|||
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.nodeInfoBroadcastSecs) }()
|
||||
case 8: try { try decoder.decodeSingularBoolField(value: &self.doubleTapAsButtonPress) }()
|
||||
case 9: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }()
|
||||
case 10: try { try decoder.decodeSingularBoolField(value: &self.disableTripleClick) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -1646,6 +1657,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
|
|||
if self.isManaged != false {
|
||||
try visitor.visitSingularBoolField(value: self.isManaged, fieldNumber: 9)
|
||||
}
|
||||
if self.disableTripleClick != false {
|
||||
try visitor.visitSingularBoolField(value: self.disableTripleClick, fieldNumber: 10)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -1659,6 +1673,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
|
|||
if lhs.nodeInfoBroadcastSecs != rhs.nodeInfoBroadcastSecs {return false}
|
||||
if lhs.doubleTapAsButtonPress != rhs.doubleTapAsButtonPress {return false}
|
||||
if lhs.isManaged != rhs.isManaged {return false}
|
||||
if lhs.disableTripleClick != rhs.disableTripleClick {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
@ -1698,6 +1713,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
9: .standard(proto: "tx_gpio"),
|
||||
10: .standard(proto: "broadcast_smart_minimum_distance"),
|
||||
11: .standard(proto: "broadcast_smart_minimum_interval_secs"),
|
||||
12: .standard(proto: "gps_en_gpio"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -1717,6 +1733,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
case 9: try { try decoder.decodeSingularUInt32Field(value: &self.txGpio) }()
|
||||
case 10: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumDistance) }()
|
||||
case 11: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumIntervalSecs) }()
|
||||
case 12: try { try decoder.decodeSingularUInt32Field(value: &self.gpsEnGpio) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -1756,6 +1773,9 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if self.broadcastSmartMinimumIntervalSecs != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.broadcastSmartMinimumIntervalSecs, fieldNumber: 11)
|
||||
}
|
||||
if self.gpsEnGpio != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.gpsEnGpio, fieldNumber: 12)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -1771,6 +1791,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if lhs.txGpio != rhs.txGpio {return false}
|
||||
if lhs.broadcastSmartMinimumDistance != rhs.broadcastSmartMinimumDistance {return false}
|
||||
if lhs.broadcastSmartMinimumIntervalSecs != rhs.broadcastSmartMinimumIntervalSecs {return false}
|
||||
if lhs.gpsEnGpio != rhs.gpsEnGpio {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@ struct BluetoothConnectionTip: Tip {
|
|||
Text("tip.bluetooth.connect.message")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "questionmark.circle")
|
||||
Image(systemName: "flipphone")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@
|
|||
Text("tip.channels.share.message")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "questionmark.circle")
|
||||
Image(systemName: "qrcode")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,25 @@ struct MessagesTip: Tip {
|
|||
Text("tip.messages.message")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "questionmark.circle")
|
||||
Image(systemName: "bubble.left.and.bubble.right")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct ContactsTip: Tip {
|
||||
|
||||
var id: String {
|
||||
return "tip.messages.contacts"
|
||||
}
|
||||
var title: Text {
|
||||
//Text("tip.messages.contacts.title")
|
||||
Text("Contacts")
|
||||
}
|
||||
var message: Text? {
|
||||
//Text("tip.messages.contacts.message")
|
||||
Text("Each node shows as an available contact. Nodes with recent messages and favorites show up at the top of the list. Select a node to send or view messages. Long press to favorite or mute the node, send a trace route or delete the conversation.")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "person.circle")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,11 @@ struct ChannelList: View {
|
|||
Text("delete")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.top, .bottom])
|
||||
|
|
|
|||
|
|
@ -259,7 +259,9 @@ struct ChannelMessageList: View {
|
|||
.padding([.top])
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.onAppear(perform: {
|
||||
self.bleManager.context = context
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
if channel.allPrivateMessages.count > 0 {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last!.messageId)
|
||||
}
|
||||
|
|
@ -384,7 +386,7 @@ struct ChannelMessageList: View {
|
|||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false, smartPosition: false) {
|
||||
if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
|
|
@ -401,7 +403,7 @@ struct ChannelMessageList: View {
|
|||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false, smartPosition: false) {
|
||||
if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
|
|
@ -409,6 +411,7 @@ struct ChannelMessageList: View {
|
|||
}) {
|
||||
Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,9 @@ struct Messages: View {
|
|||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarItems(leading: MeshtasticLogo())
|
||||
.onAppear {
|
||||
self.bleManager.context = context
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
if UserDefaults.preferredPeripheralId.count > 0 {
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? -1))
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
struct UserList: View {
|
||||
|
||||
|
|
@ -37,6 +40,9 @@ struct UserList: View {
|
|||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY")
|
||||
VStack {
|
||||
List {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
TipView(ContactsTip(), arrowEdge: .bottom)
|
||||
}
|
||||
ForEach(users) { (user: UserEntity) in
|
||||
let mostRecent = user.messageList.last
|
||||
let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 ))))
|
||||
|
|
@ -96,60 +102,60 @@ struct UserList: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 62)
|
||||
.contextMenu {
|
||||
Button {
|
||||
user.vip = !user.vip
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User VIP Error")
|
||||
}
|
||||
} label: {
|
||||
Label(user.vip ? "Un-Favorite" : "Favorite", systemImage: user.vip ? "star.slash.fill" : "star.fill")
|
||||
.frame(height: 62)
|
||||
.contextMenu {
|
||||
Button {
|
||||
user.vip = !user.vip
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User VIP Error")
|
||||
}
|
||||
Button {
|
||||
user.mute = !user.mute
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User Mute Error")
|
||||
}
|
||||
} label: {
|
||||
Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash")
|
||||
} label: {
|
||||
Label(user.vip ? "Un-Favorite" : "Favorite", systemImage: user.vip ? "star.slash.fill" : "star.fill")
|
||||
}
|
||||
Button {
|
||||
user.mute = !user.mute
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User Mute Error")
|
||||
}
|
||||
Button {
|
||||
let success = bleManager.sendTraceRouteRequest(destNum: user.num, wantResponse: true)
|
||||
if success {
|
||||
isPresentingTraceRouteSentAlert = true
|
||||
}
|
||||
} label: {
|
||||
Label("Trace Route", systemImage: "signpost.right.and.left")
|
||||
} label: {
|
||||
Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
Button {
|
||||
let success = bleManager.sendTraceRouteRequest(destNum: user.num, wantResponse: true)
|
||||
if success {
|
||||
isPresentingTraceRouteSentAlert = true
|
||||
}
|
||||
if user.messageList.count > 0 {
|
||||
Button(role: .destructive) {
|
||||
isPresentingDeleteUserMessagesConfirm = true
|
||||
userSelection = user
|
||||
} label: {
|
||||
Label("Delete Messages", systemImage: "trash")
|
||||
}
|
||||
} label: {
|
||||
Label("Trace Route", systemImage: "signpost.right.and.left")
|
||||
}
|
||||
if user.messageList.count > 0 {
|
||||
Button(role: .destructive) {
|
||||
isPresentingDeleteUserMessagesConfirm = true
|
||||
userSelection = user
|
||||
} label: {
|
||||
Label("Delete Messages", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
"Trace Route Sent",
|
||||
isPresented: $isPresentingTraceRouteSentAlert
|
||||
) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text("This could take a while, response will appear in the mesh log.")
|
||||
}
|
||||
.confirmationDialog(
|
||||
"This conversation will be deleted.",
|
||||
isPresented: $isPresentingDeleteUserMessagesConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
}
|
||||
.alert(
|
||||
"Trace Route Sent",
|
||||
isPresented: $isPresentingTraceRouteSentAlert
|
||||
) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text("This could take a while, response will appear in the mesh log.")
|
||||
}
|
||||
.confirmationDialog(
|
||||
"This conversation will be deleted.",
|
||||
isPresented: $isPresentingDeleteUserMessagesConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(role: .destructive) {
|
||||
deleteUserMessages(user: userSelection!, context: context)
|
||||
context.refresh(node!.user!, mergeChanges: true)
|
||||
|
|
|
|||
|
|
@ -237,12 +237,14 @@ struct UserMessageList: View {
|
|||
}
|
||||
.padding([.top])
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.onAppear(perform: {
|
||||
self.bleManager.context = context
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
if user.messageList.count > 0 {
|
||||
scrollView.scrollTo(user.messageList.last!.messageId)
|
||||
}
|
||||
})
|
||||
}
|
||||
.onChange(of: user.messageList, perform: { _ in
|
||||
if user.messageList.count > 0 {
|
||||
scrollView.scrollTo(user.messageList.last!.messageId)
|
||||
|
|
@ -335,7 +337,7 @@ struct UserMessageList: View {
|
|||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(destNum: user.num, wantResponse: true, smartPosition: false) {
|
||||
if bleManager.sendPosition(destNum: user.num, wantResponse: true) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
|
|
@ -352,7 +354,7 @@ struct UserMessageList: View {
|
|||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(destNum: user.num, wantResponse: true, smartPosition: false) {
|
||||
if bleManager.sendPosition(destNum: user.num, wantResponse: true) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ struct NodeDetail: View {
|
|||
Divider()
|
||||
NavigationLink {
|
||||
if #available (iOS 17, macOS 14, *) {
|
||||
NodeMapSwiftUI(node: node)
|
||||
NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num)
|
||||
} else {
|
||||
NodeMapControl(node: node)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,16 @@ struct NodeListItem: View {
|
|||
}
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text(node.user?.longName ?? "unknown".localized)
|
||||
.fontWeight(.medium)
|
||||
.font(.callout)
|
||||
HStack {
|
||||
Text(node.user?.longName ?? "unknown".localized)
|
||||
.fontWeight(.medium)
|
||||
.font(.callout)
|
||||
if node.user?.vip ?? false {
|
||||
Spacer()
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
if connected {
|
||||
HStack {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
|
||||
|
|
|
|||
|
|
@ -14,30 +14,41 @@ import WeatherKit
|
|||
struct NodeMapSwiftUI: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
/// Map State
|
||||
@Namespace var mapScope
|
||||
/// Parameters
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@State var showUserLocation: Bool = false
|
||||
@State var positions: [PositionEntity] = []
|
||||
//@State var waypoints: [WaypointEntity] = []
|
||||
/// Map State User Defaults
|
||||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
|
||||
@AppStorage("meshMapShowConvexHull") private var showConvexHull = true
|
||||
@AppStorage("enableMapTraffic") private var showTraffic: Bool = true
|
||||
@AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = true
|
||||
@AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid
|
||||
// Map Configuration
|
||||
@Namespace var mapScope
|
||||
@State private var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true)
|
||||
@State private var position = MapCameraPosition.automatic
|
||||
@State private var scene: MKLookAroundScene?
|
||||
@State private var isLookingAround = false
|
||||
@State private var isEditingSettings = false
|
||||
@State private var showUserLocation: Bool = false
|
||||
@State var selected: PositionEntity?
|
||||
/// Data
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@State private var selected: PositionEntity?
|
||||
@State private var selectedWaypoint: WaypointEntity?
|
||||
@State private var selectedWaypointRect: CGRect = .zero
|
||||
@State private var selectedWaypointPoint: CGPoint = .zero
|
||||
@State private var showingPositionPopover = false
|
||||
@State private var showingWaypointPopover = false
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
predicate: NSPredicate(
|
||||
format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
), animation: .none)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
@State var waypoiintSelectionRect: CGRect = .zero
|
||||
|
||||
var body: some View {
|
||||
let nodeColor = UIColor(hex: UInt32(node.num))
|
||||
|
||||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
let mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in
|
||||
|
|
@ -46,165 +57,226 @@ struct NodeMapSwiftUI: View {
|
|||
|
||||
if node.hasPositions {
|
||||
ZStack {
|
||||
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
|
||||
/// Route Lines
|
||||
if showRouteLines {
|
||||
let gradient = LinearGradient(
|
||||
colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)],
|
||||
startPoint: .leading, endPoint: .trailing
|
||||
)
|
||||
let stroke = StrokeStyle(
|
||||
lineWidth: 5,
|
||||
lineCap: .round, lineJoin: .round, dash: [10, 10]
|
||||
)
|
||||
MapPolyline(coordinates: lineCoords)
|
||||
.stroke(gradient, style: stroke)
|
||||
}
|
||||
/// Node Annotations
|
||||
ForEach(positionArray.reversed(), id: \.id) { position in
|
||||
let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3))
|
||||
let formatter = MeasurementFormatter()
|
||||
let speedText = formatter.string(from: Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour))
|
||||
Annotation(position.latest ? node.user?.shortName ?? "?" : (pf.contains(.Speed) && position.speed > 2) ? speedText : "", coordinate: position.coordinate) {
|
||||
ZStack {
|
||||
if position.latest {
|
||||
Circle()
|
||||
.foregroundStyle(Color(nodeColor.lighter()).opacity(0.4))
|
||||
.frame(width: 60, height: 60)
|
||||
if pf.contains(.Heading) {
|
||||
Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north.fill" : "location.north")
|
||||
.symbolEffect(.pulse.byLayer)
|
||||
.padding(5)
|
||||
.foregroundStyle(Color(nodeColor).isLight() ? .black : .white)
|
||||
.background(Color(UIColor(hex: UInt32(node.num)).darker()))
|
||||
.clipShape(Circle())
|
||||
.rotationEffect(.degrees(Double(position.heading)))
|
||||
// .onTapGesture {
|
||||
// selected = (selected == position ? nil : position) // <-- here
|
||||
// print("tapity tap tap \(position.time)")
|
||||
// }
|
||||
} else {
|
||||
Image(systemName: "flipphone")
|
||||
.symbolEffect(.pulse.byLayer)
|
||||
.padding(5)
|
||||
.foregroundStyle(Color(nodeColor).isLight() ? .black : .white)
|
||||
.background(Color(UIColor(hex: UInt32(node.num)).darker()))
|
||||
.clipShape(Circle())
|
||||
// .onTapGesture {
|
||||
// selected = (selected == position ? nil : position) // <-- here
|
||||
// print("tapity tap tap \(position.time)")
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
if showNodeHistory {
|
||||
MapReader { reader in
|
||||
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
|
||||
/// Node Color from node.num
|
||||
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: 5,
|
||||
lineCap: .round, lineJoin: .round, dash: [10, 10]
|
||||
)
|
||||
MapPolyline(coordinates: lineCoords)
|
||||
.stroke(gradient, style: dashed)
|
||||
}
|
||||
}
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
let hull = lineCoords.getConvexHull()
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(Color(nodeColor.darker()), lineWidth: 5)
|
||||
.foregroundStyle(Color(nodeColor).opacity(0.4))
|
||||
}
|
||||
/// Waypoint Annotations
|
||||
ForEach(Array(waypoints), id: \.id) { waypoint in
|
||||
Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) {
|
||||
ZStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 35)
|
||||
// .onTapGesture(coordinateSpace: .global) { location in
|
||||
// print("Tapped at \(location)")
|
||||
// let pinLocation = reader.convert(location, from: .local)
|
||||
// print(pinLocation)
|
||||
// let size = CGSize(width: 1, height: 50)
|
||||
// let rect = CGRect(origin: location, size: size)
|
||||
// selectedWaypointRect = rect
|
||||
// selectedWaypointPoint = location
|
||||
// showingWaypointPopover = true
|
||||
// selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Node Annotations
|
||||
ForEach(positionArray, id: \.id) { position in
|
||||
let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3))
|
||||
let formatter = MeasurementFormatter()
|
||||
let headingDegrees = Angle.degrees(Double(position.heading))
|
||||
Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) {
|
||||
ZStack {
|
||||
if position.latest {
|
||||
Circle()
|
||||
.foregroundStyle(Color(nodeColor.lighter()).opacity(0.4))
|
||||
.frame(width: 60, height: 60)
|
||||
if pf.contains(.Heading) {
|
||||
Image(systemName: pf.contains(.Speed) && position.speed > 0 ? "location.north.fill" : "hexagon")
|
||||
.padding(2)
|
||||
.foregroundStyle(Color(UIColor(hex: UInt32(node.num)).lighter()).isLight() ? .black : .white)
|
||||
.background(Color(UIColor(hex: UInt32(node.num)).lighter()))
|
||||
Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "hexagon")
|
||||
.symbolEffect(.pulse.byLayer)
|
||||
.padding(5)
|
||||
.foregroundStyle(Color(nodeColor).isLight() ? .black : .white)
|
||||
.background(Color(UIColor(hex: UInt32(node.num)).darker()))
|
||||
.clipShape(Circle())
|
||||
.rotationEffect(.degrees(Double(position.heading)))
|
||||
.rotationEffect(headingDegrees)
|
||||
.onTapGesture {
|
||||
showingPositionPopover = true
|
||||
selected = (selected == position ? nil : position) // <-- here
|
||||
}
|
||||
.popover(isPresented: $showingPositionPopover) {
|
||||
PositionPopover(position: position)
|
||||
.padding()
|
||||
.opacity(0.8)
|
||||
.presentationCompactAdaptation(.popover)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "mappin.circle")
|
||||
.padding(2)
|
||||
.foregroundStyle(Color(UIColor(hex: UInt32(node.num)).lighter()).isLight() ? .black : .white)
|
||||
.background(Color(UIColor(hex: UInt32(node.num)).lighter()))
|
||||
Image(systemName: "flipphone")
|
||||
.symbolEffect(.pulse.byLayer)
|
||||
.padding(5)
|
||||
.foregroundStyle(Color(nodeColor).isLight() ? .black : .white)
|
||||
.background(Color(UIColor(hex: UInt32(node.num)).darker()))
|
||||
.clipShape(Circle())
|
||||
.onTapGesture {
|
||||
showingPositionPopover = true
|
||||
selected = (selected == position ? nil : position) // <-- here
|
||||
}
|
||||
.popover(isPresented: $showingPositionPopover, arrowEdge: .bottom) {
|
||||
PositionPopover(position: position)
|
||||
.tag(position.id)
|
||||
.padding()
|
||||
.opacity(0.8)
|
||||
.presentationCompactAdaptation(.popover)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if showNodeHistory {
|
||||
if pf.contains(.Heading) {
|
||||
Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north.circle" : "hexagon")
|
||||
.padding(2)
|
||||
.foregroundStyle(Color(UIColor(hex: UInt32(node.num)).lighter()).isLight() ? .black : .white)
|
||||
.background(Color(UIColor(hex: UInt32(node.num)).lighter()))
|
||||
.clipShape(Circle())
|
||||
.rotationEffect(headingDegrees)
|
||||
} else {
|
||||
Image(systemName: "mappin.circle")
|
||||
.padding(2)
|
||||
.foregroundStyle(Color(UIColor(hex: UInt32(node.num)).lighter()).isLight() ? .black : .white)
|
||||
.background(Color(UIColor(hex: UInt32(node.num)).lighter()))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tag(position.time)
|
||||
.annotationTitles(.automatic)
|
||||
.annotationSubtitles(.automatic)
|
||||
}
|
||||
.tag(position.time)
|
||||
}
|
||||
}
|
||||
.mapScope(mapScope)
|
||||
.mapStyle(mapStyle)
|
||||
.mapControls {
|
||||
MapScaleView(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
if showUserLocation {
|
||||
MapUserLocationButton(scope: mapScope)
|
||||
.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)
|
||||
}
|
||||
MapPitchToggle(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
MapCompass(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.overlay(alignment: .bottom) {
|
||||
if scene != nil && isLookingAround {
|
||||
LookAroundPreview(initialScene: scene)
|
||||
.frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(.horizontal, 20)
|
||||
.controlSize(.regular)
|
||||
.overlay(alignment: .bottom) {
|
||||
if scene != nil && isLookingAround {
|
||||
LookAroundPreview(initialScene: scene)
|
||||
.frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isEditingSettings) {
|
||||
VStack {
|
||||
Form {
|
||||
Section(header: Text("Map Options")) {
|
||||
Picker(selection: $selectedMapLayer, label: Text("")) {
|
||||
ForEach(MapLayer.allCases, id: \.self) { layer in
|
||||
if layer != MapLayer.offline {
|
||||
Text(layer.localized)
|
||||
// .popover(item: $selectedWaypoint, attachmentAnchor: .rect(.rect(selectedWaypointRect)), arrowEdge: .bottom) { selection in
|
||||
// //.popover(isPresented: $showingWaypointPopover, arrowEdge: .bottom) {
|
||||
// WaypointPopover(waypoint: selection)
|
||||
// .padding()
|
||||
// .opacity(0.8)
|
||||
// .presentationCompactAdaptation(.popover)
|
||||
// }
|
||||
.sheet(isPresented: $isEditingSettings) {
|
||||
VStack {
|
||||
Form {
|
||||
Section(header: Text("Map Options")) {
|
||||
Picker(selection: $selectedMapLayer, label: Text("")) {
|
||||
ForEach(MapLayer.allCases, id: \.self) { layer in
|
||||
if layer != MapLayer.offline {
|
||||
Text(layer.localized)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .hybrid:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .satellite:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.imagery(elevation: .realistic)
|
||||
case .offline:
|
||||
return
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .hybrid:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .satellite:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.imagery(elevation: .realistic)
|
||||
case .offline:
|
||||
return
|
||||
}
|
||||
}
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
Toggle(isOn: $showNodeHistory) {
|
||||
Label("Node History", systemImage: "building.columns.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.showNodeHistory.toggle()
|
||||
UserDefaults.enableMapNodeHistoryPins = self.showNodeHistory
|
||||
}
|
||||
Toggle(isOn: $showRouteLines) {
|
||||
Label("Route Lines", systemImage: "road.lanes")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.showRouteLines.toggle()
|
||||
UserDefaults.enableMapRouteLines = self.showRouteLines
|
||||
}
|
||||
Toggle(isOn: $showConvexHull) {
|
||||
Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.showConvexHull.toggle()
|
||||
UserDefaults.enableMapConvexHull = self.showConvexHull
|
||||
}
|
||||
Toggle(isOn: $showTraffic) {
|
||||
Label("Traffic", systemImage: "car")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.showTraffic.toggle()
|
||||
UserDefaults.enableMapTraffic = self.showTraffic
|
||||
}
|
||||
Toggle(isOn: $showPointsOfInterest) {
|
||||
Label("Points of Interest", systemImage: "mappin.and.ellipse")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.showPointsOfInterest.toggle()
|
||||
UserDefaults.enableMapPointsOfInterest = self.showPointsOfInterest
|
||||
}
|
||||
}
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
Toggle(isOn: $showNodeHistory) {
|
||||
Label("Node History", systemImage: "building.columns.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.showNodeHistory.toggle()
|
||||
UserDefaults.enableMapNodeHistoryPins = self.showNodeHistory
|
||||
}
|
||||
Toggle(isOn: $showRouteLines) {
|
||||
Label("Route Lines", systemImage: "road.lanes")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.showRouteLines.toggle()
|
||||
UserDefaults.enableMapRouteLines = self.showRouteLines
|
||||
}
|
||||
Toggle(isOn: $showTraffic) {
|
||||
Label("Traffic", systemImage: "car")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.showTraffic.toggle()
|
||||
UserDefaults.enableMapTraffic = self.showTraffic
|
||||
}
|
||||
Toggle(isOn: $showPointsOfInterest) {
|
||||
Label("Points of Interest", systemImage: "mappin.and.ellipse")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.showPointsOfInterest.toggle()
|
||||
UserDefaults.enableMapPointsOfInterest = self.showPointsOfInterest
|
||||
}
|
||||
}
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Button {
|
||||
isEditingSettings = false
|
||||
} label: {
|
||||
|
|
@ -214,84 +286,84 @@ struct NodeMapSwiftUI: View {
|
|||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
#endif
|
||||
}
|
||||
//.presentationDetents([.fraction(0.4)])
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.onChange(of: node) {
|
||||
let mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 1500, heading: 0, pitch: 0))
|
||||
if let mostRecent {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: mostRecent.coordinate)
|
||||
#endif
|
||||
}
|
||||
.presentationDetents([.fraction(0.46)])
|
||||
//.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .hybrid:
|
||||
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .satellite:
|
||||
mapStyle = MapStyle.imagery(elevation: .realistic)
|
||||
case .offline:
|
||||
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
}
|
||||
if self.scene == nil {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: mostRecent!.coordinate)
|
||||
}
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isEditingSettings = !isEditingSettings
|
||||
.onChange(of: node) {
|
||||
let mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
position = MapCameraPosition.automatic//.camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 1500, heading: 0, pitch: 0))
|
||||
if let mostRecent {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: mostRecent.coordinate)
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
/// Look Around Button
|
||||
if self.scene != nil {
|
||||
}
|
||||
.onAppear {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .hybrid:
|
||||
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .satellite:
|
||||
mapStyle = MapStyle.imagery(elevation: .realistic)
|
||||
case .offline:
|
||||
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
}
|
||||
if self.scene == nil {
|
||||
Task {
|
||||
scene = try? await fetchScene(for: mostRecent!.coordinate)
|
||||
}
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isLookingAround = !isLookingAround
|
||||
isEditingSettings = !isEditingSettings
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars")
|
||||
Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
/// Look Around Button
|
||||
if self.scene != nil {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isLookingAround = !isLookingAround
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
MapZoomStepper(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
MapPitchSlider(scope: mapScope)
|
||||
.mapControlVisibility(.visible)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
}
|
||||
.onDisappear {
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}}
|
||||
.navigationBarTitle(String((node.user?.shortName ?? "unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
bluetoothOn: bleManager.isSwitchedOn,
|
||||
deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
|
|
@ -303,7 +375,7 @@ struct NodeMapSwiftUI: View {
|
|||
}
|
||||
|
||||
private func fetchScene(for coordinate: CLLocationCoordinate2D) async throws -> MKLookAroundScene? {
|
||||
let lookAroundScene = MKLookAroundSceneRequest(coordinate: coordinate)
|
||||
return try await lookAroundScene.scene
|
||||
let lookAroundScene = MKLookAroundSceneRequest(coordinate: coordinate)
|
||||
return try await lookAroundScene.scene
|
||||
}
|
||||
}
|
||||
|
|
|
|||
126
Meshtastic/Views/Nodes/Helpers/PositionPopover.swift
Normal file
126
Meshtastic/Views/Nodes/Helpers/PositionPopover.swift
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
//
|
||||
// PositionPopover.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 9/17/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
struct PositionPopover: View {
|
||||
var position: PositionEntity
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(position.nodePosition?.user?.num ?? 0))))
|
||||
Text(position.nodePosition?.user?.longName ?? "Unknown")
|
||||
.font(.title3)
|
||||
let degrees = Angle.degrees(Double(position.heading))
|
||||
}
|
||||
Divider()
|
||||
VStack (alignment: .leading) {
|
||||
/// Time
|
||||
Label {
|
||||
Text(position.time?.formatted() ?? "Unknown")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "clock.badge.checkmark")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
/// Coordinate
|
||||
Label {
|
||||
Text("\(String(format: "%.6f", position.coordinate.latitude)), \(String(format: "%.6f", position.coordinate.longitude))")
|
||||
.font(.footnote)
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
/// Altitude
|
||||
Label {
|
||||
Text("Altitude: \(distanceFormatter.string(fromDistance: Double(position.altitude)))")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "mountain.2.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3))
|
||||
/// Sats in view
|
||||
if pf.contains(.Satsinview) {
|
||||
Label {
|
||||
Text("Sats in view: \(String(position.satsInView))")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "sparkles")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Sequence Number
|
||||
if pf.contains(.SeqNo) {
|
||||
Label {
|
||||
Text("Sequence: \(String(position.seqNo))")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "number")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Heading
|
||||
if pf.contains(.Heading) {
|
||||
let degrees = Angle.degrees(Double(position.heading))
|
||||
Label {
|
||||
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
|
||||
Text("Heading: \(heading.formatted())")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "location.north")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
.rotationEffect(degrees)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Speed
|
||||
if pf.contains(.Speed) {
|
||||
let formatter = MeasurementFormatter()
|
||||
Label {
|
||||
Text("Speed: \(formatter.string(from: Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour)))")
|
||||
// .font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "gauge.with.dots.needle.33percent")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Distance
|
||||
if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 {
|
||||
let metersAway = position.coordinate.distance(from: LocationHelper.currentLocation)
|
||||
Label {
|
||||
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
// .font(.footnote)
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Meshtastic/Views/Nodes/Helpers/WaypointPopover.swift
Normal file
98
Meshtastic/Views/Nodes/Helpers/WaypointPopover.swift
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
//
|
||||
// WaypointPopover.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen on 9/19/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
struct WaypointPopover: View {
|
||||
var waypoint: WaypointEntity
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.blue)
|
||||
Text(waypoint.name ?? "?")
|
||||
.font(.title3)
|
||||
if waypoint.locked > 0 {
|
||||
Image(systemName: "lock.fill" )
|
||||
.font(.title2)
|
||||
} else {
|
||||
// Edit Button
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
VStack (alignment: .leading) {
|
||||
// Description
|
||||
if (waypoint.longDescription ?? "").count > 0 {
|
||||
Label {
|
||||
Text(waypoint.longDescription ?? "")
|
||||
.foregroundColor(.primary)
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} icon: {
|
||||
Image(systemName: "doc.plaintext")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Created
|
||||
Label {
|
||||
Text("Created: \(waypoint.created?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
.font(.footnote)
|
||||
} icon: {
|
||||
Image(systemName: "clock.badge.checkmark")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
/// Updated
|
||||
if waypoint.lastUpdated != nil {
|
||||
Label {
|
||||
Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
.font(.footnote)
|
||||
} icon: {
|
||||
Image(systemName: "clock.arrow.circlepath")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Updated
|
||||
if waypoint.expire != nil {
|
||||
Label {
|
||||
Text("Expires: \(waypoint.expire?.formatted() ?? "?")")
|
||||
.foregroundColor(.primary)
|
||||
.font(.footnote)
|
||||
} icon: {
|
||||
Image(systemName: "clock.badge.xmark")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
/// Distance
|
||||
if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 {
|
||||
let metersAway = waypoint.coordinate.distance(from: LocationHelper.currentLocation)
|
||||
Label {
|
||||
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
.font(.footnote)
|
||||
} icon: {
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tag(waypoint.id)
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ struct NodeList: View {
|
|||
sortDescriptors: [NSSortDescriptor(key: "user.vip", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false)],
|
||||
animation: .default)
|
||||
|
||||
private var nodes: FetchedResults<NodeInfoEntity>
|
||||
var nodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
|
||||
|
||||
|
|
@ -42,7 +42,39 @@ struct NodeList: View {
|
|||
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
List(nodes, id: \.self, selection: $selectedNode) { node in
|
||||
|
||||
NodeListItem(node: node, connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1), modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0))
|
||||
NodeListItem(node: node,
|
||||
connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num,
|
||||
connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1),
|
||||
modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0))
|
||||
.contextMenu {
|
||||
if node.user != nil {
|
||||
Button {
|
||||
node.user!.vip = !node.user!.vip
|
||||
context.refresh(node, mergeChanges: true)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User VIP Error")
|
||||
}
|
||||
} label: {
|
||||
Label(node.user?.vip ?? false ? "Un-Favorite" : "Favorite", systemImage: node.user?.vip ?? false ? "star.slash.fill" : "star.fill")
|
||||
}
|
||||
Button {
|
||||
node.user!.mute = !node.user!.mute
|
||||
context.refresh(node, mergeChanges: true)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save User Mute Error")
|
||||
}
|
||||
} label: {
|
||||
Label(node.user!.mute ? "Show Alerts" : "Hide Alerts", systemImage: node.user!.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.searchable(text: nodesQuery, prompt: "Find a node")
|
||||
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ struct PositionLog: View {
|
|||
Text(speed.formatted())
|
||||
}
|
||||
TableColumn("Heading") { position in
|
||||
Text("\(position.heading)°")
|
||||
let heading = Measurement(value: Double(position.heading), unit: UnitAngle.degrees)
|
||||
Text("\(heading.formatted())")
|
||||
}
|
||||
TableColumn("SNR") { position in
|
||||
Text("\(String(format: "%.2f", position.snr)) dB")
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ struct PositionConfig: View {
|
|||
Button(buttonText) {
|
||||
|
||||
if fixedPosition {
|
||||
_ = bleManager.sendPosition(destNum: node!.num, wantResponse: true, smartPosition: false)
|
||||
_ = bleManager.sendPosition(destNum: node!.num, wantResponse: true)
|
||||
}
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
|
||||
|
|
|
|||
|
|
@ -51,16 +51,16 @@ struct ShareChannels: View {
|
|||
var qrCodeImage = QrCodeImage()
|
||||
|
||||
var body: some View {
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
VStack {
|
||||
TipView(ShareChannelsTip(), arrowEdge: .bottom)
|
||||
}
|
||||
}
|
||||
GeometryReader { bounds in
|
||||
let smallest = min(bounds.size.width, bounds.size.height)
|
||||
ScrollView {
|
||||
if node != nil && node?.myInfo != nil {
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
VStack {
|
||||
TipView(ShareChannelsTip(), arrowEdge: .top)
|
||||
}
|
||||
}
|
||||
Grid {
|
||||
GridRow {
|
||||
Spacer()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import ActivityKit
|
|||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 16.2, *)
|
||||
struct WidgetsLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
|
||||
|
|
@ -52,7 +51,7 @@ struct WidgetsLiveActivity: Widget {
|
|||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
} else {
|
||||
Text("Plugged In")
|
||||
Text("PWD")
|
||||
.font(.title3)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
|
@ -101,7 +100,6 @@ struct WidgetsLiveActivity: Widget {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.2, *)
|
||||
struct WidgetsLiveActivity_Previews: PreviewProvider {
|
||||
static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G")
|
||||
static let state = MeshActivityAttributes.ContentState(
|
||||
|
|
@ -123,7 +121,6 @@ struct WidgetsLiveActivity_Previews: PreviewProvider {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.2, *)
|
||||
struct LiveActivityView: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.isLuminanceReduced) var isLuminanceReduced
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@
|
|||
"telemetry.config"="Telemetry Config";
|
||||
"timeout"="Timeout";
|
||||
"timestamp"="Timestamp";
|
||||
"tip.bluetooth.connect.title"="Connected LoRa Radio";
|
||||
"tip.bluetooth.connect.title"="Connected Radio";
|
||||
"tip.bluetooth.connect.message"="Shows information for the Lora radio currently connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity.";
|
||||
"tip.channels.share.title"="Sharing Meshtastic Channels";
|
||||
"tip.channels.share.message"="In a Meshtastic LoRa Mesh there are up to 8 channels. The first one is the Primary channel where most activity happens and is required. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. It talks on its primary and your secondary channel. A channel with the name 'admin' controls nodes remotely. Other channels are for private groups, each with its own key.";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue