mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #564 from meshtastic/2.3.2_Working_Changes
2.3.2 working changes
This commit is contained in:
commit
b05b595499
43 changed files with 2038 additions and 783 deletions
|
|
@ -185,6 +185,9 @@
|
|||
DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB444F29F8AC9C00EE2349 /* UIImage.swift */; };
|
||||
DDDB445229F8ACF900EE2349 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445129F8ACF900EE2349 /* Date.swift */; };
|
||||
DDDB445429F8AD1600EE2349 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445329F8AD1600EE2349 /* Data.swift */; };
|
||||
DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC22372BA92344002C44F1 /* MeshMapContent.swift */; };
|
||||
DDDCD5702BB26F5C00BE6B60 /* NodeListFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */; };
|
||||
DDDCD5722BB3E46400BE6B60 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5226EB1DF10058C060 /* BLEManager.swift */; };
|
||||
DDDE59F529AF163D00490C6C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61C29AE7E8E003C5A37 /* WidgetKit.framework */; };
|
||||
DDDE59F629AF163D00490C6C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */; };
|
||||
DDDE59F929AF163D00490C6C /* WidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */; };
|
||||
|
|
@ -456,6 +459,9 @@
|
|||
DDDB445329F8AD1600EE2349 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
|
||||
DDDC22312BA76701002C44F1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
DDDC22322BA76961002C44F1 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
DDDC22372BA92344002C44F1 /* MeshMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMapContent.swift; sourceTree = "<group>"; };
|
||||
DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListFilter.swift; sourceTree = "<group>"; };
|
||||
DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 31.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsBundle.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -747,7 +753,7 @@
|
|||
DDAD49EB2AFAE82500B4425D /* Map */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */,
|
||||
DDDC22362BA9232C002C44F1 /* MapContent */,
|
||||
DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */,
|
||||
DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */,
|
||||
DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */,
|
||||
|
|
@ -958,6 +964,7 @@
|
|||
DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */,
|
||||
DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */,
|
||||
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */,
|
||||
DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -986,6 +993,15 @@
|
|||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DDDC22362BA9232C002C44F1 /* MapContent */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DDDC22372BA92344002C44F1 /* MeshMapContent.swift */,
|
||||
DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */,
|
||||
);
|
||||
path = MapContent;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DDDE59F729AF163D00490C6C /* Widgets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1214,6 +1230,7 @@
|
|||
DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */,
|
||||
DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */,
|
||||
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
|
||||
DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */,
|
||||
DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */,
|
||||
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */,
|
||||
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */,
|
||||
|
|
@ -1343,6 +1360,7 @@
|
|||
DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */,
|
||||
DD5E520E298EE33B00D21B61 /* mqtt.pb.swift in Sources */,
|
||||
DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */,
|
||||
DDDCD5702BB26F5C00BE6B60 /* NodeListFilter.swift in Sources */,
|
||||
DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */,
|
||||
DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */,
|
||||
DDDB443D29F6592F00EE2349 /* NetworkManager.swift in Sources */,
|
||||
|
|
@ -1387,6 +1405,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DDD3BBD5292D763200D609B3 /* MeshtasticTests.swift in Sources */,
|
||||
DDDCD5722BB3E46400BE6B60 /* BLEManager.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -1591,7 +1610,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.3.1;
|
||||
MARKETING_VERSION = 2.3.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1625,7 +1644,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.3.1;
|
||||
MARKETING_VERSION = 2.3.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1747,7 +1766,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.3.1;
|
||||
MARKETING_VERSION = 2.3.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1780,7 +1799,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.3.1;
|
||||
MARKETING_VERSION = 2.3.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1891,6 +1910,7 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */,
|
||||
DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */,
|
||||
DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */,
|
||||
DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */,
|
||||
|
|
@ -1922,7 +1942,7 @@
|
|||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */;
|
||||
currentVersion = DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */;
|
||||
name = Meshtastic.xcdatamodeld;
|
||||
path = Meshtastic/Meshtastic.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
|||
|
|
@ -50,6 +50,20 @@ enum MeshMapTypes: Int, CaseIterable, Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
enum MeshMapDistances: Double, CaseIterable, Identifiable {
|
||||
case fiftyMiles = 80467.2
|
||||
case oneHundredMiles = 160934
|
||||
case twoHundredMiles = 321869
|
||||
case fiveHundredMiles = 804672
|
||||
case oneThousandMiles = 1609000
|
||||
case twentyFiveHundredMiles = 4023360
|
||||
var id: Double { self.rawValue }
|
||||
var description: String {
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
return "up to \(distanceFormatter.string(fromDistance: Double(self.rawValue))) away"
|
||||
}
|
||||
}
|
||||
|
||||
enum UserTrackingModes: Int, CaseIterable, Identifiable {
|
||||
case none = 0
|
||||
case follow = 1
|
||||
|
|
@ -116,7 +130,7 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
enum MapLayer: String, CaseIterable, Equatable {
|
||||
enum MapLayer: String, CaseIterable, Equatable, Decodable {
|
||||
case standard
|
||||
case hybrid
|
||||
case satellite
|
||||
|
|
@ -124,7 +138,7 @@ enum MapLayer: String, CaseIterable, Equatable {
|
|||
var localized: String { self.rawValue.localized }
|
||||
}
|
||||
|
||||
enum MapTileServer: String, CaseIterable, Identifiable {
|
||||
enum MapTileServer: String, CaseIterable, Identifiable, Decodable {
|
||||
case openStreetMap
|
||||
case openStreetMapDE
|
||||
case openStreetMapFR
|
||||
|
|
@ -259,7 +273,7 @@ enum OverlayType: String, CaseIterable, Equatable {
|
|||
var localized: String { self.rawValue.localized }
|
||||
}
|
||||
|
||||
enum MapOverlayServer: String, CaseIterable, Identifiable {
|
||||
enum MapOverlayServer: String, CaseIterable, Identifiable, Decodable {
|
||||
case baseReReflectivityCurrent
|
||||
case baseReReflectivityOneHourAgo
|
||||
case echoTopsEetCurrent
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
case lostAndFound = 9
|
||||
case sensor = 6
|
||||
case tak = 7
|
||||
case takTracker = 10
|
||||
case repeater = 4
|
||||
case router = 2
|
||||
case routerClient = 3
|
||||
|
|
@ -40,11 +41,14 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
return "Sensor"
|
||||
case .tak:
|
||||
return "TAK"
|
||||
case .takTracker:
|
||||
return "TAK Tracker"
|
||||
case .clientHidden:
|
||||
return "Client Hidden"
|
||||
case .lostAndFound:
|
||||
return "Lost and Found"
|
||||
}
|
||||
|
||||
}
|
||||
var description: String {
|
||||
switch self {
|
||||
|
|
@ -64,6 +68,8 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
return "device.role.sensor".localized
|
||||
case .tak:
|
||||
return "device.role.tak".localized
|
||||
case .takTracker:
|
||||
return "device.role.taktracker".localized
|
||||
case .clientHidden:
|
||||
return "device.role.clienthidden".localized
|
||||
case .lostAndFound:
|
||||
|
|
@ -74,17 +80,21 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
var systemName: String {
|
||||
switch self {
|
||||
case .client:
|
||||
return "iphone.gen3.radiowaves.left.and.right"
|
||||
return "apps.iphone"
|
||||
case .clientMute:
|
||||
return "speaker.slash"
|
||||
case .router, .routerClient, .repeater:
|
||||
case .router, .routerClient:
|
||||
return "wifi.router"
|
||||
case .repeater:
|
||||
return "repeat"
|
||||
case .tracker:
|
||||
return "mappin.and.ellipse.circle"
|
||||
case .sensor:
|
||||
return "sensor"
|
||||
case .tak:
|
||||
return "shield.checkered"
|
||||
case .takTracker:
|
||||
return "dog"
|
||||
case .clientHidden:
|
||||
return "eye.slash"
|
||||
case .lostAndFound:
|
||||
|
|
@ -110,6 +120,8 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
return Config.DeviceConfig.Role.sensor
|
||||
case .tak:
|
||||
return Config.DeviceConfig.Role.tak
|
||||
case .takTracker:
|
||||
return Config.DeviceConfig.Role.takTracker
|
||||
case .clientHidden:
|
||||
return Config.DeviceConfig.Role.clientHidden
|
||||
case .lostAndFound:
|
||||
|
|
|
|||
|
|
@ -13,17 +13,20 @@ enum BubblePosition {
|
|||
|
||||
enum Tapbacks: Int, CaseIterable, Identifiable {
|
||||
|
||||
case heart = 0
|
||||
case thumbsUp = 1
|
||||
case thumbsDown = 2
|
||||
case haHa = 3
|
||||
case exclamation = 4
|
||||
case question = 5
|
||||
case poop = 6
|
||||
case wave = 0
|
||||
case heart = 1
|
||||
case thumbsUp = 2
|
||||
case thumbsDown = 3
|
||||
case haHa = 4
|
||||
case exclamation = 5
|
||||
case question = 6
|
||||
case poop = 7
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var emojiString: String {
|
||||
switch self {
|
||||
case .wave:
|
||||
return "👋"
|
||||
case .heart:
|
||||
return "❤️"
|
||||
case .thumbsUp:
|
||||
|
|
@ -42,6 +45,8 @@ enum Tapbacks: Int, CaseIterable, Identifiable {
|
|||
}
|
||||
var description: String {
|
||||
switch self {
|
||||
case .wave:
|
||||
return "tapback.wave".localized
|
||||
case .heart:
|
||||
return "tapback.heart".localized
|
||||
case .thumbsUp:
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable {
|
|||
|
||||
case thirtySeconds = 30
|
||||
case oneMinute = 60
|
||||
case twoMinutes = 120
|
||||
case fiveMinutes = 300
|
||||
case tenMinutes = 600
|
||||
case fifteenMinutes = 900
|
||||
|
|
@ -74,6 +75,8 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable {
|
|||
return "interval.thirty.seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
case .twoMinutes:
|
||||
return "interval.two.minutes".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
case .tenMinutes:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,36 @@ import MapKit
|
|||
import SwiftUI
|
||||
|
||||
extension PositionEntity {
|
||||
|
||||
static func allPositionsFetchRequest() -> NSFetchRequest<PositionEntity> {
|
||||
let request: NSFetchRequest<PositionEntity> = PositionEntity.fetchRequest()
|
||||
request.fetchLimit = 200
|
||||
//request.fetchBatchSize = 1
|
||||
request.returnsObjectsAsFaults = false
|
||||
request.includesSubentities = true
|
||||
request.returnsDistinctResults = true
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
|
||||
let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate)
|
||||
|
||||
let pointOfInterest = LocationHelper.currentLocation
|
||||
|
||||
if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude {
|
||||
let D: Double = UserDefaults.meshMapDistance * 1.1
|
||||
let R: Double = 6371009
|
||||
let meanLatitidue = pointOfInterest.latitude * .pi / 180
|
||||
let deltaLatitude = D / R * 180 / .pi
|
||||
let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi
|
||||
let minLatitude: Double = pointOfInterest.latitude - deltaLatitude
|
||||
let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude
|
||||
let minLongitude: Double = pointOfInterest.longitude - deltaLongitude
|
||||
let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude
|
||||
let distancePredicate = NSPredicate(format: "(%lf <= (longitudeI / 1e7)) AND ((longitudeI / 1e7) <= %lf) AND (%lf <= (latitudeI / 1e7)) AND ((latitudeI / 1e7) <= %lf)", minLongitude, maxLongitude,minLatitude, maxLatitude)
|
||||
request.predicate = NSCompoundPredicate(type: .and, subpredicates: [positionPredicate, distancePredicate])
|
||||
} else {
|
||||
request.predicate = positionPredicate
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
var latitude: Double? {
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,18 @@ import MapKit
|
|||
import SwiftUI
|
||||
|
||||
extension WaypointEntity {
|
||||
|
||||
static func allWaypointssFetchRequest() -> NSFetchRequest<WaypointEntity> {
|
||||
let request: NSFetchRequest<WaypointEntity> = WaypointEntity.fetchRequest()
|
||||
request.fetchLimit = 50
|
||||
//request.fetchBatchSize = 1
|
||||
//request.returnsObjectsAsFaults = false
|
||||
//request.includesSubentities = true
|
||||
request.returnsDistinctResults = true
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)]
|
||||
request.predicate = NSPredicate(format: "expire == nil || expire >= %@", Date() as NSDate)
|
||||
return request
|
||||
}
|
||||
|
||||
var latitude: Double? {
|
||||
|
||||
|
|
|
|||
|
|
@ -67,4 +67,29 @@ extension String {
|
|||
: $0 + String($1)
|
||||
}
|
||||
}
|
||||
|
||||
var length: Int {
|
||||
return count
|
||||
}
|
||||
|
||||
subscript (i: Int) -> String {
|
||||
return self[i ..< i + 1]
|
||||
}
|
||||
|
||||
func substring(fromIndex: Int) -> String {
|
||||
return self[min(fromIndex, length) ..< length]
|
||||
}
|
||||
|
||||
func substring(toIndex: Int) -> String {
|
||||
return self[0 ..< max(0, toIndex)]
|
||||
}
|
||||
|
||||
subscript (r: Range<Int>) -> String {
|
||||
let range = Range(uncheckedBounds: (lower: max(0, min(length, r.lowerBound)),
|
||||
upper: min(length, max(0, r.upperBound))))
|
||||
let start = index(startIndex, offsetBy: range.lowerBound)
|
||||
let end = index(start, offsetBy: range.upperBound - range.lowerBound)
|
||||
return String(self[start ..< end])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,37 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
@propertyWrapper
|
||||
struct UserDefault<T: Decodable> {
|
||||
let key: UserDefaults.Keys
|
||||
let defaultValue: T
|
||||
|
||||
init(_ key: UserDefaults.Keys, defaultValue: T) {
|
||||
self.key = key
|
||||
self.defaultValue = defaultValue
|
||||
}
|
||||
|
||||
var wrappedValue: T {
|
||||
get {
|
||||
if defaultValue as? any RawRepresentable != nil {
|
||||
let storedValue = UserDefaults.standard.object(forKey: key.rawValue)
|
||||
|
||||
guard let storedValue,
|
||||
let jsonString = (storedValue as? String != nil) ? "\"\(storedValue)\"" : "\(storedValue)",
|
||||
let data = jsonString.data(using: .utf8),
|
||||
let value = (try? JSONDecoder().decode(T.self, from: data)) else { return defaultValue }
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
return UserDefaults.standard.object(forKey: key.rawValue) as? T ?? defaultValue
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set((newValue as? any RawRepresentable)?.rawValue ?? newValue, forKey: key.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UserDefaults {
|
||||
enum Keys: String, CaseIterable {
|
||||
case preferredPeripheralId
|
||||
|
|
@ -14,14 +45,22 @@ extension UserDefaults {
|
|||
case provideLocation
|
||||
case provideLocationInterval
|
||||
case mapLayer
|
||||
case meshMapDistance
|
||||
case enableMapWaypoints
|
||||
case meshMapRecentering
|
||||
case meshMapShowNodeHistory
|
||||
case meshMapShowRouteLines
|
||||
case enableMapConvexHull
|
||||
case enableMapRecentering
|
||||
case enableMapNodeHistoryPins
|
||||
case enableMapRouteLines
|
||||
case enableMapTraffic
|
||||
case enableMapPointsOfInterest
|
||||
case enableOfflineMaps
|
||||
case enableOfflineMapsMBTiles
|
||||
case mapTileServer
|
||||
case enableOverlayServer
|
||||
case mapOverlayServer
|
||||
case mapTilesAboveLabels
|
||||
case mapUseLegacy
|
||||
case enableDetectionNotifications
|
||||
|
|
@ -29,195 +68,94 @@ extension UserDefaults {
|
|||
case enableSmartPosition
|
||||
case modemPreset
|
||||
case firmwareVersion
|
||||
case testIntEnum
|
||||
}
|
||||
|
||||
func reset() {
|
||||
Keys.allCases.forEach { removeObject(forKey: $0.rawValue) }
|
||||
}
|
||||
static var preferredPeripheralId: String {
|
||||
get {
|
||||
UserDefaults.standard.string(forKey: "preferredPeripheralId") ?? ""
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "preferredPeripheralId")
|
||||
}
|
||||
}
|
||||
static var preferredPeripheralNum: Int {
|
||||
get {
|
||||
UserDefaults.standard.integer(forKey: "preferredPeripheralNum")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "preferredPeripheralNum")
|
||||
}
|
||||
}
|
||||
static var provideLocation: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "provideLocation")
|
||||
} set {
|
||||
UserDefaults.standard.set(newValue, forKey: "provideLocation")
|
||||
}
|
||||
}
|
||||
static var provideLocationInterval: Int {
|
||||
get {
|
||||
UserDefaults.standard.integer(forKey: "provideLocationInterval")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "provideLocationInterval")
|
||||
}
|
||||
}
|
||||
static var mapLayer: MapLayer {
|
||||
get {
|
||||
MapLayer(rawValue: UserDefaults.standard.string(forKey: "mapLayer") ?? MapLayer.standard.rawValue) ?? MapLayer.standard
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue.rawValue, forKey: "mapLayer")
|
||||
}
|
||||
}
|
||||
static var enableMapRecentering: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "meshMapRecentering")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "meshMapRecentering")
|
||||
}
|
||||
}
|
||||
static var enableMapNodeHistoryPins: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "meshMapShowNodeHistory")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "meshMapShowNodeHistory")
|
||||
}
|
||||
}
|
||||
static var enableMapRouteLines: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "meshMapShowRouteLines")
|
||||
}
|
||||
set {
|
||||
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")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "enableMapTraffic")
|
||||
}
|
||||
}
|
||||
static var enableMapPointsOfInterest: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableMapPointsOfInterest")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "enableMapPointsOfInterest")
|
||||
}
|
||||
}
|
||||
static var enableOfflineMaps: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableOfflineMaps")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "enableOfflineMaps")
|
||||
}
|
||||
}
|
||||
static var enableOfflineMapsMBTiles: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableOfflineMapsMBTiles")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "enableOfflineMapsMBTiles")
|
||||
}
|
||||
}
|
||||
static var mapTileServer: MapTileServer {
|
||||
get {
|
||||
MapTileServer(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServer.openStreetMap.rawValue) ?? MapTileServer.openStreetMap
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer")
|
||||
}
|
||||
}
|
||||
static var enableOverlayServer: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableOverlayServer")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "enableOverlayServer")
|
||||
}
|
||||
}
|
||||
static var mapOverlayServer: MapOverlayServer {
|
||||
get {
|
||||
MapOverlayServer(rawValue: UserDefaults.standard.string(forKey: "mapOverlayServer") ?? MapOverlayServer.baseReReflectivityCurrent.rawValue) ?? MapOverlayServer.baseReReflectivityCurrent
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue.rawValue, forKey: "mapOverlayServer")
|
||||
}
|
||||
}
|
||||
static var mapTilesAboveLabels: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "mapTilesAboveLabels")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "mapTilesAboveLabels")
|
||||
}
|
||||
}
|
||||
|
||||
@UserDefault(.preferredPeripheralId, defaultValue: "")
|
||||
static var preferredPeripheralId: String
|
||||
|
||||
@UserDefault(.preferredPeripheralNum, defaultValue: 0)
|
||||
static var preferredPeripheralNum: Int
|
||||
|
||||
@UserDefault(.provideLocation, defaultValue: false)
|
||||
static var provideLocation: Bool
|
||||
|
||||
@UserDefault(.provideLocationInterval, defaultValue: 0)
|
||||
static var provideLocationInterval: Int
|
||||
|
||||
@UserDefault(.mapLayer, defaultValue: .standard)
|
||||
static var mapLayer: MapLayer
|
||||
|
||||
@UserDefault(.meshMapDistance, defaultValue: 800000)
|
||||
static var meshMapDistance: Double
|
||||
|
||||
static var mapUseLegacy: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "mapUseLegacy")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "mapUseLegacy")
|
||||
}
|
||||
}
|
||||
@UserDefault(.enableMapWaypoints, defaultValue: false)
|
||||
static var enableMapWaypoints: Bool
|
||||
|
||||
static var enableDetectionNotifications: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableDetectionNotifications")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "enableDetectionNotifications")
|
||||
}
|
||||
}
|
||||
|
||||
static var detectionSensorRole: DetectionSensorRole {
|
||||
get {
|
||||
DetectionSensorRole(rawValue: UserDefaults.standard.string(forKey: "detectionSensorRole") ?? DetectionSensorRole.sensor.rawValue) ?? DetectionSensorRole.sensor
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue.rawValue, forKey: "detectionSensorRole")
|
||||
}
|
||||
}
|
||||
static var enableSmartPosition: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableSmartPosition")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "enableSmartPosition")
|
||||
}
|
||||
}
|
||||
static var modemPreset: Int {
|
||||
get {
|
||||
UserDefaults.standard.integer(forKey: "modemPreset")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "modemPreset")
|
||||
}
|
||||
}
|
||||
static var firmwareVersion: String {
|
||||
get {
|
||||
UserDefaults.standard.string(forKey: "firmwareVersion") ?? "0.0.0"
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "firmwareVersion")
|
||||
}
|
||||
}
|
||||
@UserDefault(.enableMapRecentering, defaultValue: false)
|
||||
static var enableMapRecentering: Bool
|
||||
|
||||
@UserDefault(.enableMapNodeHistoryPins, defaultValue: false)
|
||||
static var enableMapNodeHistoryPins: Bool
|
||||
|
||||
@UserDefault(.enableMapRouteLines, defaultValue: false)
|
||||
static var enableMapRouteLines: Bool
|
||||
|
||||
@UserDefault(.enableMapConvexHull, defaultValue: false)
|
||||
static var enableMapConvexHull: Bool
|
||||
|
||||
@UserDefault(.enableMapTraffic, defaultValue: false)
|
||||
static var enableMapTraffic: Bool
|
||||
|
||||
@UserDefault(.enableMapPointsOfInterest, defaultValue: false)
|
||||
static var enableMapPointsOfInterest: Bool
|
||||
|
||||
@UserDefault(.enableOfflineMaps, defaultValue: false)
|
||||
static var enableOfflineMaps: Bool
|
||||
|
||||
@UserDefault(.enableOfflineMapsMBTiles, defaultValue: false)
|
||||
static var enableOfflineMapsMBTiles: Bool
|
||||
|
||||
@UserDefault(.mapTileServer, defaultValue: .openStreetMap)
|
||||
static var mapTileServer: MapTileServer
|
||||
|
||||
@UserDefault(.enableOverlayServer, defaultValue: false)
|
||||
static var enableOverlayServer: Bool
|
||||
|
||||
@UserDefault(.mapOverlayServer, defaultValue: .baseReReflectivityCurrent)
|
||||
static var mapOverlayServer: MapOverlayServer
|
||||
|
||||
@UserDefault(.mapTilesAboveLabels, defaultValue: false)
|
||||
static var mapTilesAboveLabels: Bool
|
||||
|
||||
@UserDefault(.mapUseLegacy, defaultValue: false)
|
||||
static var mapUseLegacy: Bool
|
||||
|
||||
@UserDefault(.enableDetectionNotifications, defaultValue: false)
|
||||
static var enableDetectionNotifications: Bool
|
||||
|
||||
@UserDefault(.detectionSensorRole, defaultValue: .sensor)
|
||||
static var detectionSensorRole: DetectionSensorRole
|
||||
|
||||
@UserDefault(.enableSmartPosition, defaultValue: false)
|
||||
static var enableSmartPosition: Bool
|
||||
|
||||
@UserDefault(.modemPreset, defaultValue: 0)
|
||||
static var modemPreset: Int
|
||||
|
||||
@UserDefault(.firmwareVersion, defaultValue: "0.0.0")
|
||||
static var firmwareVersion: String
|
||||
|
||||
@UserDefault(.testIntEnum, defaultValue: .one)
|
||||
static var testIntEnum: TestIntEnum
|
||||
}
|
||||
|
||||
enum TestIntEnum: Int, Decodable {
|
||||
case one = 1
|
||||
case two
|
||||
case three
|
||||
}
|
||||
|
|
|
|||
|
|
@ -983,9 +983,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return success
|
||||
}
|
||||
|
||||
public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool {
|
||||
var success = false
|
||||
let fromNodeNum = connectedPeripheral.num
|
||||
public func getPositionFromPhoneGPS(channel: Int32, destNum: Int64) -> Position? {
|
||||
var positionPacket = Position()
|
||||
|
||||
let fetchChannelRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "ChannelEntity")
|
||||
|
|
@ -993,7 +991,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
do {
|
||||
guard let fetchedChannel = try context!.fetch(fetchChannelRequest) as? [ChannelEntity] else {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
|
||||
|
|
@ -1019,8 +1017,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
|
||||
} else {
|
||||
if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 {
|
||||
return false
|
||||
if destNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 {
|
||||
return nil
|
||||
}
|
||||
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
|
||||
|
|
@ -1041,6 +1039,61 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
|
||||
return positionPacket
|
||||
}
|
||||
|
||||
public func setFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool {
|
||||
var adminPacket = AdminMessage()
|
||||
guard let positionPacket = getPositionFromPhoneGPS(channel: channel, destNum: fromUser.num) else {
|
||||
return false
|
||||
}
|
||||
adminPacket.setFixedPosition = positionPacket
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(fromUser.num)
|
||||
meshPacket.from = UInt32(fromUser.num)
|
||||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
meshPacket.priority = MeshPacket.Priority.reliable
|
||||
meshPacket.wantAck = true
|
||||
meshPacket.channel = UInt32(channel)
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
let messageDescription = "🚀 Sent Set Fixed Postion Admin Message to: \(fromUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: fromUser) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func removeFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool {
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.removeFixedPosition = true
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(fromUser.num)
|
||||
meshPacket.from = UInt32(fromUser.num)
|
||||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
meshPacket.priority = MeshPacket.Priority.reliable
|
||||
meshPacket.wantAck = true
|
||||
meshPacket.channel = UInt32(channel)
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
let messageDescription = "🚀 Sent Remove Fixed Position Admin Message to: \(fromUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: fromUser) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool {
|
||||
var success = false
|
||||
let fromNodeNum = connectedPeripheral.num
|
||||
guard let positionPacket = getPositionFromPhoneGPS(channel: channel, destNum: destNum) else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,28 +13,34 @@ import ActivityKit
|
|||
#endif
|
||||
|
||||
func generateMessageMarkdown (message: String) -> String {
|
||||
let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber]
|
||||
let detector = try! NSDataDetector(types: types.rawValue)
|
||||
let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count))
|
||||
var messageWithMarkdown = message
|
||||
if matches.count > 0 {
|
||||
for match in matches {
|
||||
guard let range = Range(match.range, in: message) else { continue }
|
||||
if match.resultType == .address {
|
||||
let address = message[range]
|
||||
let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics)
|
||||
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))")
|
||||
} else if match.resultType == .phoneNumber {
|
||||
let phone = messageWithMarkdown[range]
|
||||
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))")
|
||||
} else if match.resultType == .link {
|
||||
let url = messageWithMarkdown[range]
|
||||
let absoluteUrl = match.url?.absoluteString ?? ""
|
||||
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))\(String(match.url?.path ?? ""))](\(absoluteUrl))")
|
||||
if !message.isEmoji() {
|
||||
let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber]
|
||||
let detector = try! NSDataDetector(types: types.rawValue)
|
||||
let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count))
|
||||
var messageWithMarkdown = message
|
||||
if matches.count > 0 {
|
||||
for match in matches {
|
||||
guard let range = Range(match.range, in: message) else { continue }
|
||||
if match.resultType == .address {
|
||||
let address = message[range]
|
||||
let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics)
|
||||
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))")
|
||||
} else if match.resultType == .phoneNumber {
|
||||
let phone = messageWithMarkdown[range]
|
||||
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))")
|
||||
} else if match.resultType == .link {
|
||||
let start = match.range.lowerBound
|
||||
let stop = match.range.upperBound
|
||||
let url = message[start ..< stop]
|
||||
let absoluteUrl = match.url?.absoluteString ?? ""
|
||||
let markdownUrl = "[\(url)](\(absoluteUrl))"
|
||||
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: markdownUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
return messageWithMarkdown
|
||||
}
|
||||
return messageWithMarkdown
|
||||
return message
|
||||
}
|
||||
|
||||
func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) {
|
||||
|
|
@ -257,6 +263,8 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
newNode.id = Int64(nodeInfo.num)
|
||||
newNode.num = Int64(nodeInfo.num)
|
||||
newNode.channel = Int32(nodeInfo.channel)
|
||||
newNode.favorite = nodeInfo.isFavorite
|
||||
newNode.hopsAway = Int32(nodeInfo.hopsAway)
|
||||
|
||||
if nodeInfo.hasDeviceMetrics {
|
||||
let telemetry = TelemetryEntity(context: context)
|
||||
|
|
@ -340,6 +348,8 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
|
||||
fetchedNode[0].snr = nodeInfo.snr
|
||||
fetchedNode[0].channel = Int32(nodeInfo.channel)
|
||||
fetchedNode[0].favorite = nodeInfo.isFavorite
|
||||
fetchedNode[0].hopsAway = Int32(nodeInfo.hopsAway)
|
||||
|
||||
if nodeInfo.hasUser {
|
||||
if (fetchedNode[0].user == nil) {
|
||||
|
|
@ -724,24 +734,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
)
|
||||
]
|
||||
manager.schedule()
|
||||
|
||||
// let content = UNMutableNotificationContent()
|
||||
// content.title = "Critically Low Battery!"
|
||||
// content.body = "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining."
|
||||
// content.userInfo["target"] = "node"
|
||||
// content.userInfo["path"] = "meshtastic://node/\(telemetry.nodeTelemetry?.num ?? 0)"
|
||||
// let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
// let uuidString = UUID().uuidString
|
||||
// let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger)
|
||||
// let notificationCenter = UNUserNotificationCenter.current()
|
||||
// notificationCenter.add(request) { (error) in
|
||||
// if error != nil {
|
||||
// // Handle any errors.
|
||||
// print("Error creating local low battery notification: \(error?.localizedDescription ?? "no description")")
|
||||
// } else {
|
||||
// print("Created local low battery notification.")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// Update our live activity if there is one running, not available on mac iOS >= 16.2
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ class MqttClientProxyManager {
|
|||
defaultServerPort = Int(fullHost.components(separatedBy: ":")[1]) ?? (useSsl ? 8883 : 1883)
|
||||
}
|
||||
}
|
||||
let minimumVersion = "2.3.0"
|
||||
let latestVersion = minimumVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame
|
||||
let minimumVersion = "2.3.2"
|
||||
let currentVersion = UserDefaults.firmwareVersion
|
||||
let supportedVersion = minimumVersion.compare(currentVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(currentVersion, options: .numeric) == .orderedSame
|
||||
|
||||
if let host = host {
|
||||
let port = defaultServerPort
|
||||
|
|
@ -45,7 +46,7 @@ class MqttClientProxyManager {
|
|||
let password = node.mqttConfig?.password
|
||||
let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh"
|
||||
let prefix = root!
|
||||
topic = prefix + (latestVersion ? "/2/e" : "/2/c") + "/#"
|
||||
topic = prefix + (supportedVersion ? "/2/e" : "/2/c") + "/#"
|
||||
let qos = CocoaMQTTQoS(rawValue: UInt8(1))!
|
||||
connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic, qos: qos, cleanSession: true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV 30.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV 31.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="22522" systemVersion="23E5211a" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23E5211a" 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"/>
|
||||
|
|
@ -448,5 +448,10 @@
|
|||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -0,0 +1,461 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23E5211a" 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="positionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
|
||||
<attribute name="psk" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" 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="DeviceHardwareEntity" representedClassName="DeviceHardwareEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="activelySupported" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="architecture" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="displayName" optional="YES" attributeType="String"/>
|
||||
<attribute name="hwModel" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hwModelSlug" optional="YES" attributeType="String"/>
|
||||
<attribute name="platformioTarget" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hwModel" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="units" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="useI2SAsBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
|
||||
<fetchedProperty name="fetchedProperty" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="ExternalNotificationConfigEntity"/>
|
||||
</fetchedProperty>
|
||||
</entity>
|
||||
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
|
||||
</entity>
|
||||
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ignoreMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
|
||||
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="read" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<fetchedProperty name="tapbacks" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="messageId"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="mapPositionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="13" usesScalarValueType="YES"/>
|
||||
<attribute name="mapPublishIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mapReportingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
|
||||
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
|
||||
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
|
||||
<fetchedProperty name="allMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="toUser == nil"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="myNodeNum"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ntpServer" optional="YES" attributeType="String"/>
|
||||
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
|
||||
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
|
||||
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="favorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="hopsAway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="viaMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
|
||||
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
|
||||
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
|
||||
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
|
||||
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
|
||||
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
|
||||
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
|
||||
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
|
||||
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
|
||||
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
|
||||
<relationship name="pax" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PaxCounterEntity" inverseName="paxNode" inverseEntity="PaxCounterEntity"/>
|
||||
<relationship name="paxCounterConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PaxCounterConfigEntity" inverseName="paxCounterConfigNode" inverseEntity="PaxCounterConfigEntity"/>
|
||||
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
|
||||
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
|
||||
<relationship name="powerConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PowerConfigEntity" inverseName="powerConfigNode" inverseEntity="PowerConfigEntity"/>
|
||||
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
|
||||
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
|
||||
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
|
||||
<relationship name="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
|
||||
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
|
||||
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
|
||||
<relationship name="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
|
||||
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="num"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PaxCounterConfigEntity" representedClassName="PaxCounterConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="paxcounterUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="paxCounterConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="paxCounterConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PaxCounterEntity" representedClassName="PaxCounterEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ble" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="uptime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="paxNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="pax" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsEnGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="precisionBits" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PowerConfigEntity" representedClassName="PowerConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adcMultiplierOverride" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceBatteryInaAddress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isPowerSaving" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="lsSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="minWakeSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="onBatteryShutdownAfterSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="waitBluetoothSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="powerConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="powerConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="color" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="notes" optional="YES" attributeType="String"/>
|
||||
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
|
||||
</entity>
|
||||
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
|
||||
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideConsoleSerialPort" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isRouter" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeartbeat" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="lastRequest" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="powerMeasurementEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="powerScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="powerUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="route" optional="YES" attributeType="Transformable" customClassName="[UInt32]"/>
|
||||
<attribute name="routeText" optional="YES" attributeType="String"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
|
||||
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
|
||||
</entity>
|
||||
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hwModel" attributeType="String"/>
|
||||
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="longName" attributeType="String"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" attributeType="Integer 32" 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"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -144,11 +144,15 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
newNode.snr = packet.rxSnr
|
||||
newNode.rssi = packet.rxRssi
|
||||
newNode.viaMqtt = packet.viaMqtt
|
||||
|
||||
if packet.to == 4294967295 || packet.to == UserDefaults.preferredPeripheralNum {
|
||||
newNode.channel = Int32(packet.channel)
|
||||
}
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
|
||||
newNode.hopsAway = Int32(truncatingIfNeeded: nodeInfoMessage.hopsAway)
|
||||
newNode.hopsAway = Int32(nodeInfoMessage.hopsAway)
|
||||
newNode.favorite = nodeInfoMessage.isFavorite
|
||||
} else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart {
|
||||
newNode.hopsAway = Int32(packet.hopStart - packet.hopLimit)
|
||||
}
|
||||
if let newUserMessage = try? User(serializedData: packet.decoded.payload) {
|
||||
|
||||
|
|
@ -218,8 +222,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
}
|
||||
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
|
||||
fetchedNode[0].channel = Int32(nodeInfoMessage.channel)
|
||||
fetchedNode[0].hopsAway = Int32(truncatingIfNeeded: nodeInfoMessage.hopsAway)
|
||||
|
||||
fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway)
|
||||
fetchedNode[0].favorite = nodeInfoMessage.isFavorite
|
||||
if nodeInfoMessage.hasDeviceMetrics {
|
||||
let telemetry = TelemetryEntity(context: context)
|
||||
telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel)
|
||||
|
|
@ -231,6 +236,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries)
|
||||
}
|
||||
if nodeInfoMessage.hasUser {
|
||||
fetchedNode[0].user!.vip = nodeInfoMessage.isFavorite
|
||||
/// Seeing Some crashes here ?
|
||||
fetchedNode[0].user!.userId = nodeInfoMessage.user.id
|
||||
fetchedNode[0].user!.num = Int64(nodeInfoMessage.num)
|
||||
|
|
@ -239,6 +245,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue)
|
||||
fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased()
|
||||
}
|
||||
} else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart {
|
||||
fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit)
|
||||
}
|
||||
if (fetchedNode[0].user == nil) {
|
||||
let newUser = UserEntity(context: context)
|
||||
|
|
@ -317,9 +325,9 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
return
|
||||
}
|
||||
/// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one.
|
||||
if mutablePositions.count > 0 && position.precisionBits == 32 {
|
||||
if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) {
|
||||
let mostRecent = mutablePositions.lastObject as! PositionEntity
|
||||
if mostRecent.coordinate.distance(from: position.coordinate) < 15 {
|
||||
if mostRecent.coordinate.distance(from: position.coordinate) < 15.0 {
|
||||
mutablePositions.remove(mostRecent)
|
||||
}
|
||||
} else if mutablePositions.count > 0 && 11...16 ~= position.precisionBits {
|
||||
|
|
@ -1111,6 +1119,9 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no
|
|||
newMQTTConfig.encryptionEnabled = config.encryptionEnabled
|
||||
newMQTTConfig.jsonEnabled = config.jsonEnabled
|
||||
newMQTTConfig.tlsEnabled = config.tlsEnabled
|
||||
newMQTTConfig.mapReportingEnabled = config.mapReportingEnabled
|
||||
newMQTTConfig.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision)
|
||||
newMQTTConfig.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs)
|
||||
fetchedNode[0].mqttConfig = newMQTTConfig
|
||||
} else {
|
||||
fetchedNode[0].mqttConfig?.enabled = config.enabled
|
||||
|
|
@ -1122,6 +1133,9 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no
|
|||
fetchedNode[0].mqttConfig?.encryptionEnabled = config.encryptionEnabled
|
||||
fetchedNode[0].mqttConfig?.jsonEnabled = config.jsonEnabled
|
||||
fetchedNode[0].mqttConfig?.tlsEnabled = config.tlsEnabled
|
||||
fetchedNode[0].mqttConfig?.mapReportingEnabled = config.mapReportingEnabled
|
||||
fetchedNode[0].mqttConfig?.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision)
|
||||
fetchedNode[0].mqttConfig?.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs)
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
|
|
|
|||
|
|
@ -319,6 +319,46 @@ struct AdminMessage {
|
|||
set {payloadVariant = .removeByNodenum(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// Set specified node-num to be favorited on the NodeDB on the device
|
||||
var setFavoriteNode: UInt32 {
|
||||
get {
|
||||
if case .setFavoriteNode(let v)? = payloadVariant {return v}
|
||||
return 0
|
||||
}
|
||||
set {payloadVariant = .setFavoriteNode(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// Set specified node-num to be un-favorited on the NodeDB on the device
|
||||
var removeFavoriteNode: UInt32 {
|
||||
get {
|
||||
if case .removeFavoriteNode(let v)? = payloadVariant {return v}
|
||||
return 0
|
||||
}
|
||||
set {payloadVariant = .removeFavoriteNode(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// Set fixed position data on the node and then set the position.fixed_position = true
|
||||
var setFixedPosition: Position {
|
||||
get {
|
||||
if case .setFixedPosition(let v)? = payloadVariant {return v}
|
||||
return Position()
|
||||
}
|
||||
set {payloadVariant = .setFixedPosition(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// Clear fixed position coordinates and then set position.fixed_position = false
|
||||
var removeFixedPosition: Bool {
|
||||
get {
|
||||
if case .removeFixedPosition(let v)? = payloadVariant {return v}
|
||||
return false
|
||||
}
|
||||
set {payloadVariant = .removeFixedPosition(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// Begins an edit transaction for config, module config, owner, and channel settings changes
|
||||
/// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings)
|
||||
|
|
@ -498,6 +538,18 @@ struct AdminMessage {
|
|||
/// Remove the node by the specified node-num from the NodeDB on the device
|
||||
case removeByNodenum(UInt32)
|
||||
///
|
||||
/// Set specified node-num to be favorited on the NodeDB on the device
|
||||
case setFavoriteNode(UInt32)
|
||||
///
|
||||
/// Set specified node-num to be un-favorited on the NodeDB on the device
|
||||
case removeFavoriteNode(UInt32)
|
||||
///
|
||||
/// Set fixed position data on the node and then set the position.fixed_position = true
|
||||
case setFixedPosition(Position)
|
||||
///
|
||||
/// Clear fixed position coordinates and then set position.fixed_position = false
|
||||
case removeFixedPosition(Bool)
|
||||
///
|
||||
/// Begins an edit transaction for config, module config, owner, and channel settings changes
|
||||
/// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings)
|
||||
case beginEditSettings(Bool)
|
||||
|
|
@ -643,6 +695,22 @@ struct AdminMessage {
|
|||
guard case .removeByNodenum(let l) = lhs, case .removeByNodenum(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
case (.setFavoriteNode, .setFavoriteNode): return {
|
||||
guard case .setFavoriteNode(let l) = lhs, case .setFavoriteNode(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
case (.removeFavoriteNode, .removeFavoriteNode): return {
|
||||
guard case .removeFavoriteNode(let l) = lhs, case .removeFavoriteNode(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
case (.setFixedPosition, .setFixedPosition): return {
|
||||
guard case .setFixedPosition(let l) = lhs, case .setFixedPosition(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
case (.removeFixedPosition, .removeFixedPosition): return {
|
||||
guard case .removeFixedPosition(let l) = lhs, case .removeFixedPosition(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
case (.beginEditSettings, .beginEditSettings): return {
|
||||
guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
|
|
@ -978,6 +1046,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
|
|||
36: .standard(proto: "set_canned_message_module_messages"),
|
||||
37: .standard(proto: "set_ringtone_message"),
|
||||
38: .standard(proto: "remove_by_nodenum"),
|
||||
39: .standard(proto: "set_favorite_node"),
|
||||
40: .standard(proto: "remove_favorite_node"),
|
||||
41: .standard(proto: "set_fixed_position"),
|
||||
42: .standard(proto: "remove_fixed_position"),
|
||||
64: .standard(proto: "begin_edit_settings"),
|
||||
65: .standard(proto: "commit_edit_settings"),
|
||||
95: .standard(proto: "reboot_ota_seconds"),
|
||||
|
|
@ -1278,6 +1350,43 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
|
|||
self.payloadVariant = .removeByNodenum(v)
|
||||
}
|
||||
}()
|
||||
case 39: try {
|
||||
var v: UInt32?
|
||||
try decoder.decodeSingularUInt32Field(value: &v)
|
||||
if let v = v {
|
||||
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .setFavoriteNode(v)
|
||||
}
|
||||
}()
|
||||
case 40: try {
|
||||
var v: UInt32?
|
||||
try decoder.decodeSingularUInt32Field(value: &v)
|
||||
if let v = v {
|
||||
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .removeFavoriteNode(v)
|
||||
}
|
||||
}()
|
||||
case 41: try {
|
||||
var v: Position?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .setFixedPosition(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .setFixedPosition(v)
|
||||
}
|
||||
}()
|
||||
case 42: try {
|
||||
var v: Bool?
|
||||
try decoder.decodeSingularBoolField(value: &v)
|
||||
if let v = v {
|
||||
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .removeFixedPosition(v)
|
||||
}
|
||||
}()
|
||||
case 64: try {
|
||||
var v: Bool?
|
||||
try decoder.decodeSingularBoolField(value: &v)
|
||||
|
|
@ -1465,6 +1574,22 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
|
|||
guard case .removeByNodenum(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 38)
|
||||
}()
|
||||
case .setFavoriteNode?: try {
|
||||
guard case .setFavoriteNode(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 39)
|
||||
}()
|
||||
case .removeFavoriteNode?: try {
|
||||
guard case .removeFavoriteNode(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 40)
|
||||
}()
|
||||
case .setFixedPosition?: try {
|
||||
guard case .setFixedPosition(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 41)
|
||||
}()
|
||||
case .removeFixedPosition?: try {
|
||||
guard case .removeFixedPosition(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 42)
|
||||
}()
|
||||
case .beginEditSettings?: try {
|
||||
guard case .beginEditSettings(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 64)
|
||||
|
|
|
|||
|
|
@ -269,6 +269,14 @@ struct NodeInfoLite {
|
|||
set {_uniqueStorage()._hopsAway = newValue}
|
||||
}
|
||||
|
||||
///
|
||||
/// True if node is in our favorites list
|
||||
/// Persists between NodeDB internal clean ups
|
||||
var isFavorite: Bool {
|
||||
get {return _storage._isFavorite}
|
||||
set {_uniqueStorage()._isFavorite = newValue}
|
||||
}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
|
@ -397,35 +405,6 @@ struct OEMStore {
|
|||
fileprivate var _oemLocalModuleConfig: LocalModuleConfig? = nil
|
||||
}
|
||||
|
||||
///
|
||||
/// RemoteHardwarePins associated with a node
|
||||
struct NodeRemoteHardwarePin {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
///
|
||||
/// The node_num exposing the available gpio pin
|
||||
var nodeNum: UInt32 = 0
|
||||
|
||||
///
|
||||
/// The the available gpio pin for usage with RemoteHardware module
|
||||
var pin: RemoteHardwarePin {
|
||||
get {return _pin ?? RemoteHardwarePin()}
|
||||
set {_pin = newValue}
|
||||
}
|
||||
/// Returns true if `pin` has been explicitly set.
|
||||
var hasPin: Bool {return self._pin != nil}
|
||||
/// Clears the value of `pin`. Subsequent reads from it will return its default value.
|
||||
mutating func clearPin() {self._pin = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _pin: RemoteHardwarePin? = nil
|
||||
}
|
||||
|
||||
#if swift(>=5.5) && canImport(_Concurrency)
|
||||
extension ScreenFonts: @unchecked Sendable {}
|
||||
extension DeviceState: @unchecked Sendable {}
|
||||
|
|
@ -433,7 +412,6 @@ extension NodeInfoLite: @unchecked Sendable {}
|
|||
extension PositionLite: @unchecked Sendable {}
|
||||
extension ChannelFile: @unchecked Sendable {}
|
||||
extension OEMStore: @unchecked Sendable {}
|
||||
extension NodeRemoteHardwarePin: @unchecked Sendable {}
|
||||
#endif // swift(>=5.5) && canImport(_Concurrency)
|
||||
|
||||
// MARK: - Code below here is support for the SwiftProtobuf runtime.
|
||||
|
|
@ -600,6 +578,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
|
|||
7: .same(proto: "channel"),
|
||||
8: .standard(proto: "via_mqtt"),
|
||||
9: .standard(proto: "hops_away"),
|
||||
10: .standard(proto: "is_favorite"),
|
||||
]
|
||||
|
||||
fileprivate class _StorageClass {
|
||||
|
|
@ -612,6 +591,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
|
|||
var _channel: UInt32 = 0
|
||||
var _viaMqtt: Bool = false
|
||||
var _hopsAway: UInt32 = 0
|
||||
var _isFavorite: Bool = false
|
||||
|
||||
static let defaultInstance = _StorageClass()
|
||||
|
||||
|
|
@ -627,6 +607,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
|
|||
_channel = source._channel
|
||||
_viaMqtt = source._viaMqtt
|
||||
_hopsAway = source._hopsAway
|
||||
_isFavorite = source._isFavorite
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -654,6 +635,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
|
|||
case 7: try { try decoder.decodeSingularUInt32Field(value: &_storage._channel) }()
|
||||
case 8: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }()
|
||||
case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }()
|
||||
case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -693,6 +675,9 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
|
|||
if _storage._hopsAway != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9)
|
||||
}
|
||||
if _storage._isFavorite != false {
|
||||
try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10)
|
||||
}
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
|
@ -711,6 +696,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
|
|||
if _storage._channel != rhs_storage._channel {return false}
|
||||
if _storage._viaMqtt != rhs_storage._viaMqtt {return false}
|
||||
if _storage._hopsAway != rhs_storage._hopsAway {return false}
|
||||
if _storage._isFavorite != rhs_storage._isFavorite {return false}
|
||||
return true
|
||||
}
|
||||
if !storagesAreEqual {return false}
|
||||
|
|
@ -891,45 +877,3 @@ extension OEMStore: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension NodeRemoteHardwarePin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".NodeRemoteHardwarePin"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .standard(proto: "node_num"),
|
||||
2: .same(proto: "pin"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.nodeNum) }()
|
||||
case 2: try { try decoder.decodeSingularMessageField(value: &self._pin) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
if self.nodeNum != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.nodeNum, fieldNumber: 1)
|
||||
}
|
||||
try { if let v = self._pin {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: NodeRemoteHardwarePin, rhs: NodeRemoteHardwarePin) -> Bool {
|
||||
if lhs.nodeNum != rhs.nodeNum {return false}
|
||||
if lhs._pin != rhs._pin {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1861,6 +1861,14 @@ struct NodeInfo {
|
|||
set {_uniqueStorage()._hopsAway = newValue}
|
||||
}
|
||||
|
||||
///
|
||||
/// True if node is in our favorites list
|
||||
/// Persists between NodeDB internal clean ups
|
||||
var isFavorite: Bool {
|
||||
get {return _storage._isFavorite}
|
||||
set {_uniqueStorage()._isFavorite = newValue}
|
||||
}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
|
@ -2611,6 +2619,35 @@ struct Heartbeat {
|
|||
init() {}
|
||||
}
|
||||
|
||||
///
|
||||
/// RemoteHardwarePins associated with a node
|
||||
struct NodeRemoteHardwarePin {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
///
|
||||
/// The node_num exposing the available gpio pin
|
||||
var nodeNum: UInt32 = 0
|
||||
|
||||
///
|
||||
/// The the available gpio pin for usage with RemoteHardware module
|
||||
var pin: RemoteHardwarePin {
|
||||
get {return _pin ?? RemoteHardwarePin()}
|
||||
set {_pin = newValue}
|
||||
}
|
||||
/// Returns true if `pin` has been explicitly set.
|
||||
var hasPin: Bool {return self._pin != nil}
|
||||
/// Clears the value of `pin`. Subsequent reads from it will return its default value.
|
||||
mutating func clearPin() {self._pin = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _pin: RemoteHardwarePin? = nil
|
||||
}
|
||||
|
||||
#if swift(>=5.5) && canImport(_Concurrency)
|
||||
extension HardwareModel: @unchecked Sendable {}
|
||||
extension Constants: @unchecked Sendable {}
|
||||
|
|
@ -2645,6 +2682,7 @@ extension NeighborInfo: @unchecked Sendable {}
|
|||
extension Neighbor: @unchecked Sendable {}
|
||||
extension DeviceMetadata: @unchecked Sendable {}
|
||||
extension Heartbeat: @unchecked Sendable {}
|
||||
extension NodeRemoteHardwarePin: @unchecked Sendable {}
|
||||
#endif // swift(>=5.5) && canImport(_Concurrency)
|
||||
|
||||
// MARK: - Code below here is support for the SwiftProtobuf runtime.
|
||||
|
|
@ -3647,6 +3685,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
7: .same(proto: "channel"),
|
||||
8: .standard(proto: "via_mqtt"),
|
||||
9: .standard(proto: "hops_away"),
|
||||
10: .standard(proto: "is_favorite"),
|
||||
]
|
||||
|
||||
fileprivate class _StorageClass {
|
||||
|
|
@ -3659,6 +3698,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
var _channel: UInt32 = 0
|
||||
var _viaMqtt: Bool = false
|
||||
var _hopsAway: UInt32 = 0
|
||||
var _isFavorite: Bool = false
|
||||
|
||||
static let defaultInstance = _StorageClass()
|
||||
|
||||
|
|
@ -3674,6 +3714,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
_channel = source._channel
|
||||
_viaMqtt = source._viaMqtt
|
||||
_hopsAway = source._hopsAway
|
||||
_isFavorite = source._isFavorite
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3701,6 +3742,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
case 7: try { try decoder.decodeSingularUInt32Field(value: &_storage._channel) }()
|
||||
case 8: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }()
|
||||
case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }()
|
||||
case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -3740,6 +3782,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
if _storage._hopsAway != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9)
|
||||
}
|
||||
if _storage._isFavorite != false {
|
||||
try visitor.visitSingularBoolField(value: _storage._isFavorite, fieldNumber: 10)
|
||||
}
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
|
@ -3758,6 +3803,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
|
|||
if _storage._channel != rhs_storage._channel {return false}
|
||||
if _storage._viaMqtt != rhs_storage._viaMqtt {return false}
|
||||
if _storage._hopsAway != rhs_storage._hopsAway {return false}
|
||||
if _storage._isFavorite != rhs_storage._isFavorite {return false}
|
||||
return true
|
||||
}
|
||||
if !storagesAreEqual {return false}
|
||||
|
|
@ -4595,3 +4641,45 @@ extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension NodeRemoteHardwarePin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".NodeRemoteHardwarePin"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .standard(proto: "node_num"),
|
||||
2: .same(proto: "pin"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.nodeNum) }()
|
||||
case 2: try { try decoder.decodeSingularMessageField(value: &self._pin) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
if self.nodeNum != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.nodeNum, fieldNumber: 1)
|
||||
}
|
||||
try { if let v = self._pin {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: NodeRemoteHardwarePin, rhs: NodeRemoteHardwarePin) -> Bool {
|
||||
if lhs.nodeNum != rhs.nodeNum {return false}
|
||||
if lhs._pin != rhs._pin {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,3 +42,20 @@ struct CreateChannelsTip: Tip {
|
|||
Image(systemName: "fibrechannel")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct AdminChannelTip: Tip {
|
||||
|
||||
var id: String {
|
||||
return "tip.channel.admin"
|
||||
}
|
||||
var title: Text {
|
||||
Text("tip.channel.admin.title")
|
||||
}
|
||||
var message: Text? {
|
||||
Text("tip.channel.admin.message")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "fibrechannel")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ struct UserMessageList: View {
|
|||
if message.realACK {
|
||||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").font(.caption2).foregroundColor(.gray)
|
||||
} else {
|
||||
Text("Implicit ACK from another node").font(.caption2).foregroundColor(.orange)
|
||||
Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange)
|
||||
}
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
//
|
||||
// MeshMapContent.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 3/17/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MeshMapContent: MapContent {
|
||||
|
||||
/// Parameters
|
||||
@Binding var showUserLocation: Bool
|
||||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
|
||||
@AppStorage("enableMapConvexHull") private var showConvexHull = false
|
||||
@Binding var showTraffic: Bool
|
||||
@Binding var showPointsOfInterest: Bool
|
||||
@Binding var selectedMapLayer: MapLayer
|
||||
// Map Configuration
|
||||
@Binding var selectedPosition: PositionEntity?
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = false
|
||||
@Binding var selectedWaypoint: WaypointEntity?
|
||||
|
||||
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn)
|
||||
var positions: FetchedResults<PositionEntity>
|
||||
|
||||
@FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none)
|
||||
var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
|
||||
predicate: NSPredicate(format: "enabled == true", ""), animation: .none)
|
||||
private var routes: FetchedResults<RouteEntity>
|
||||
|
||||
var delay: Double = 0
|
||||
@State private var scale: CGFloat = 0.5
|
||||
|
||||
@MapContentBuilder
|
||||
var meshMap: some MapContent {
|
||||
let lineCoords = Array(positions).compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
})
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
if lineCoords.count > 0 {
|
||||
let hull = lineCoords.getConvexHull()
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(.blue, lineWidth: 3)
|
||||
.foregroundStyle(.indigo.opacity(0.4))
|
||||
}
|
||||
}
|
||||
/// Position Annotations
|
||||
ForEach(Array(positions), id: \.id) { position in
|
||||
/// Node color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
/// Latest Position Anotations
|
||||
Annotation(position.nodePosition?.user?.longName ?? "?", coordinate: position.coordinate) {
|
||||
LazyVStack {
|
||||
ZStack {
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
if position.nodePosition?.isOnline ?? false {
|
||||
Circle()
|
||||
.fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5)))
|
||||
.foregroundStyle(Color(nodeColor.lighter()).opacity(0.3))
|
||||
.scaleEffect(scale)
|
||||
.animation(
|
||||
Animation.easeInOut(duration: 0.6)
|
||||
.repeatForever().delay(delay), value: scale
|
||||
)
|
||||
.onAppear {
|
||||
self.scale = 1
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
if position.nodePosition?.hasDetectionSensorMetrics ?? false {
|
||||
Image(systemName: "sensor.fill")
|
||||
.symbolRenderingMode(.palette)
|
||||
.symbolEffect(.variableColor)
|
||||
.padding()
|
||||
.foregroundStyle(.white)
|
||||
.background(Color(nodeColor))
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture { location in
|
||||
selectedPosition = (selectedPosition == position ? nil : position)
|
||||
}
|
||||
}
|
||||
|
||||
/// Node History and Route Lines for favorites
|
||||
if position.nodePosition?.user?.vip ?? false {
|
||||
if showRouteLines {
|
||||
let nodePositions = Array(position.nodePosition!.positions!) as! [PositionEntity]
|
||||
let routeCoords = nodePositions.compactMap({(pos) -> CLLocationCoordinate2D in
|
||||
return pos.nodeCoordinate ?? LocationHelper.DefaultLocation
|
||||
})
|
||||
let gradient = LinearGradient(
|
||||
colors: [Color(nodeColor.lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)],
|
||||
startPoint: .leading, endPoint: .trailing
|
||||
)
|
||||
let dashed = StrokeStyle(
|
||||
lineWidth: 3,
|
||||
lineCap: .round, lineJoin: .round, dash: [10, 10]
|
||||
)
|
||||
MapPolyline(coordinates: routeCoords)
|
||||
.stroke(gradient, style: dashed)
|
||||
}
|
||||
if showNodeHistory {
|
||||
ForEach(Array(position.nodePosition!.positions!) as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
|
||||
if mappin.latest == false && mappin.nodePosition?.user?.vip ?? false {
|
||||
let pf = PositionFlags(rawValue: Int(mappin.nodePosition?.metadata?.positionFlags ?? 771))
|
||||
let headingDegrees = Angle.degrees(Double(mappin.heading))
|
||||
Annotation("", coordinate: mappin.coordinate) {
|
||||
LazyVStack {
|
||||
if pf.contains(.Heading) {
|
||||
Image(systemName: "location.north.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundStyle(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white)
|
||||
.background(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))))
|
||||
.clipShape(Circle())
|
||||
.rotationEffect(headingDegrees)
|
||||
.frame(width: 16, height: 16)
|
||||
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))))
|
||||
.strokeBorder(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white ,lineWidth: 2)
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
.annotationTitles(.hidden)
|
||||
.annotationSubtitles(.hidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Reduced Precision Map Circles
|
||||
if 11...16 ~= position.precisionBits {
|
||||
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
|
||||
let radius : CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
if radius > 0.0 {
|
||||
MapCircle(center: position.coordinate, radius: radius)
|
||||
.foregroundStyle(Color(nodeColor).opacity(0.25))
|
||||
.stroke(.white, lineWidth: 2)
|
||||
}
|
||||
}
|
||||
/// Routes
|
||||
ForEach(Array(routes)) { route in
|
||||
let routeLocations = Array(route.locations!) as! [LocationEntity]
|
||||
let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in
|
||||
return loc.locationCoordinate ?? LocationHelper.DefaultLocation
|
||||
})
|
||||
Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(.green))
|
||||
.strokeBorder(.white, lineWidth: 3)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(.black))
|
||||
.strokeBorder(.white, lineWidth: 3)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
let solid = StrokeStyle(
|
||||
lineWidth: 3,
|
||||
lineCap: .round, lineJoin: .round
|
||||
)
|
||||
MapPolyline(coordinates: routeCoords)
|
||||
.stroke(Color(UIColor(hex: UInt32(route.color))), style: solid)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Waypoint Annotations
|
||||
if waypoints.count > 0 && showWaypoints {
|
||||
ForEach(Array(waypoints) as! [WaypointEntity], id: \.self) { waypoint in
|
||||
Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) {
|
||||
LazyVStack {
|
||||
ZStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40)
|
||||
.onTapGesture(perform: { location in
|
||||
selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MapContentBuilder
|
||||
var body: some MapContent {
|
||||
meshMap
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import CoreData
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct NodeMapContent: MapContent {
|
||||
|
|
@ -16,10 +17,12 @@ struct NodeMapContent: MapContent {
|
|||
/// Map State User Defaults
|
||||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = false
|
||||
@AppStorage("enableMapConvexHull") private var showConvexHull = false
|
||||
@AppStorage("enableMapTraffic") private var showTraffic: Bool = false
|
||||
@AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false
|
||||
@AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid
|
||||
|
||||
// Map Configuration
|
||||
@Namespace var mapScope
|
||||
@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true)
|
||||
|
|
@ -29,13 +32,8 @@ struct NodeMapContent: MapContent {
|
|||
@State var isShowingAltitude = false
|
||||
@State var isEditingSettings = false
|
||||
@State var selectedPosition: PositionEntity?
|
||||
@State var showWaypoints = false
|
||||
@State var selectedWaypoint: WaypointEntity?
|
||||
@State var isMeshMap = false
|
||||
|
||||
//let region: MKCoordinateRegion
|
||||
|
||||
|
||||
@MapContentBuilder
|
||||
var nodeMap: some MapContent {
|
||||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
|
|
@ -45,7 +43,6 @@ struct NodeMapContent: MapContent {
|
|||
/// Node Color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(node.num))
|
||||
|
||||
|
||||
/// Node Annotations
|
||||
ForEach(positionArray, id: \.id) { position in
|
||||
let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771))
|
||||
|
|
@ -81,7 +78,6 @@ struct NodeMapContent: MapContent {
|
|||
MapPolyline(coordinates: lineCoords)
|
||||
.stroke(gradient, style: dashed)
|
||||
}
|
||||
|
||||
/// Node Annotations
|
||||
ForEach(positionArray, id: \.id) { position in
|
||||
Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) {
|
||||
|
|
@ -13,12 +13,14 @@ import MapKit
|
|||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MapSettingsForm: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Binding var nodeHistory: Bool
|
||||
@Binding var routeLines: Bool
|
||||
@Binding var convexHull: Bool
|
||||
@AppStorage("meshMapShowNodeHistory") private var nodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var routeLines = false
|
||||
@AppStorage("enableMapConvexHull") private var convexHull = false
|
||||
@AppStorage("enableMapWaypoints") private var waypoints = false
|
||||
@Binding var traffic: Bool
|
||||
@Binding var pointsOfInterest: Bool
|
||||
@Binding var mapLayer: MapLayer
|
||||
@AppStorage("meshMapDistance") private var meshMapDistance: Double = 800000
|
||||
@Binding var meshMap: Bool
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -39,24 +41,46 @@ struct MapSettingsForm: View {
|
|||
.onChange(of: mapLayer) { newMapLayer in
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
}
|
||||
if !meshMap {
|
||||
Toggle(isOn: $nodeHistory) {
|
||||
Label("Node History", systemImage: "building.columns.fill")
|
||||
if meshMap {
|
||||
HStack {
|
||||
Label("Show nodes", systemImage: "lines.measurement.horizontal")
|
||||
Picker("", selection: $meshMapDistance) {
|
||||
ForEach(MeshMapDistances.allCases) { di in
|
||||
Text(di.description)
|
||||
.tag(di.id)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
.onChange(of: meshMapDistance) { newMeshMapDistance in
|
||||
UserDefaults.meshMapDistance = newMeshMapDistance
|
||||
}
|
||||
Toggle(isOn: $waypoints) {
|
||||
Label("Show Waypoints ", systemImage: "signpost.right.and.left")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.nodeHistory.toggle()
|
||||
UserDefaults.enableMapNodeHistoryPins = self.nodeHistory
|
||||
}
|
||||
Toggle(isOn: $routeLines) {
|
||||
Label("Route Lines", systemImage: "road.lanes")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.routeLines.toggle()
|
||||
UserDefaults.enableMapRouteLines = self.routeLines
|
||||
UserDefaults.enableMapWaypoints = !waypoints
|
||||
}
|
||||
}
|
||||
|
||||
Toggle(isOn: $nodeHistory) {
|
||||
Label("Node History", systemImage: "building.columns.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.nodeHistory.toggle()
|
||||
UserDefaults.enableMapNodeHistoryPins = self.nodeHistory
|
||||
}
|
||||
Toggle(isOn: $routeLines) {
|
||||
Label("Route Lines", systemImage: "road.lanes")
|
||||
}
|
||||
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.onTapGesture {
|
||||
self.routeLines.toggle()
|
||||
UserDefaults.enableMapRouteLines = self.routeLines
|
||||
}
|
||||
Toggle(isOn: $convexHull) {
|
||||
Label("Convex Hull", systemImage: "button.angledbottom.horizontal.right")
|
||||
}
|
||||
|
|
@ -97,7 +121,7 @@ Spacer()
|
|||
.padding(.bottom)
|
||||
#endif
|
||||
}
|
||||
.presentationDetents([.fraction(0.45), .fraction(0.65)])
|
||||
.presentationDetents([.fraction(meshMap ? 0.55 : 0.45), .fraction(0.65)])
|
||||
.presentationDragIndicator(.visible)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,23 +20,17 @@ struct NodeMapSwiftUI: View {
|
|||
@State var showUserLocation: Bool = false
|
||||
@State var positions: [PositionEntity] = []
|
||||
/// Map State User Defaults
|
||||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
|
||||
@AppStorage("enableMapConvexHull") private var showConvexHull = false
|
||||
@AppStorage("enableMapTraffic") private var showTraffic: Bool = false
|
||||
@AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false
|
||||
@AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid
|
||||
// Map Configuration
|
||||
@Namespace var mapScope
|
||||
@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true)
|
||||
@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: .all, showsTraffic: true)
|
||||
@State var position = MapCameraPosition.automatic
|
||||
@State var scene: MKLookAroundScene?
|
||||
@State var isLookingAround = false
|
||||
@State var isShowingAltitude = false
|
||||
@State var isEditingSettings = false
|
||||
@State var selectedPosition: PositionEntity?
|
||||
@State var showWaypoints = false
|
||||
@State var selectedWaypoint: WaypointEntity?
|
||||
@State var isMeshMap = false
|
||||
|
||||
@State private var mapRegion = MKCoordinateRegion.init()
|
||||
|
|
@ -87,12 +81,8 @@ struct NodeMapSwiftUI: View {
|
|||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedWaypoint) { selection in
|
||||
WaypointForm(waypoint: selection)
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $isEditingSettings) {
|
||||
MapSettingsForm(nodeHistory: $showNodeHistory, routeLines: $showRouteLines, convexHull: $showConvexHull, traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap)
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap)
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
|
|
@ -161,21 +151,6 @@ struct NodeMapSwiftUI: View {
|
|||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
/// Show / Hide Waypoints Button
|
||||
if waypoints.count > 0 {
|
||||
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
showWaypoints = !showWaypoints
|
||||
}
|
||||
}) {
|
||||
Image(systemName: showWaypoints ? "signpost.right.and.left.fill" : "signpost.right.and.left")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
/// Look Around Button
|
||||
if self.scene != nil {
|
||||
Button(action: {
|
||||
|
|
|
|||
131
Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift
Normal file
131
Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// NodeListFilter.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 3/25/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct NodeListFilter: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
/// Filters
|
||||
@Binding var viaLora: Bool
|
||||
@Binding var viaMqtt: Bool
|
||||
@Binding var isOnline: Bool
|
||||
@Binding var distanceFilter: Bool
|
||||
@Binding var maximumDistance: Double
|
||||
@Binding var hopsAway: Int
|
||||
@Binding var deviceRole: Int
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(header: Text("Node Filters")) {
|
||||
Toggle(isOn: $viaLora) {
|
||||
|
||||
Label {
|
||||
Text("Via Lora")
|
||||
} icon: {
|
||||
Image(systemName: "dot.radiowaves.left.and.right")
|
||||
.rotationEffect(.degrees(-90))
|
||||
}
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $viaMqtt) {
|
||||
|
||||
Label {
|
||||
Text("Via Mqtt")
|
||||
} icon: {
|
||||
Image(systemName: "dot.radiowaves.up.forward")
|
||||
}
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
Toggle(isOn: $isOnline) {
|
||||
|
||||
Label {
|
||||
Text("Online Only")
|
||||
} icon: {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
|
||||
// Toggle(isOn: $distanceFilter) {
|
||||
//
|
||||
// Label {
|
||||
// Text("Distance")
|
||||
// } icon: {
|
||||
// Image(systemName: "map")
|
||||
// }
|
||||
// }
|
||||
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
//
|
||||
// .listRowSeparator(distanceFilter ? .hidden : .visible)
|
||||
// if distanceFilter {
|
||||
// HStack {
|
||||
// Label("Show nodes", systemImage: "lines.measurement.horizontal")
|
||||
// Picker("", selection: $maximumDistance) {
|
||||
// ForEach(MeshMapDistances.allCases) { di in
|
||||
// Text(di.description)
|
||||
// .tag(di.id)
|
||||
// }
|
||||
// }
|
||||
// .pickerStyle(DefaultPickerStyle())
|
||||
// }
|
||||
// }
|
||||
HStack {
|
||||
Label("Hops Away", systemImage: "hare")
|
||||
Picker("", selection: $hopsAway) {
|
||||
Text("Any")
|
||||
.tag(-1)
|
||||
Text("Direct")
|
||||
.tag(0)
|
||||
ForEach(1..<8) {
|
||||
Text("\($0)")
|
||||
.tag($0)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
HStack {
|
||||
Label("Device Role", systemImage: "apps.iphone")
|
||||
Picker("", selection: $deviceRole) {
|
||||
Text("All Roles")
|
||||
.tag(-1)
|
||||
ForEach(DeviceRoles.allCases) { dr in
|
||||
Label {
|
||||
Text(" \(dr.name)")
|
||||
} icon: {
|
||||
Image(systemName: dr.systemName)
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
#endif
|
||||
}
|
||||
.presentationDetents([.fraction(0.40), .fraction(0.50)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
|
|
@ -130,9 +130,9 @@ struct NodeListItem: View {
|
|||
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if node.viaMqtt && connectedNode != node.num {
|
||||
Image(systemName: "network")
|
||||
Image(systemName: "dot.radiowaves.up.forward")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.callout)
|
||||
.frame(width: 30)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import Foundation
|
|||
import MapKit
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MeshMap: View {
|
||||
|
||||
|
|
@ -22,157 +24,28 @@ struct MeshMap: View {
|
|||
/// Parameters
|
||||
@State var showUserLocation: Bool = true
|
||||
/// Map State User Defaults
|
||||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
|
||||
@AppStorage("enableMapConvexHull") private var showConvexHull = false
|
||||
@AppStorage("enableMapTraffic") private var showTraffic: Bool = false
|
||||
@AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false
|
||||
@AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid
|
||||
@AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .standard
|
||||
// Map Configuration
|
||||
@Namespace var mapScope
|
||||
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true)
|
||||
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .excludingAll, showsTraffic: false)
|
||||
@State var position = MapCameraPosition.automatic
|
||||
@State var scene: MKLookAroundScene?
|
||||
@State var isLookingAround = false
|
||||
@State var isEditingSettings = false
|
||||
@State var selectedPosition: PositionEntity?
|
||||
@State var showWaypoints = true
|
||||
@State var editingWaypoint: WaypointEntity?
|
||||
@State var selectedWaypoint: WaypointEntity?
|
||||
@State var newWaypointCoord :CLLocationCoordinate2D?
|
||||
@State var newWaypointCoord: CLLocationCoordinate2D?
|
||||
@State var isMeshMap = true
|
||||
|
||||
var delay: Double = 0
|
||||
@State private var scale: CGFloat = 0.5
|
||||
/// && time >= %@
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
|
||||
predicate: NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none)
|
||||
private var positions: FetchedResults<PositionEntity>
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
predicate: NSPredicate(
|
||||
format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
), animation: .none)
|
||||
private var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
|
||||
predicate: NSPredicate(format: "enabled == true", ""), animation: .none)
|
||||
private var routes: FetchedResults<RouteEntity>
|
||||
|
||||
var body: some View {
|
||||
|
||||
let lineCoords = Array(positions).compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationHelper.DefaultLocation
|
||||
})
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
MapReader { reader in
|
||||
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
if lineCoords.count > 0 {
|
||||
let hull = lineCoords.getConvexHull()
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(.blue, lineWidth: 3)
|
||||
.foregroundStyle(.indigo.opacity(0.4))
|
||||
}
|
||||
}
|
||||
/// Position Annotations
|
||||
ForEach(Array(positions), id: \.id) { position in
|
||||
/// Node color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
Annotation(position.nodePosition?.user?.longName ?? "?", coordinate: position.coordinate) {
|
||||
LazyVStack {
|
||||
ZStack {
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
if position.nodePosition?.isOnline ?? false {
|
||||
Circle()
|
||||
.fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5)))
|
||||
.foregroundStyle(Color(nodeColor.lighter()).opacity(0.3))
|
||||
.scaleEffect(scale)
|
||||
.animation(
|
||||
Animation.easeInOut(duration: 0.6)
|
||||
.repeatForever().delay(delay), value: scale
|
||||
)
|
||||
.onAppear {
|
||||
self.scale = 1
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
if position.nodePosition?.hasDetectionSensorMetrics ?? false {
|
||||
Image(systemName: "sensor.fill")
|
||||
.symbolRenderingMode(.palette)
|
||||
.symbolEffect(.variableColor)
|
||||
.padding()
|
||||
.foregroundStyle(.white)
|
||||
.background(Color(nodeColor))
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture { location in
|
||||
selectedPosition = (selectedPosition == position ? nil : position)
|
||||
}
|
||||
}
|
||||
/// Reduced Precision Map Circles
|
||||
if 11...16 ~= position.precisionBits {
|
||||
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
|
||||
let radius : CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
if radius > 0.0 {
|
||||
MapCircle(center: position.coordinate, radius: radius)
|
||||
.foregroundStyle(Color(nodeColor).opacity(0.25))
|
||||
.stroke(.white, lineWidth: 2)
|
||||
}
|
||||
}
|
||||
/// Routes
|
||||
ForEach(Array(routes), id: \.id) { route in
|
||||
let routeLocations = Array(route.locations!) as! [LocationEntity]
|
||||
let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in
|
||||
return loc.locationCoordinate ?? LocationHelper.DefaultLocation
|
||||
})
|
||||
Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(.green))
|
||||
.strokeBorder(.white, lineWidth: 3)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
Annotation("Finish", coordinate: routeCoords.last ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(.black))
|
||||
.strokeBorder(.white, lineWidth: 3)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
let solid = StrokeStyle(
|
||||
lineWidth: 3,
|
||||
lineCap: .round, lineJoin: .round
|
||||
)
|
||||
MapPolyline(coordinates: routeCoords)
|
||||
.stroke(Color(UIColor(hex: UInt32(route.color))), style: solid)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Waypoint Annotations
|
||||
if waypoints.count > 0 && showWaypoints {
|
||||
ForEach(Array(waypoints), id: \.id) { waypoint in
|
||||
Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) {
|
||||
LazyVStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40)
|
||||
.onTapGesture(perform: { location in
|
||||
selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MeshMapContent(showUserLocation: $showUserLocation, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, selectedWaypoint: $selectedWaypoint)
|
||||
|
||||
}
|
||||
.mapScope(mapScope)
|
||||
.mapStyle(mapStyle)
|
||||
|
|
@ -185,22 +58,39 @@ struct MeshMap: View {
|
|||
.mapControlVisibility(.automatic)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.onTapGesture(count: 1, perform: { location in
|
||||
newWaypointCoord = reader.convert(location , from: .local)
|
||||
.onTapGesture(count: 1, perform: { position in
|
||||
newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init()
|
||||
})
|
||||
.gesture(
|
||||
LongPressGesture(minimumDuration: 0.5)
|
||||
.sequenced(before: SpatialTapGesture(coordinateSpace: .local))
|
||||
.onEnded { value in
|
||||
switch value {
|
||||
case let .second(_, tapValue):
|
||||
guard let point = tapValue?.location else {
|
||||
print("Unable to retreive tap location from gesture data.")
|
||||
return
|
||||
}
|
||||
|
||||
guard let coordinate = reader.convert(point, from: .local) else {
|
||||
print("Unable to convert local point to coordinate on map.")
|
||||
return
|
||||
}
|
||||
|
||||
newWaypointCoord = coordinate
|
||||
editingWaypoint = WaypointEntity(context: context)
|
||||
editingWaypoint!.name = "Waypoint Pin"
|
||||
editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480)
|
||||
editingWaypoint!.latitudeI = Int32((newWaypointCoord?.latitude ?? 0) * 1e7)
|
||||
editingWaypoint!.longitudeI = Int32((newWaypointCoord?.longitude ?? 0) * 1e7)
|
||||
editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480)
|
||||
editingWaypoint!.id = 0
|
||||
print("Long press occured at: \(coordinate)")
|
||||
default: return
|
||||
}
|
||||
})
|
||||
.onLongPressGesture(minimumDuration: 0.5, maximumDistance: 10) {
|
||||
editingWaypoint = WaypointEntity(context: context)
|
||||
editingWaypoint!.name = "Waypoint Pin"
|
||||
editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480)
|
||||
editingWaypoint!.latitudeI = Int32((newWaypointCoord?.latitude ?? 0) * 1e7)
|
||||
editingWaypoint!.longitudeI = Int32((newWaypointCoord?.longitude ?? 0) * 1e7)
|
||||
editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480)
|
||||
editingWaypoint!.id = 0
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.sheet(item: $selectedPosition) { selection in
|
||||
PositionPopover(position: selection, popover: false)
|
||||
.padding()
|
||||
|
|
@ -214,7 +104,7 @@ struct MeshMap: View {
|
|||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $isEditingSettings) {
|
||||
MapSettingsForm(nodeHistory: $showNodeHistory, routeLines: $showRouteLines, convexHull: $showConvexHull, traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap)
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap)
|
||||
}
|
||||
.onChange(of: (appState.navigationPath)) { newPath in
|
||||
|
||||
|
|
@ -235,12 +125,12 @@ struct MeshMap: View {
|
|||
print("Waypoint id not found")
|
||||
return
|
||||
}
|
||||
guard let waypoint = waypoints.first(where: { $0.id == Int64(waypointId) }) else {
|
||||
print("Waypoint not found")
|
||||
return
|
||||
}
|
||||
showWaypoints = true
|
||||
position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60))
|
||||
// guard let waypoint = waypoints.first(where: { $0.id == Int64(waypointId) }) else {
|
||||
// print("Waypoint not found")
|
||||
// return
|
||||
// }
|
||||
//showWaypoints = true
|
||||
//position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60))
|
||||
}
|
||||
}
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
|
|
@ -270,36 +160,7 @@ struct MeshMap: View {
|
|||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
/// Show / Hide Waypoints Button
|
||||
if waypoints.count > 0 {
|
||||
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
showWaypoints = !showWaypoints
|
||||
}
|
||||
}) {
|
||||
Image(systemName: showWaypoints ? "signpost.right.and.left.fill" : "signpost.right.and.left")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
/// 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)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
|
|
|
|||
|
|
@ -7,28 +7,6 @@
|
|||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
struct NodeSearchState {
|
||||
var searchText = ""
|
||||
var searchScope = SearchScopes.all
|
||||
var predicate: NSPredicate = .init()
|
||||
|
||||
enum SearchScopes: CaseIterable, Identifiable {
|
||||
case all
|
||||
case lora
|
||||
case mqtt
|
||||
|
||||
var id: Self { self }
|
||||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .all: return "All"
|
||||
case .lora: return "LoRa"
|
||||
case .mqtt: return "MQTT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NodeList: View {
|
||||
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.all
|
||||
|
|
@ -38,11 +16,18 @@ struct NodeList: View {
|
|||
@State private var isPresentingDeleteNodeAlert = false
|
||||
@State private var isPresentingPositionSentAlert = false
|
||||
@State private var deleteNodeId: Int64 = 0
|
||||
@State private var searchState = NodeSearchState()
|
||||
@State private var searchText = ""
|
||||
@State private var viaLora = true
|
||||
@State private var viaMqtt = true
|
||||
@State private var isOnline = false
|
||||
@State private var distanceFilter = false
|
||||
@State private var maxDistance: Double = 800000
|
||||
@State private var hopsAway: Int = -1
|
||||
@State private var deviceRole: Int = -1
|
||||
|
||||
@State var isEditingFilters = false
|
||||
|
||||
@SceneStorage("selectedDetailView") var selectedDetailView: String?
|
||||
|
||||
@State private var searchText = ""
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
|
@ -159,14 +144,31 @@ struct NodeList: View {
|
|||
Text("Any missed messages will be delivered again.")
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchState.searchText, placement: nodes.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a node")
|
||||
.sheet(isPresented: $isEditingFilters) {
|
||||
NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole)
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .trailing) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
isEditingFilters = !isEditingFilters
|
||||
}
|
||||
}) {
|
||||
Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill")
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.tint(Color(UIColor.secondarySystemBackground))
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
}
|
||||
.controlSize(.regular)
|
||||
.padding(5)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.searchable(text: $searchText, placement: .automatic, prompt: "Find a node")
|
||||
.disableAutocorrection(true)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.searchScopes($searchState.searchScope) {
|
||||
ForEach(NodeSearchState.SearchScopes.allCases) { scope in
|
||||
Text(scope.title).tag(scope)
|
||||
}
|
||||
}
|
||||
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
|
||||
.listStyle(.plain)
|
||||
.confirmationDialog(
|
||||
|
|
@ -220,7 +222,7 @@ struct NodeList: View {
|
|||
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true)
|
||||
})
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
|
||||
} else {
|
||||
if #available (iOS 17, *) {
|
||||
ContentUnavailableView("select.node", systemImage: "flipphone")
|
||||
|
|
@ -237,51 +239,104 @@ struct NodeList: View {
|
|||
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
.onChange(of: searchState.searchText) { _ in
|
||||
.onChange(of: searchText) { _ in
|
||||
searchNodeList()
|
||||
}
|
||||
.onChange(of: searchState.searchScope) { _ in
|
||||
.onChange(of: viaLora) { _ in
|
||||
if !viaLora && !viaMqtt {
|
||||
viaMqtt = true
|
||||
}
|
||||
searchNodeList()
|
||||
}
|
||||
.onChange(of: viaMqtt) { _ in
|
||||
if !viaLora && !viaMqtt {
|
||||
viaLora = true
|
||||
}
|
||||
searchNodeList()
|
||||
}
|
||||
.onChange(of: deviceRole) { _ in
|
||||
searchNodeList()
|
||||
}
|
||||
.onChange(of: hopsAway) { _ in
|
||||
searchNodeList()
|
||||
}
|
||||
.onChange(of: isOnline) { _ in
|
||||
searchNodeList()
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
searchNodeList()
|
||||
}
|
||||
}
|
||||
|
||||
private func searchNodeList() {
|
||||
/// Case Insensitive Search Text Predicates
|
||||
let searchPredicates = ["user.userId", "user.hwModel", "user.longName", "user.shortName"].map { property in
|
||||
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchState.searchText)
|
||||
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
|
||||
}
|
||||
/// Create a compound predicate using each text search preicate as an OR
|
||||
let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates)
|
||||
|
||||
/// Set the predicate to nil if the search string is empty
|
||||
if searchState.searchText.isEmpty {
|
||||
nodes.nsPredicate = nil
|
||||
return
|
||||
/// Create an array of predicates to hold our AND predicates
|
||||
var predicates: [NSPredicate] = []
|
||||
/// Mqtt
|
||||
if !(viaLora && viaMqtt) {
|
||||
if viaLora {
|
||||
let loraPredicate = NSPredicate(format: "viaMqtt == NO")
|
||||
predicates.append(loraPredicate)
|
||||
} else {
|
||||
let mqttPredicate = NSPredicate(format: "viaMqtt == YES")
|
||||
predicates.append(mqttPredicate)
|
||||
}
|
||||
}
|
||||
/// Role
|
||||
if deviceRole > -1 {
|
||||
let rolePredicate = NSPredicate(format: "user.role == %i", Int32(deviceRole))
|
||||
predicates.append(rolePredicate)
|
||||
}
|
||||
/// Hops Away
|
||||
if hopsAway > 0 {
|
||||
let hopsAwayPredicate = NSPredicate(format: "hopsAway == %i", Int32(hopsAway))
|
||||
predicates.append(hopsAwayPredicate)
|
||||
}
|
||||
|
||||
/// Add a predicate for the search scope if selected
|
||||
if searchState.searchScope != .all {
|
||||
|
||||
if searchState.searchScope == .lora {
|
||||
let loraPredicate = NSPredicate(format: "viaMqtt == NO")
|
||||
let scopePredicate = NSCompoundPredicate(type: .and, subpredicates: [loraPredicate])
|
||||
nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, scopePredicate])
|
||||
return
|
||||
/// Online
|
||||
if isOnline {
|
||||
let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
|
||||
predicates.append(isOnlinePredicate)
|
||||
}
|
||||
/// Distance
|
||||
if distanceFilter {
|
||||
let pointOfInterest = LocationHelper.currentLocation
|
||||
|
||||
if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude {
|
||||
let D: Double = maxDistance * 1.1
|
||||
let R: Double = 6371009
|
||||
let meanLatitidue = pointOfInterest.latitude * .pi / 180
|
||||
let deltaLatitude = D / R * 180 / .pi
|
||||
let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi
|
||||
let minLatitude: Double = pointOfInterest.latitude - deltaLatitude
|
||||
let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude
|
||||
let minLongitude: Double = pointOfInterest.longitude - deltaLongitude
|
||||
let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude
|
||||
let distancePredicate = NSPredicate(format: "(%lf <= (positions[first].longitudeI / 1e7))", minLongitude, maxLongitude,minLatitude, maxLatitude)
|
||||
//let distancePredicate = NSPredicate(format: "(%lf <= (positions[LAST].longitudeI / 1e7)) AND ((positions[LAST].longitudeI / 1e7) <= %lf) AND (%lf <= (positions[LAST].latitudeI / 1e7)) AND ((positions[LAST].latitudeI / 1e7) <= %lf)", minLongitude, maxLongitude,minLatitude, maxLatitude)
|
||||
|
||||
} else if searchState.searchScope == .mqtt {
|
||||
let mqttPredicate = NSPredicate(format: "viaMqtt == YES")
|
||||
let scopePredicate = NSCompoundPredicate(type: .and, subpredicates: [mqttPredicate])
|
||||
nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, scopePredicate])
|
||||
return
|
||||
//predicates.append(distancePredicate)
|
||||
}
|
||||
}
|
||||
|
||||
if predicates.count > 0 || !searchText.isEmpty {
|
||||
|
||||
if !searchText.isEmpty {
|
||||
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates])
|
||||
} else {
|
||||
nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
|
||||
}
|
||||
} else {
|
||||
/// Use the text search predicate
|
||||
nodes.nsPredicate = textSearchPredicate
|
||||
nodes.nsPredicate = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ struct ChannelForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.45), .fraction(0.65)])
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
import SwiftUI
|
||||
|
||||
enum DetectionSensorRole: String, CaseIterable, Equatable {
|
||||
enum DetectionSensorRole: String, CaseIterable, Equatable, Decodable {
|
||||
case sensor
|
||||
case client
|
||||
var description: String {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ struct MQTTConfig: View {
|
|||
@State var mqttConnected: Bool = false
|
||||
@State var defaultTopic = "msh/US"
|
||||
@State var nearbyTopics = [String]()
|
||||
@State var mapReportingEnabled = false
|
||||
@State var mapPublishIntervalSecs = 3600
|
||||
@State var preciseLocation: Bool = false
|
||||
@State var mapPositionPrecision: Double = 13.0
|
||||
|
||||
|
||||
let locale = Locale.current
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -47,7 +53,7 @@ struct MQTTConfig: View {
|
|||
Section(header: Text("options")) {
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
Label("enabled", systemImage: "dot.radiowaves.up.forward")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
|
|
@ -58,7 +64,7 @@ struct MQTTConfig: View {
|
|||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
if enabled && proxyToClientEnabled {
|
||||
if enabled && proxyToClientEnabled && node!.mqttConfig!.proxyToClientEnabled == true {
|
||||
Toggle(isOn: $mqttConnected) {
|
||||
Label(mqttConnected ? "mqtt.disconnect".localized : "mqtt.connect".localized, systemImage: "server.rack")
|
||||
}
|
||||
|
|
@ -75,14 +81,95 @@ struct MQTTConfig: View {
|
|||
Text("JSON mode is a limited, unencrypted MQTT output for locally integrating with home assistant")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
|
||||
Section(header: Text("Map Report")) {
|
||||
|
||||
Toggle(isOn: $tlsEnabled) {
|
||||
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
|
||||
Text("Your MQTT Server must support TLS.")
|
||||
Toggle(isOn: $mapReportingEnabled) {
|
||||
Label("enabled", systemImage: "map")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
if mapReportingEnabled {
|
||||
Picker("Map Publish Interval", selection: $mapPublishIntervalSecs ) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
if ui.rawValue >= 3600 {
|
||||
Text(ui.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $preciseLocation) {
|
||||
Label("Precise Location", systemImage: "scope")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
.onChange(of: preciseLocation) { pl in
|
||||
if pl == false {
|
||||
mapPositionPrecision = 12
|
||||
} else {
|
||||
mapPositionPrecision = 32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !preciseLocation {
|
||||
VStack(alignment: .leading) {
|
||||
Label("Approximate Location", systemImage: "location.slash.circle.fill")
|
||||
Slider(value: $mapPositionPrecision, in: 11...16, step: 1) {
|
||||
} minimumValueLabel: {
|
||||
Image(systemName: "minus")
|
||||
} maximumValueLabel: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(header: Text("Root Topic")) {
|
||||
HStack {
|
||||
Label("Root Topic", systemImage: "tree")
|
||||
TextField("Root Topic", text: $root)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: root, perform: { _ in
|
||||
let totalBytes = root.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 30 {
|
||||
let firstNBytes = Data(root.utf8.prefix(30))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
root = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.asciiCapable)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disableAutocorrection(true)
|
||||
.listRowSeparator(.hidden)
|
||||
Text("The root topic to use for MQTT.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
|
||||
if nearbyTopics.count > 0 {
|
||||
Picker("Nearby Topics", selection: $selectedTopic ) {
|
||||
ForEach(nearbyTopics, id: \.self) { nt in
|
||||
Text(nt)
|
||||
}
|
||||
}
|
||||
.pickerStyle(InlinePickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("If the default region topic is too busy you can choose a more local topic.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Server")) {
|
||||
HStack {
|
||||
Label("Address", systemImage: "server.rack")
|
||||
|
|
@ -161,45 +248,13 @@ struct MQTTConfig: View {
|
|||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/)
|
||||
HStack {
|
||||
Label("Root Topic", systemImage: "tree")
|
||||
TextField("Root Topic", text: $root)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: root, perform: { _ in
|
||||
let totalBytes = root.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 30 {
|
||||
let firstNBytes = Data(root.utf8.prefix(30))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
root = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.asciiCapable)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disableAutocorrection(true)
|
||||
.listRowSeparator(.hidden)
|
||||
Text("The root topic to use for MQTT.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
|
||||
if nearbyTopics.count > 0 {
|
||||
Picker("Nearby Topics", selection: $selectedTopic ) {
|
||||
ForEach(nearbyTopics, id: \.self) { nt in
|
||||
Text(nt)
|
||||
}
|
||||
}
|
||||
.pickerStyle(InlinePickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("If the default region topic is too busy you can choose a more local topic.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
Toggle(isOn: $tlsEnabled) {
|
||||
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
|
||||
Text("Your MQTT Server must support TLS.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
Text("You can set uplink and downlink for each channel.")
|
||||
Text("For all Mqtt functionality other than the map report you must also set uplink and downlink for each channel you want to bridge over Mqtt.")
|
||||
.font(.callout)
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
|
|
@ -219,6 +274,9 @@ struct MQTTConfig: View {
|
|||
mqtt.encryptionEnabled = self.encryptionEnabled
|
||||
mqtt.jsonEnabled = self.jsonEnabled
|
||||
mqtt.tlsEnabled = self.tlsEnabled
|
||||
mqtt.mapReportingEnabled = self.mapReportingEnabled
|
||||
mqtt.mapReportSettings.positionPrecision = UInt32(self.mapPositionPrecision)
|
||||
mqtt.mapReportSettings.publishIntervalSecs = UInt32(self.mapPublishIntervalSecs)
|
||||
let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
|
|
@ -233,20 +291,6 @@ struct MQTTConfig: View {
|
|||
ZStack {
|
||||
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", mqttProxyConnected: bleManager.mqttProxyConnected)
|
||||
})
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
setMqttValues()
|
||||
// Need to request a TelemetryModuleConfig from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil {
|
||||
print("empty mqtt module config")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: address) { newAddress in
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
if newAddress != node!.mqttConfig!.address { hasChanges = true }
|
||||
|
|
@ -276,6 +320,9 @@ struct MQTTConfig: View {
|
|||
}
|
||||
}
|
||||
.onChange(of: proxyToClientEnabled) { newProxyToClientEnabled in
|
||||
if newProxyToClientEnabled {
|
||||
jsonEnabled = false
|
||||
}
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
if newProxyToClientEnabled != node!.mqttConfig!.proxyToClientEnabled { hasChanges = true }
|
||||
if newProxyToClientEnabled {
|
||||
|
|
@ -289,6 +336,9 @@ struct MQTTConfig: View {
|
|||
}
|
||||
}
|
||||
.onChange(of: jsonEnabled) { newJsonEnabled in
|
||||
if newJsonEnabled {
|
||||
proxyToClientEnabled = false
|
||||
}
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
if newJsonEnabled != node!.mqttConfig!.jsonEnabled { hasChanges = true }
|
||||
}
|
||||
|
|
@ -309,6 +359,33 @@ struct MQTTConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: mapReportingEnabled) { newMapReportingEnabled in
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
if newMapReportingEnabled != node!.mqttConfig!.mapReportingEnabled { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: preciseLocation) { _ in
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: mapPublishIntervalSecs) { newMapPublishIntervalSecs in
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
if newMapPublishIntervalSecs != node!.mqttConfig!.mapPublishIntervalSecs { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
setMqttValues()
|
||||
// Need to request a TelemetryModuleConfig from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil {
|
||||
print("empty mqtt module config")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func setMqttValues() {
|
||||
|
||||
|
|
@ -361,16 +438,23 @@ struct MQTTConfig: View {
|
|||
})
|
||||
}
|
||||
}
|
||||
self.enabled = (node?.mqttConfig?.enabled ?? false)
|
||||
self.proxyToClientEnabled = (node?.mqttConfig?.proxyToClientEnabled ?? false)
|
||||
self.enabled = node?.mqttConfig?.enabled ?? false
|
||||
self.proxyToClientEnabled = node?.mqttConfig?.proxyToClientEnabled ?? false
|
||||
self.address = node?.mqttConfig?.address ?? ""
|
||||
self.username = node?.mqttConfig?.username ?? ""
|
||||
self.password = node?.mqttConfig?.password ?? ""
|
||||
self.root = node?.mqttConfig?.root ?? "msh"
|
||||
self.encryptionEnabled = (node?.mqttConfig?.encryptionEnabled ?? false)
|
||||
self.jsonEnabled = (node?.mqttConfig?.jsonEnabled ?? false)
|
||||
self.tlsEnabled = (node?.mqttConfig?.tlsEnabled ?? false)
|
||||
self.encryptionEnabled = node?.mqttConfig?.encryptionEnabled ?? false
|
||||
self.jsonEnabled = node?.mqttConfig?.jsonEnabled ?? false
|
||||
self.tlsEnabled = node?.mqttConfig?.tlsEnabled ?? false
|
||||
self.mqttConnected = bleManager.mqttProxyConnected
|
||||
self.mapReportingEnabled = node?.mqttConfig?.mapReportingEnabled ?? false
|
||||
self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600)
|
||||
self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 12)
|
||||
if mapPositionPrecision == 0.0 {
|
||||
self.mapPositionPrecision = 12
|
||||
}
|
||||
self.preciseLocation = mapPositionPrecision == 32
|
||||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ struct PositionConfig: View {
|
|||
/// walking speeds are likely to be error prone like the compass
|
||||
@State var includeHeading = false
|
||||
|
||||
/// Minimum Version for fixed postion admin messages
|
||||
@State var minimumVersion = "2.3.3"
|
||||
@State private var supportedVersion = true
|
||||
@State private var showingSetFixedAlert = false
|
||||
//@State private var showingRemoveFixedAlert = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Form {
|
||||
|
|
@ -152,14 +158,13 @@ struct PositionConfig: View {
|
|||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $fixedPosition) {
|
||||
Label("Fixed Position", systemImage: "location.square.fill")
|
||||
Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval. Fixed position will always use the most recent position the device has.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $fixedPosition) {
|
||||
Label("Fixed Position", systemImage: "location.square.fill")
|
||||
Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
Section(header: Text("Position Flags")) {
|
||||
|
|
@ -263,9 +268,49 @@ struct PositionConfig: View {
|
|||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.positionConfig == nil)
|
||||
.alert(node?.positionConfig?.fixedPosition ?? false ? "Remove Fixed Position" : "Set Fixed Position", isPresented: $showingSetFixedAlert) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
fixedPosition = !fixedPosition
|
||||
}
|
||||
if node?.positionConfig?.fixedPosition ?? false {
|
||||
Button("Remove", role: .destructive) {
|
||||
if !bleManager.removeFixedPosition(fromUser: node!.user!, channel: 0) {
|
||||
print("Set Position Failed")
|
||||
}
|
||||
print("Remove a fixed position here")
|
||||
node?.positionConfig?.fixedPosition = false
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Position Config with Fixed Position = false")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving Position Config Entity \(nsError)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button("Set") {
|
||||
if !bleManager.setFixedPosition(fromUser: node!.user!, channel: 0) {
|
||||
print("Set Position Failed")
|
||||
}
|
||||
print("Set a fixed position")
|
||||
node?.positionConfig?.fixedPosition = true
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Position Config with Fixed Position = true")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving Position Config Entity \(nsError)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text(node?.positionConfig?.fixedPosition ?? false ? "This will disable fixed position and remove the currently set position." : "This will send a current position from your phone and enable fixed position.")
|
||||
}
|
||||
|
||||
SaveConfigButton(node: node, hasChanges: $hasChanges) {
|
||||
if fixedPosition {
|
||||
if fixedPosition && !supportedVersion {
|
||||
_ = bleManager.sendPosition(channel: 0, destNum: node?.num ?? 0, wantResponse: true)
|
||||
}
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
|
|
@ -316,6 +361,7 @@ struct PositionConfig: View {
|
|||
self.bleManager.context = context
|
||||
}
|
||||
setPositionValues()
|
||||
supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame
|
||||
// Need to request a PositionConfig from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.positionConfig == nil {
|
||||
print("empty position config")
|
||||
|
|
@ -325,6 +371,23 @@ struct PositionConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: fixedPosition) { newFixed in
|
||||
print("Changing Fixed Position Value")
|
||||
if supportedVersion {
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
print("We have a node and position config")
|
||||
print("We have turned on fixed position \(!node!.positionConfig!.fixedPosition && newFixed)")
|
||||
/// Fixed Position is off to start
|
||||
if !node!.positionConfig!.fixedPosition && newFixed {
|
||||
print("fire alert")
|
||||
showingSetFixedAlert = true
|
||||
} else if node!.positionConfig!.fixedPosition && !newFixed {
|
||||
/// Fixed Position is on to start
|
||||
showingSetFixedAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: deviceGpsEnabled) { newDeviceGps in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newDeviceGps != node!.positionConfig!.deviceGpsEnabled { hasChanges = true }
|
||||
|
|
@ -355,11 +418,6 @@ struct PositionConfig: View {
|
|||
if newSmartPositionEnabled != node!.positionConfig!.smartPositionEnabled { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: fixedPosition) { newFixed in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newFixed != node!.positionConfig!.fixedPosition { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: positionBroadcastSeconds) { newPositionBroadcastSeconds in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newPositionBroadcastSeconds != node!.positionConfig!.positionBroadcastSeconds { hasChanges = true }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ struct Firmware: View {
|
|||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
var node: NodeInfoEntity?
|
||||
@State var minimumVersion = "2.3.0"
|
||||
@State var minimumVersion = "2.3.2"
|
||||
@State var version = ""
|
||||
@State private var currentDevice: DeviceHardware?
|
||||
@State private var latestStable: FirmwareRelease?
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
#if canImport(TipKit)
|
||||
import TipKit
|
||||
#endif
|
||||
|
||||
struct Settings: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
|
@ -50,34 +53,43 @@ struct Settings: View {
|
|||
NavigationLink {
|
||||
AboutMeshtastic()
|
||||
} label: {
|
||||
Image(systemName: "questionmark.app")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("about.meshtastic")
|
||||
Label {
|
||||
Text("about.meshtastic")
|
||||
} icon: {
|
||||
Image(systemName: "questionmark.app")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.about)
|
||||
NavigationLink {
|
||||
AppSettings()
|
||||
} label: {
|
||||
Image(systemName: "gearshape")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("appsettings")
|
||||
Label {
|
||||
Text("appsettings")
|
||||
} icon: {
|
||||
Image(systemName: "gearshape")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.appSettings)
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
NavigationLink {
|
||||
Routes()
|
||||
} label: {
|
||||
Image(systemName: "road.lanes.curved.right")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("routes")
|
||||
Label {
|
||||
Text("routes")
|
||||
} icon: {
|
||||
Image(systemName: "road.lanes.curved.right")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.routes)
|
||||
NavigationLink {
|
||||
RouteRecorder()
|
||||
} label: {
|
||||
Image(systemName: "record.circle")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("route.recorder")
|
||||
Label {
|
||||
Text("route.recorder")
|
||||
} icon: {
|
||||
Image(systemName: "record.circle")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.routeRecorder)
|
||||
}
|
||||
|
|
@ -122,6 +134,9 @@ struct Settings: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
TipView(AdminChannelTip(), arrowEdge: .top)
|
||||
}
|
||||
} else {
|
||||
if bleManager.connectedPeripheral != nil {
|
||||
Text("Connected Node \(node?.user?.longName ?? "unknown".localized)")
|
||||
|
|
@ -152,26 +167,33 @@ struct Settings: View {
|
|||
NavigationLink {
|
||||
LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "dot.radiowaves.left.and.right")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("lora")
|
||||
Label {
|
||||
Text("lora")
|
||||
} icon: {
|
||||
Image(systemName: "dot.radiowaves.left.and.right")
|
||||
.rotationEffect(.degrees(-90))
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.loraConfig)
|
||||
NavigationLink {
|
||||
Channels(node: nodes.first(where: { $0.num == preferredNodeNum }))
|
||||
} label: {
|
||||
Image(systemName: "fibrechannel")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("channels")
|
||||
Label {
|
||||
Text("channels")
|
||||
} icon: {
|
||||
Image(systemName: "fibrechannel")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.channelConfig)
|
||||
.disabled(selectedNode > 0 && selectedNode != preferredNodeNum)
|
||||
NavigationLink {
|
||||
ShareChannels(node: nodes.first(where: { $0.num == preferredNodeNum }))
|
||||
} label: {
|
||||
Image(systemName: "qrcode")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("share.channels")
|
||||
Label {
|
||||
Text("share.channels")
|
||||
} icon: {
|
||||
Image(systemName: "qrcode")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.shareChannels)
|
||||
.disabled(selectedNode > 0 && selectedNode != preferredNodeNum)
|
||||
|
|
@ -180,58 +202,72 @@ struct Settings: View {
|
|||
NavigationLink {
|
||||
UserConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "person.crop.rectangle.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("user")
|
||||
Label {
|
||||
Text("user")
|
||||
} icon: {
|
||||
Image(systemName: "person.crop.rectangle.fill")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.userConfig)
|
||||
NavigationLink {
|
||||
BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("bluetooth")
|
||||
Label {
|
||||
Text("bluetooth")
|
||||
} icon: {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.bluetoothConfig)
|
||||
NavigationLink {
|
||||
DeviceConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "flipphone")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("device")
|
||||
Label {
|
||||
Text("device")
|
||||
} icon: {
|
||||
Image(systemName: "flipphone")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.deviceConfig)
|
||||
NavigationLink {
|
||||
DisplayConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "display")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("display")
|
||||
Label {
|
||||
Text("display")
|
||||
} icon: {
|
||||
Image(systemName: "display")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.displayConfig)
|
||||
NavigationLink {
|
||||
NetworkConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "network")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("network")
|
||||
Label {
|
||||
Text("network")
|
||||
} icon: {
|
||||
Image(systemName: "network")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.networkConfig)
|
||||
NavigationLink {
|
||||
PositionConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "location")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("position")
|
||||
Label {
|
||||
Text("position")
|
||||
} icon: {
|
||||
Image(systemName: "location")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.positionConfig)
|
||||
|
||||
NavigationLink {
|
||||
PowerConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "bolt.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("config.power.settings")
|
||||
Label {
|
||||
Text("config.power.settings")
|
||||
} icon: {
|
||||
Image(systemName: "bolt.fill")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.powerConfig)
|
||||
}
|
||||
|
|
@ -240,92 +276,114 @@ struct Settings: View {
|
|||
NavigationLink {
|
||||
AmbientLightingConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "light.max")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("ambient.lighting")
|
||||
Label {
|
||||
Text("ambient.lighting")
|
||||
} icon: {
|
||||
Image(systemName: "light.max")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.ambientLightingConfig)
|
||||
}
|
||||
NavigationLink {
|
||||
CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "list.bullet.rectangle.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("canned.messages")
|
||||
Label {
|
||||
Text("canned.messages")
|
||||
} icon: {
|
||||
Image(systemName: "list.bullet.rectangle.fill")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.cannedMessagesConfig)
|
||||
NavigationLink {
|
||||
DetectionSensorConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "sensor")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("detection.sensor")
|
||||
Label {
|
||||
Text("detection.sensor")
|
||||
} icon: {
|
||||
Image(systemName: "sensor")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.detectionSensorConfig)
|
||||
NavigationLink {
|
||||
ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "megaphone")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("external.notification")
|
||||
Label {
|
||||
Text("external.notification")
|
||||
} icon: {
|
||||
Image(systemName: "megaphone")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.externalNotificationConfig)
|
||||
NavigationLink {
|
||||
MQTTConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "dot.radiowaves.right")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("mqtt")
|
||||
Label {
|
||||
Text("mqtt")
|
||||
} icon: {
|
||||
Image(systemName: "dot.radiowaves.up.forward")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.mqttConfig)
|
||||
NavigationLink {
|
||||
RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "point.3.connected.trianglepath.dotted")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("range.test")
|
||||
Label {
|
||||
Text("range.test")
|
||||
} icon: {
|
||||
Image(systemName: "point.3.connected.trianglepath.dotted")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.rangeTestConfig)
|
||||
if node?.metadata?.hasWifi ?? false {
|
||||
NavigationLink {
|
||||
PaxCounterConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "figure.walk.motion")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("config.module.paxcounter.settings")
|
||||
Label {
|
||||
Text("config.module.paxcounter.settings")
|
||||
} icon: {
|
||||
Image(systemName: "figure.walk.motion")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.paxCounterConfig)
|
||||
}
|
||||
NavigationLink {
|
||||
RtttlConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "music.note.list")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("ringtone")
|
||||
Label {
|
||||
Text("ringtone")
|
||||
} icon: {
|
||||
Image(systemName: "music.note.list")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.ringtoneConfig)
|
||||
NavigationLink {
|
||||
SerialConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "terminal")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("serial")
|
||||
Label {
|
||||
Text("serial")
|
||||
} icon: {
|
||||
Image(systemName: "terminal")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.serialConfig)
|
||||
NavigationLink {
|
||||
StoreForwardConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "envelope.arrow.triangle.branch")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("storeforward")
|
||||
Label {
|
||||
Text("storeforward")
|
||||
} icon: {
|
||||
Image(systemName: "envelope.arrow.triangle.branch")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.storeAndForwardConfig)
|
||||
NavigationLink {
|
||||
TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "chart.xyaxis.line")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("telemetry")
|
||||
Label {
|
||||
Text("telemetry")
|
||||
} icon: {
|
||||
Image(systemName: "chart.xyaxis.line")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.telemetryConfig)
|
||||
}
|
||||
|
|
@ -333,18 +391,22 @@ struct Settings: View {
|
|||
NavigationLink {
|
||||
MeshLog()
|
||||
} label: {
|
||||
Image(systemName: "list.bullet.rectangle")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("mesh.log")
|
||||
Label {
|
||||
Text("mesh.log")
|
||||
} icon: {
|
||||
Image(systemName: "list.bullet.rectangle")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.meshLog)
|
||||
NavigationLink {
|
||||
let connectedNode = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
AdminMessageList(user: connectedNode?.user)
|
||||
} label: {
|
||||
Image(systemName: "building.columns")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("admin.log")
|
||||
Label {
|
||||
Text("admin.log")
|
||||
} icon: {
|
||||
Image(systemName: "building.columns")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.adminMessageLog)
|
||||
}
|
||||
|
|
@ -352,9 +414,11 @@ struct Settings: View {
|
|||
NavigationLink {
|
||||
Firmware(node: nodes.first(where: { $0.num == preferredNodeNum }))
|
||||
} label: {
|
||||
Image(systemName: "arrow.up.arrow.down.square")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("Firmware Updates")
|
||||
Label {
|
||||
Text("Firmware Updates")
|
||||
} icon: {
|
||||
Image(systemName: "arrow.up.arrow.down.square")
|
||||
}
|
||||
}
|
||||
.tag(SettingsSidebar.about)
|
||||
.disabled(selectedNode > 0 && selectedNode != preferredNodeNum)
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@
|
|||
"device.role.routerclient"="Router Client - Mesh Pakete werden bevorzugt über diesen Node gerouted. Der Router Client kann parallel auch von einer Client-App genutzt werden.";
|
||||
"device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role.";
|
||||
"device.role.tracker"="Tracker - For use with devices intended as a GPS tracker. Position packets sent from this device will be higher priority, with position broadcasting every two minutes. Smart Position Broadcast will default to off.";
|
||||
"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery.";
|
||||
"device.role.sensor"="Broadcasts telemetry packets as priority.";
|
||||
"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts.";
|
||||
"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts.";
|
||||
"direct.messages"="Direktnachrichten";
|
||||
"dismiss.keyboard"="Dismiss Keyboard";
|
||||
"display"="Display (Device Screen)";
|
||||
|
|
@ -309,12 +313,15 @@
|
|||
"tapback.exclamation"="Ausrufezeichen";
|
||||
"tapback.question"="Fragezeichen";
|
||||
"tapback.poop"="Kacke";
|
||||
"tapback.wave"="Wave";
|
||||
"telemetry"="Telemetrie (Sensoren)";
|
||||
"telemetry.config"="Telemetrie Einstellungen";
|
||||
"timeout"="Zeitlimit erreicht";
|
||||
"timestamp"="Timestamp";
|
||||
"tip.bluetooth.connect.title"="Connected LoRa 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.channel.admin.title"="Admin Channel";
|
||||
"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices.";
|
||||
"tip.channels.create.title"="Manage Channels";
|
||||
"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)";
|
||||
"tip.channels.share.title"="Sharing Meshtastic Channels";
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery.";
|
||||
"device.role.sensor"="Broadcasts telemetry packets as priority.";
|
||||
"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts.";
|
||||
"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts.";
|
||||
"device.role.repeater"="Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.";
|
||||
"device.role.router"="Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list.";
|
||||
"device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile devices.";
|
||||
|
|
@ -326,12 +327,15 @@
|
|||
"tapback.exclamation"="Exclamation Mark";
|
||||
"tapback.question"="Question Mark";
|
||||
"tapback.poop"="Poop";
|
||||
"tapback.wave"="Wave";
|
||||
"telemetry"="Telemetry (Sensors)";
|
||||
"telemetry.config"="Telemetry Config";
|
||||
"timeout"="Timeout";
|
||||
"timestamp"="Timestamp";
|
||||
"tip.bluetooth.connect.title"="Connected Radio";
|
||||
"tip.bluetooth.connect.message"="Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity.";
|
||||
"tip.channel.admin.title"="Admin Channel";
|
||||
"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices.";
|
||||
"tip.channels.create.title"="Manage Channels";
|
||||
"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)";
|
||||
"tip.channels.share.title"="Sharing Meshtastic Channels";
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@
|
|||
"device.role.lostandfound"="Transmet régulièrement la position par message dans le canal par défaut pour vous aider à retrouver l'appareil.";
|
||||
"device.role.sensor"="Transmet les paquets de télémétrie en priorité.";
|
||||
"device.role.tak"="Optimisé pour le système de communication ATAK, diminue les émissions de routine.";
|
||||
"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts.";
|
||||
"device.role.repeater"="Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages avec un minimum de surcharge. Invisible dans la liste des noeuds.";
|
||||
"device.role.router"="Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages. Visible dans la liste des noeuds.";
|
||||
"device.role.routerclient"="Combinaison des modes ROUTER et CLIENT. Pas pour les appareils mobiles.";
|
||||
|
|
@ -292,6 +293,7 @@
|
|||
"tapback.exclamation"="Point d'exclamation";
|
||||
"tapback.question"="Point d'interrogation";
|
||||
"tapback.poop"="Caca";
|
||||
"tapback.wave"="Wave";
|
||||
"telemetry"="Télémetrie (Capteurs)";
|
||||
"telemetry.config"="Configuration de télémetrie";
|
||||
"timeout"="Délai d'expiration";
|
||||
|
|
@ -300,6 +302,8 @@
|
|||
"tip.bluetooth.connect.message"="Affiche les informations de la radio Lora connectée via le bluetooth. Vous pouvez faire un glissé vers la gauche pour déconnecter la radio et un appui long pour voir les statistiques ou démarrer l'activité en direct.";
|
||||
"tip.channels.create.title"="Gérer les canaux";
|
||||
"tip.channels.create.message"="La pluspart des données de votre maillage sont envoyées sur le canal principal. Vous pouvez définir des canaux secondaires pour créer des groupes de messagerie additionnelle sécurisés avec leur propre clé. [Conseils de configuration du canal](https://meshtastic.org/docs/configuration/tips/)";
|
||||
"tip.channel.admin.title"="Admin Channel";
|
||||
"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices.";
|
||||
"tip.channels.share.title"="Partage des canaux Meshtastic";
|
||||
"tip.channels.share.message"="Un code QR Meshtastic contient la configuration LoRa et les valeurs de canal nécessaires pour communiquer. La plupart des activités du maillage ont lieu sur le canal principal requis. Si vous ne partagez pas votre canal principal, votre premier canal partagé devient le canal principal de l’autre réseau. Les autres canaux sont pour les groupes privés, chacun avec sa propre clé.";
|
||||
"tip.messages.title"="Messages";
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"device.role.lostandfound"="משדר מיקום כהודעה לערוץ ברירת מחדל לעיתים קבועות בכדי לסייע במציאת המכשיר.";
|
||||
"device.role.sensor"="משדר טלמטריה בעדיפות גבוהה.";
|
||||
"device.role.tak"="מותאם למערכת ATAK, מקטין תקשורת קבועה.";
|
||||
"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts.";
|
||||
"device.role.repeater"="מכשיר תשתית להרחבת המש על ידי העברת הודעות עם דאטה נוסף מינימלי.";
|
||||
"device.role.router"="מכשיר תשתית להרחבת המש על ידי העברת הודעות. מופיע ברשימת מכשירים.";
|
||||
"device.role.routerclient"="קומבינציה של ROUTER וCLIENT. לא למכשירים ניידים.";
|
||||
|
|
@ -316,12 +317,15 @@
|
|||
"tapback.exclamation"="סימן קריאה";
|
||||
"tapback.question"="סימן שאלה";
|
||||
"tapback.poop"="חרא";
|
||||
"tapback.wave"="Wave";
|
||||
"telemetry"="טלמטריה (חיישנים)";
|
||||
"telemetry.config"="הגדרות טלמטריה";
|
||||
"timeout"="זמן קצוב";
|
||||
"timestamp"="שעה/תאריך";
|
||||
"tip.bluetooth.connect.title"="מכשיר מחובר";
|
||||
"tip.bluetooth.connect.message"="מראה מידע אודות מכשיר המשטסטיק המחובר כעת לבלוטוס. ניתן לגרור שמאלה להתנתקות או לחיצה ארוכה לראות סטטיסטיקה או להתחיל פעילות.";
|
||||
"tip.channel.admin.title"="Admin Channel";
|
||||
"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices.";
|
||||
"tip.channels.create.title"="Manage Channels";
|
||||
"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)";
|
||||
"tip.channels.share.title"="משתף ערוצי משטסטיק";
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@
|
|||
"device.role.routerclient"="Router Client - Hybryda ról klienta i routera. Podobnie jak w przypadku routera, z tym że Router Client może być używany zarówno jako router, jak i klient połączony z aplikacją. Radia BLE/Wi-Fi i ekran OLED nie zostaną uśpione.";
|
||||
"device.role.repeater"="Przekaźnik - Pakiety siatki będą preferować trasowanie przez ten węzeł. Ta rola eliminuje niepotrzebny nadmiar, taki jak NodeInfo, DeviceTelemetry i inne pakiety siatki, skutkując tym, że urządzenie nie będzie widoczne jako część sieci. Proszę zobaczyć tryb Rebroadcast dla dodatkowych ustawień specyficznych dla tej roli.";
|
||||
"device.role.tracker"="Tracker - Do użytku z urządzeniami przeznaczonymi jako śledzenie GPS. Pakiety pozycyjne wysyłane z tego urządzenia będą miały wyższy priorytet, z nadawaniem pozycji co dwie minuty. Inteligentna transmisja pozycji będzie domyślnie wyłączona.";
|
||||
"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery.";
|
||||
"device.role.sensor"="Broadcasts telemetry packets as priority.";
|
||||
"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts.";
|
||||
"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts.";
|
||||
"direct.messages"="Bezpośrednie Wiadomości";
|
||||
"dismiss.keyboard"="Zamknij";
|
||||
"display"="Wyświetlacz (Ekran Urządzenia)";
|
||||
|
|
@ -310,12 +314,15 @@
|
|||
"tapback.exclamation"="Wykrzyknik";
|
||||
"tapback.question"="Znak zapytania";
|
||||
"tapback.poop"="Kupa";
|
||||
"tapback.wave"="Wave";
|
||||
"telemetry"="Telemetria (czujniki)";
|
||||
"telemetry.config"="Konfiguracja telemetrii";
|
||||
"timeout"="Limit czasu";
|
||||
"timestamp"="Znacznik czasu";
|
||||
"tip.bluetooth.connect.title"="Connected LoRa 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.channel.admin.title"="Admin Channel";
|
||||
"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices.";
|
||||
"tip.channels.create.title"="Manage Channels";
|
||||
"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)";
|
||||
"tip.channels.share.title"="Sharing Meshtastic Channels";
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 556e49ba619e2f4d8fa3c2dee2a94129a43d5f08
|
||||
Subproject commit dea3a82ef2accd25112b4ef1c6f8991b579740f4
|
||||
|
|
@ -98,6 +98,10 @@
|
|||
"device.role.routerclient"="路由客户端模式 - 优先转发 Mesh 网络中其他节点的消息,App 也可以连接到电台进行收发操作。";
|
||||
"device.role.repeater"="中继模式 - Mesh 网络数据包将优先通过此节点路由。此模式可消除不必要的开销,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 数据包,从而使设备不显示为 Mesh 网络的一部分。有关此角色的其他特定设置,请参阅转播模式。";
|
||||
"device.role.tracker"="定位模式 - 用于作为 GPS 跟踪器。从该设备发送的定位数据包优先级较高,每两分钟广播一次。智能位置广播默认为关闭。";
|
||||
"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery.";
|
||||
"device.role.sensor"="Broadcasts telemetry packets as priority.";
|
||||
"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts.";
|
||||
"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts.";
|
||||
"direct.messages"="直频消息";
|
||||
"dismiss.keyboard"="隐藏键盘";
|
||||
"display"="屏幕(电台屏幕)";
|
||||
|
|
@ -309,12 +313,15 @@
|
|||
"tapback.exclamation"="感叹号";
|
||||
"tapback.question"="问号";
|
||||
"tapback.poop"="便便";
|
||||
"tapback.wave"="Wave";
|
||||
"telemetry"="遥测(传感器)";
|
||||
"telemetry.config"="遥测配置";
|
||||
"timeout"="超时";
|
||||
"timestamp"="时间戳";
|
||||
"tip.bluetooth.connect.title"="连接到 LoRa 电台";
|
||||
"tip.bluetooth.connect.message"="显示当前通过蓝牙连接的 Lora 电台的信息。您可以向左滑动断开电台,长按查看统计信息或开始实时活动。";
|
||||
"tip.channel.admin.title"="Admin Channel";
|
||||
"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices.";
|
||||
"tip.channels.create.title"="Manage Channels";
|
||||
"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)";
|
||||
"tip.channels.share.title"="共享 Meshtastic 频道";
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@
|
|||
"device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼點的消息,App 也可以連接到電台進行收發操作。";
|
||||
"device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。";
|
||||
"device.role.tracker"="追蹤模式 - 用於作為 GPS 追蹤器。從該設備發送的定位數據包優先級較高,每兩分鐘廣播一次。智能位置廣播預設為關閉。";
|
||||
"device.role.sensor"="Broadcasts telemetry packets as priority.";
|
||||
"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts.";
|
||||
"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts.";
|
||||
"direct.messages"="聊天";
|
||||
"dismiss.keyboard"="隱藏鍵盤";
|
||||
"display"="螢幕(電台螢幕)";
|
||||
|
|
@ -309,12 +312,15 @@
|
|||
"tapback.exclamation"="驚嘆號";
|
||||
"tapback.question"="問號";
|
||||
"tapback.poop"="便便";
|
||||
"tapback.wave"="Wave";
|
||||
"telemetry"="遠測(傳感器)";
|
||||
"telemetry.config"="遠側設定";
|
||||
"timeout"="超時";
|
||||
"timestamp"="時間戳記";
|
||||
"tip.bluetooth.connect.title"="連接到 LoRa 電台";
|
||||
"tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 電台的信息。您可以向左滑動斷開電台,長按查看統計訊息或開始即時活動。";
|
||||
"tip.channel.admin.title"="Admin Channel";
|
||||
"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices.";
|
||||
"tip.channels.create.title"="管理頻道";
|
||||
"tip.channels.create.message"="現在 Mesh 上的資料會通過主通道發送。您可以設定輔助通道來建立由自己的金鑰保護的其他訊息組 [頻道設定提示](https://meshtastic.org/docs/configuration/radio/channels/)";
|
||||
"tip.channels.share.title"="共享 Meshtastic 頻道";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue