Merge pull request #655 from meshtastic/2.3.10_Working_Changes

2.3.10 Working Changes
This commit is contained in:
Garth Vander Houwen 2024-06-03 10:43:26 -07:00 committed by GitHub
commit 369623d234
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
144 changed files with 3616 additions and 2032 deletions

View file

@ -1,3 +1,7 @@
# Exclude automatically generated Swift files
excluded:
- Meshtastic/Protobufs
line_length: 400
type_name:
@ -46,3 +50,11 @@ disabled_rules: # rule identifiers to exclude from running
nesting:
type_level:
warning: 3
custom_rules:
disable_print:
included: ".*\\.swift"
name: "Disable `print()`"
regex: "((\\bprint)|(Swift\\.print))\\s*\\("
message: "Consider using a dedicated log message or the Xcode debugger instead of using `print`. ex. logger.debug(...)"
severity: warning

View file

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
25183D462C0A6D97001E31D5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25183D452C0A6D97001E31D5 /* Logger.swift */; };
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; };
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; };
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; };
@ -244,6 +245,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
25183D452C0A6D97001E31D5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = "<group>"; };
@ -437,6 +439,8 @@
DDCDC6CC29481FCC004C1DDA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
DDCDC6CE294821AD004C1DDA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConfig.swift; sourceTree = "<group>"; };
DDD28D362C0CCCD10063CFA3 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 37.xcdatamodel"; sourceTree = "<group>"; };
DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = "<group>"; };
DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClientProxyManager.swift; sourceTree = "<group>"; };
DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = "<group>"; };
@ -943,6 +947,7 @@
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */,
DDDB443C29F6592F00EE2349 /* NetworkManager.swift */,
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */,
25183D452C0A6D97001E31D5 /* Logger.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -1032,10 +1037,10 @@
isa = PBXNativeTarget;
buildConfigurationList = DDC2E17E26CE248F0042C5E4 /* Build configuration list for PBXNativeTarget "Meshtastic" */;
buildPhases = (
BB450974275599CE00509624 /* ShellScript */,
DDC2E15026CE248E0042C5E4 /* Sources */,
DDC2E15126CE248E0042C5E4 /* Frameworks */,
DDC2E15226CE248E0042C5E4 /* Resources */,
BB450974275599CE00509624 /* ShellScript */,
DDDE5A0829AF163F00490C6C /* Embed Foundation Extensions */,
);
buildRules = (
@ -1094,8 +1099,9 @@
DDC2E14C26CE248E0042C5E4 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1250;
LastUpgradeCheck = 1540;
TargetAttributes = {
DDC2E15326CE248E0042C5E4 = {
CreatedOnToolsVersion = 12.5.1;
@ -1124,6 +1130,7 @@
fr,
"zh-Hant-TW",
se,
"pt-PT",
);
mainGroup = DDC2E14B26CE248E0042C5E4;
packageReferences = (
@ -1177,6 +1184,7 @@
/* Begin PBXShellScriptBuildPhase section */
BB450974275599CE00509624 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@ -1190,7 +1198,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https: //github.com/realm/SwiftLint\"\nfi\n";
shellScript = "if [[ \"$(uname -m)\" == arm64 ]]\nthen\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif command -v swiftlint >/dev/null 2>&1\nthen\n swiftlint\nelse\n echo \"warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions.\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
@ -1299,6 +1307,7 @@
D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */,
DD0E20FE2B87090400F2D100 /* paxcount.pb.swift in Sources */,
DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */,
25183D462C0A6D97001E31D5 /* Logger.swift in Sources */,
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */,
DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */,
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */,
@ -1433,6 +1442,7 @@
DDDC22312BA76701002C44F1 /* fr */,
DDDC22322BA76961002C44F1 /* zh-Hant-TW */,
DDF45C352BC465B2005ED5F2 /* se */,
DDD28D362C0CCCD10063CFA3 /* pt-PT */,
);
name = Localizable.strings;
sourceTree = "<group>";
@ -1478,6 +1488,7 @@
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@ -1540,6 +1551,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@ -1583,7 +1595,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.3.9;
MARKETING_VERSION = 2.3.10;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1617,7 +1629,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.3.9;
MARKETING_VERSION = 2.3.10;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1690,7 +1702,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.9;
MARKETING_VERSION = 2.3.10;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1723,7 +1735,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.9;
MARKETING_VERSION = 2.3.10;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1825,6 +1837,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */,
DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */,
DD268D8C2BCC7D11008073AE /* MeshtasticDataModelV 35.xcdatamodel */,
DDDBC87C2BC65682001E8DF7 /* MeshtasticDataModelV 34.xcdatamodel */,
@ -1862,7 +1875,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */;
currentVersion = DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1540"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -58,6 +58,8 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable {
case oneThousandMiles = 1609000
case fifteenHundredMiles = 2414016
case twentyFiveHundredMiles = 4023360
case fiveThouandMiles = 8046720
case tenThousandMiles = 16093440
var id: Double { self.rawValue }
var description: String {
let distanceFormatter = MKDistanceFormatter()

View file

@ -21,7 +21,7 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
case repeater = 4
case router = 2
case routerClient = 3
var id: Int { self.rawValue }
var name: String {
switch self {
@ -48,7 +48,7 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
case .lostAndFound:
return "Lost and Found"
}
}
var description: String {
switch self {
@ -76,7 +76,7 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
return "device.role.lostandfound".localized
}
}
var systemName: String {
switch self {
case .client:

View file

@ -7,6 +7,40 @@
import Foundation
enum NagIntervals: Int, CaseIterable, Identifiable {
case unset = 0
case oneSecond = 1
case fiveSeconds = 5
case tenSeconds = 10
case fifteenSeconds = 15
case thirtySeconds = 30
case oneMinute = 60
case fiveMinutes = 300
var id: Int { self.rawValue }
var description: String {
switch self {
case .unset:
return "unset".localized
case .oneSecond:
return "interval.one.second".localized
case .fiveSeconds:
return "interval.five.seconds".localized
case .tenSeconds:
return "interval.ten.seconds".localized
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
case .oneMinute:
return "interval.one.minute".localized
case .fiveMinutes:
return "interval.five.minutes".localized
}
}
}
enum OutputIntervals: Int, CaseIterable, Identifiable {
case unset = 0

View file

@ -28,7 +28,7 @@ enum RegionCodes: Int, CaseIterable, Identifiable {
case my_919 = 17
case sg_923 = 18
case lora24 = 13
var topic: String {
var topic: String {
switch self {
case .unset:
"UNSET"

View file

@ -103,9 +103,9 @@ enum GpsMode: Int, CaseIterable, Equatable {
case enabled = 1
case disabled = 0
case notPresent = 2
var id: Int { self.rawValue }
var description: String {
switch self {
case .disabled:

View file

@ -15,7 +15,7 @@ enum ActivityType: Int, CaseIterable, Identifiable {
case driving = 3
case overlanding = 4
case skiing = 5
var id: Int { self.rawValue }
var description: String {
switch self {
@ -33,7 +33,7 @@ enum ActivityType: Int, CaseIterable, Identifiable {
return "routes.activitytype.skiing".localized
}
}
var fileNameString: String {
switch self {
case .walking:

View file

@ -15,7 +15,7 @@ enum Aqi: Int, CaseIterable, Identifiable {
case unhealthy = 3
case veryUnhealthy = 4
case hazardous = 5
var id: Int { self.rawValue }
var description: String {
switch self {
@ -65,7 +65,7 @@ enum Aqi: Int, CaseIterable, Identifiable {
return Range(301...500)
}
}
static func getAqi(for value: Int) -> Aqi {
let aqi: Aqi
switch value {
@ -96,7 +96,7 @@ enum Iaq: Int, CaseIterable, Identifiable {
case heavilyPolluted = 4
case severelyPolluted = 5
case extremelyPolluted = 6
var id: Int { self.rawValue }
var description: String {
switch self {
@ -134,7 +134,7 @@ enum Iaq: Int, CaseIterable, Identifiable {
return .brown
}
}
var range: Range<Int> {
switch self {
case .excellent:

View file

@ -14,8 +14,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
if metricsType == 0 {
// Create Device Metrics Header
csvString = "\("battery.level".localized), \("voltage".localized), \("channel.utilization".localized), \("airtime".localized), \("uptime".localized), \("timestamp".localized)"
for dm in telemetry {
if dm.metricsType == 0 {
for dm in telemetry where dm.metricsType == 0 {
csvString += "\n"
csvString += String(dm.batteryLevel)
csvString += ", "
@ -28,26 +27,23 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
csvString += String(dm.uptimeSeconds)
csvString += ", "
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
}
}
} else if metricsType == 1 {
// Create Environment Telemetry Header
csvString = "Temperature, Relative Humidity, Barometric Pressure, Indoor Air Quality, Gas Resistance, \("timestamp".localized)"
for dm in telemetry {
if dm.metricsType == 1 {
csvString += "\n"
csvString += String(dm.temperature.localeTemperature())
csvString += ", "
csvString += String(dm.relativeHumidity)
csvString += ", "
csvString += String(dm.barometricPressure)
csvString += ", "
csvString += String(dm.iaq)
csvString += ", "
csvString += String(dm.gasResistance)
csvString += ", "
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
}
for dm in telemetry where dm.metricsType == 1 {
csvString += "\n"
csvString += String(dm.temperature.localeTemperature())
csvString += ", "
csvString += String(dm.relativeHumidity)
csvString += ", "
csvString += String(dm.barometricPressure)
csvString += ", "
csvString += String(dm.iaq)
csvString += ", "
csvString += String(dm.gasResistance)
csvString += ", "
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
}
}
return csvString
@ -118,11 +114,8 @@ func positionToCsvFile(positions: [PositionEntity]) -> String {
return csvString
}
func routeToCsvFile(locations: [LocationEntity]) -> String {
var csvString: String = ""
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
// Create Position Header
csvString = "Id, Latitude, Longitude, Altitude, Speed, Heading"
for loc in locations {

View file

@ -8,15 +8,15 @@
import Foundation
extension Bundle {
public var appName: String { getInfo("CFBundleName") }
public var displayName: String { getInfo("CFBundleDisplayName") }
public var language: String { getInfo("CFBundleDevelopmentRegion") }
public var identifier: String { getInfo("CFBundleIdentifier") }
public var copyright: String { getInfo("NSHumanReadableCopyright").replacingOccurrences(of: "\\\\n", with: "\n") }
public var appBuild: String { getInfo("CFBundleVersion") }
public var appVersionLong: String { getInfo("CFBundleShortVersionString") }
//public var appVersionShort: String { getInfo("CFBundleShortVersion") }
public var appName: String { getInfo("CFBundleName") }
public var displayName: String { getInfo("CFBundleDisplayName") }
public var language: String { getInfo("CFBundleDevelopmentRegion") }
public var identifier: String { getInfo("CFBundleIdentifier") }
public var copyright: String { getInfo("NSHumanReadableCopyright").replacingOccurrences(of: "\\\\n", with: "\n") }
public var appBuild: String { getInfo("CFBundleVersion") }
public var appVersionLong: String { getInfo("CFBundleShortVersionString") }
// public var appVersionShort: String { getInfo("CFBundleShortVersion") }
fileprivate func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" }
}

View file

@ -28,19 +28,19 @@ extension [CLLocationCoordinate2D] {
/// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product.
/// Returns a positive value, if OAB makes a counter-clockwise turn,
/// negative for clockwise turn, and zero if the points are collinear.
func cross(P: CLLocationCoordinate2D, A: CLLocationCoordinate2D, B: CLLocationCoordinate2D) -> Double {
let part1 = (A.longitude - P.longitude) * (B.latitude - P.latitude)
let part2 = (A.latitude - P.latitude) * (B.longitude - P.longitude)
return part1 - part2;
func cross(p: CLLocationCoordinate2D, a: CLLocationCoordinate2D, b: CLLocationCoordinate2D) -> Double {
let part1 = (a.longitude - p.longitude) * (b.latitude - p.latitude)
let part2 = (a.latitude - p.latitude) * (b.longitude - p.longitude)
return part1 - part2
}
// Sort points lexicographically
let points = self.sorted() {
let points = self.sorted {
$0.longitude == $1.longitude ? $0.latitude < $1.latitude : $0.longitude < $1.longitude
}
// Build the lower hull
var lower: [CLLocationCoordinate2D] = []
for p in points {
while lower.count >= 2 && cross(P: lower[lower.count - 2], A: lower[lower.count - 1], B: p) <= 0 {
while lower.count >= 2 && cross(p: lower[lower.count - 2], a: lower[lower.count - 1], b: p) <= 0 {
lower.removeLast()
}
lower.append(p)
@ -48,7 +48,7 @@ extension [CLLocationCoordinate2D] {
// Build upper hull
var upper: [CLLocationCoordinate2D] = []
for p in points.reversed() {
while upper.count >= 2 && cross(P: upper[upper.count-2], A: upper[upper.count-1], B: p) <= 0 {
while upper.count >= 2 && cross(p: upper[upper.count-2], a: upper[upper.count-1], b: p) <= 0 {
upper.removeLast()
}
upper.append(p)

View file

@ -47,7 +47,6 @@ extension UIColor {
let red = CGFloat((hex & 0xFF0000) >> 16)
let green = CGFloat((hex & 0x00FF00) >> 8)
let blue = CGFloat((hex & 0x0000FF))
/// print("\(red) - \(green) - \(blue)")
self.init(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0)
}
}

View file

@ -12,13 +12,13 @@ extension ChannelEntity {
self.value(forKey: "allPrivateMessages") as? [MessageEntity] ?? [MessageEntity]()
}
var unreadMessages: Int {
let unreadMessages = allPrivateMessages.filter{ ($0 as AnyObject).read == false }
let unreadMessages = allPrivateMessages.filter { ($0 as AnyObject).read == false }
return unreadMessages.count
}
var protoBuf: Channel {
var channel = Channel()
channel.index = self.index

View file

@ -39,4 +39,3 @@ extension LocationEntity {
}
}
}

View file

@ -8,17 +8,17 @@
import Foundation
extension MyInfoEntity {
var messageList: [MessageEntity] {
self.value(forKey: "allMessages") as? [MessageEntity] ?? [MessageEntity]()
}
var unreadMessages: Int {
let unreadMessages = messageList.filter{ ($0 as AnyObject).read == false && ($0 as AnyObject).isEmoji == false }
let unreadMessages = messageList.filter { ($0 as AnyObject).read == false && ($0 as AnyObject).isEmoji == false }
return unreadMessages.count
}
var hasAdmin: Bool {
let adminChannel = channels?.filter{ ($0 as AnyObject).name?.lowercased() == "admin" }
let adminChannel = channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" }
return adminChannel?.count ?? 0 > 0
}
}

View file

@ -9,37 +9,36 @@ import Foundation
import CoreData
extension NodeInfoEntity {
var hasPositions: Bool {
return positions?.count ?? 0 > 0
}
var hasDeviceMetrics: Bool {
let deviceMetrics = telemetries?.filter{ ($0 as AnyObject).metricsType == 0 }
let deviceMetrics = telemetries?.filter { ($0 as AnyObject).metricsType == 0 }
return deviceMetrics?.count ?? 0 > 0
}
var hasEnvironmentMetrics: Bool {
let environmentMetrics = telemetries?.filter{ ($0 as AnyObject).metricsType == 1 }
let environmentMetrics = telemetries?.filter { ($0 as AnyObject).metricsType == 1 }
return environmentMetrics?.count ?? 0 > 0
}
var hasDetectionSensorMetrics: Bool {
return user?.sensorMessageList.count ?? 0 > 0
}
var hasTraceRoutes: Bool {
return traceRoutes?.count ?? 0 > 0
}
var hasPax: Bool {
return pax?.count ?? 0 > 0
}
var isStoreForwardRouter: Bool {
return storeForwardConfig?.isRouter ?? false
}
var isOnline: Bool {
let fifteenMinutesAgo = Calendar.current.date(byAdding: .minute, value: -15, to: Date())
if lastHeard?.compare(fifteenMinutesAgo!) == .orderedDescending {
@ -50,13 +49,13 @@ extension NodeInfoEntity {
}
public func createNodeInfo(num: Int64, context: NSManagedObjectContext) -> NodeInfoEntity {
let newNode = NodeInfoEntity(context: context)
newNode.id = Int64(num)
newNode.num = Int64(num)
let newUser = UserEntity(context: context)
newUser.num = Int64(num)
let userId = String(format:"%2X", num)
let userId = String(format: "%2X", num)
newUser.userId = "!\(userId)"
let last4 = String(userId.suffix(4))
newUser.longName = "Meshtastic \(last4)"

View file

@ -11,7 +11,7 @@ import MapKit
import SwiftUI
extension PositionEntity {
static func allPositionsFetchRequest() -> NSFetchRequest<PositionEntity> {
let request: NSFetchRequest<PositionEntity> = PositionEntity.fetchRequest()
request.fetchLimit = 1000
@ -20,20 +20,20 @@ extension PositionEntity {
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")
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 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 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)
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

View file

@ -9,7 +9,6 @@ import Foundation
import CoreData
extension UserEntity {
var messageList: [MessageEntity] {
self.value(forKey: "allMessages") as? [MessageEntity] ?? [MessageEntity]()
@ -18,22 +17,21 @@ extension UserEntity {
var adminMessageList: [MessageEntity] {
self.value(forKey: "adminMessages") as? [MessageEntity] ?? [MessageEntity]()
}
var sensorMessageList: [MessageEntity] {
self.value(forKey: "detectionSensorMessages") as? [MessageEntity] ?? [MessageEntity]()
}
var unreadMessages: Int {
let unreadMessages = messageList.filter{ ($0 as AnyObject).read == false }
let unreadMessages = messageList.filter { ($0 as AnyObject).read == false }
return unreadMessages.count
}
}
public func createUser(num: Int64, context: NSManagedObjectContext) -> UserEntity {
let newUser = UserEntity(context: context)
newUser.num = Int64(num)
let userId = String(format:"%2X", num)
let userId = String(format: "%2X", num)
newUser.userId = "!\(userId)"
let last4 = String(userId.suffix(4))
newUser.longName = "Meshtastic \(last4)"

View file

@ -10,13 +10,13 @@ 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.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)

View file

@ -8,7 +8,7 @@
import Foundation
extension Date {
func formattedDate(format: String) -> String {
let dateformat = DateFormatter()
dateformat.dateFormat = format
@ -20,12 +20,12 @@ extension Date {
}
func relativeTimeOfDay() -> String {
let hour = Calendar.current.component(.hour, from: self)
switch hour {
case 6..<12 : return "relativetimeofday.morning".localized
case 12 : return "relativetimeofday.midday".localized
case 13..<17 : return "relativetimeofday.afternoon".localized
case 17..<22 : return "relativetimeofday.evening".localized
case 6..<12: return "relativetimeofday.morning".localized
case 12: return "relativetimeofday.midday".localized
case 13..<17: return "relativetimeofday.afternoon".localized
case 17..<22: return "relativetimeofday.evening".localized
default: return "relativetimeofday.nighttime".localized
}
}

View file

@ -5,6 +5,7 @@
// Copyright(c) Garth Vander Houwen 5/5/23.
//
import Foundation
import OSLog
let allocatedSizeResourceKeys: Set<URLResourceKey> = [
.isRegularFileKey,
@ -52,11 +53,13 @@ public extension FileManager {
do {
accumulatedSize += try contentItemURL.regularFileAllocatedSize()
} catch {
print("💥 File Manager Error: \(error.localizedDescription)")
Logger.services.error("💥 File Manager Error: \(error.localizedDescription)")
}
}
if let error = enumeratorError { print("💥 AllocatedSizeOfDirectory enumeratorError = \(error.localizedDescription)") }
if let error = enumeratorError {
Logger.services.error("💥 AllocatedSizeOfDirectory enumeratorError = \(error.localizedDescription)")
}
return Double(accumulatedSize).toBytes

View file

@ -16,7 +16,7 @@ extension PlottableMeasurement: Plottable where UnitType == UnitLength {
var primitivePlottable: Double {
self.measurement.converted(to: .meters).value
}
init?(primitivePlottable: Double) {
self.init(
measurement: Measurement(

View file

@ -16,7 +16,7 @@ extension UIColor {
var green: CGFloat = 0
var alpha: CGFloat = 0
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
return UIColor(
red: add(componentDelta, toComponent: red),
@ -37,7 +37,7 @@ extension UIColor {
private func add(_ value: CGFloat, toComponent: CGFloat) -> CGFloat {
return max(0, min(1, toComponent + value))
}
static var random: UIColor {
return UIColor(
red: .random(in: 0...1),

View file

@ -8,10 +8,10 @@
import Foundation
extension URL {
func regularFileAllocatedSize() throws -> UInt64 {
let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys)
guard resourceValues.isRegularFile ?? false else {
return 0
}

View file

@ -29,7 +29,7 @@ struct UserDefault<T: Decodable> {
return value
}
return UserDefaults.standard.object(forKey: key.rawValue) as? T ?? defaultValue
}
set {
@ -95,10 +95,10 @@ extension UserDefaults {
@UserDefault(.meshMapDistance, defaultValue: 800000)
static var meshMapDistance: Double
@UserDefault(.enableMapWaypoints, defaultValue: false)
static var enableMapWaypoints: Bool
@UserDefault(.enableMapRecentering, defaultValue: false)
static var enableMapRecentering: Bool
@ -146,13 +146,13 @@ extension UserDefaults {
@UserDefault(.enableSmartPosition, defaultValue: false)
static var enableSmartPosition: Bool
@UserDefault(.channelMessageNotifications, defaultValue: true)
static var channelMessageNotifications: Bool
@UserDefault(.newNodeNotifications, defaultValue: true)
static var newNodeNotifications: Bool
@UserDefault(.lowBatteryNotifications, defaultValue: true)
static var lowBatteryNotifications: Bool

File diff suppressed because it is too large Load diff

View file

@ -8,10 +8,6 @@ import SwiftUI
class SwiftUIEmojiTextField: UITextField {
override func awakeFromNib() {
super.awakeFromNib()
}
func setEmoji() {
_ = self.textInputMode
}
@ -21,11 +17,9 @@ class SwiftUIEmojiTextField: UITextField {
}
override var textInputMode: UITextInputMode? {
for mode in UITextInputMode.activeInputModes {
if mode.primaryLanguage == "emoji" {
self.keyboardType = .default // do not remove this
return mode
}
for mode in UITextInputMode.activeInputModes where mode.primaryLanguage == "emoji" {
self.keyboardType = .default // do not remove this
return mode
}
return nil
}

View file

@ -1,5 +1,6 @@
import Foundation
import SwiftUI
import OSLog
class LocalNotificationManager {
@ -37,7 +38,7 @@ class LocalNotificationManager {
content.body = notification.content
content.sound = .default
content.interruptionLevel = .timeSensitive
if notification.target != nil {
content.userInfo["target"] = notification.target
}
@ -60,7 +61,7 @@ class LocalNotificationManager {
UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in
for notification in notifications {
print(notification)
Logger.services.debug("\(notification)")
}
}
}

View file

@ -1,12 +1,13 @@
import Foundation
import CoreLocation
import MapKit
import OSLog
class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
static let shared = LocationHelper()
var locationManager = CLLocationManager()
//@Published var region = MKCoordinateRegion()
// @Published var region = MKCoordinateRegion()
@Published var authorizationStatus: CLAuthorizationStatus?
override init() {
super.init()
@ -47,7 +48,7 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
}
return sats
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .authorizedAlways:
@ -67,9 +68,9 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location manager error: \(error.localizedDescription)")
Logger.services.error("Location manager error: \(error.localizedDescription)")
}
}

View file

@ -7,6 +7,7 @@
import SwiftUI
import CoreLocation
import OSLog
// Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`.
@available(iOS 17.0, macOS 14.0, *)
@ -16,7 +17,7 @@ import CoreLocation
private let manager: CLLocationManager
private var background: CLBackgroundActivitySession?
var enableSmartPosition: Bool = UserDefaults.enableSmartPosition
@Published var locationsArray: [CLLocation]
@Published var isStationary = false
@Published var count = 0
@ -25,12 +26,12 @@ import CoreLocation
@Published var recordingStarted: Date?
@Published var distanceTraveled = 0.0
@Published var elevationGain = 0.0
@Published
var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") {
didSet { UserDefaults.standard.set(updatesStarted, forKey: "liveUpdatesStarted") }
}
@Published
var backgroundActivity: Bool = UserDefaults.standard.bool(forKey: "BGActivitySessionStarted") {
didSet {
@ -38,27 +39,27 @@ import CoreLocation
UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted")
}
}
private init() {
self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`.
self.manager.allowsBackgroundLocationUpdates = true
locationsArray = [CLLocation]()
}
func startLocationUpdates() {
if self.manager.authorizationStatus == .notDetermined {
self.manager.requestWhenInUseAuthorization()
}
print("Starting location updates")
Task() {
Logger.services.info("📍 Starting location updates")
Task {
do {
self.updatesStarted = true
let updates = CLLocationUpdate.liveUpdates()
for try await update in updates {
if !self.updatesStarted { break }
if !self.updatesStarted { break }
if let loc = update.location {
self.isStationary = update.isStationary
var locationAdded: Bool
locationAdded = addLocation(loc, smartPostion: enableSmartPosition)
if !isRecording && locationAdded {
@ -69,30 +70,30 @@ import CoreLocation
}
}
} catch {
print("Could not start location updates")
Logger.services.error("💥 Could not start location updates: \(error.localizedDescription)")
}
return
}
}
func stopLocationUpdates() {
print("Stopping location updates")
Logger.services.info("🛑 Stopping location updates")
self.updatesStarted = false
}
func addLocation(_ location: CLLocation, smartPostion: Bool) -> Bool {
if smartPostion {
let age = -location.timestamp.timeIntervalSinceNow
if age > 10 {
print("Bad Location \(self.count): Too Old \(age) seconds ago \(location)")
Logger.services.warning("📍 Bad Location \(self.count): Too Old \(age) seconds ago \(location)")
return false
}
if location.horizontalAccuracy < 0 {
print("Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)")
Logger.services.warning("📍 Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)")
return false
}
if location.horizontalAccuracy > 5 {
print("Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)")
Logger.services.warning("📍 Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)")
return false
}
}
@ -111,9 +112,9 @@ import CoreLocation
}
return true
}
static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090)
static var satsInView: Int {
var sats = 0
if let newLocation = shared.locationsArray.last {

View file

@ -0,0 +1,19 @@
import OSLog
extension Logger {
/// The logger's subsystem.
private static var subsystem = Bundle.main.bundleIdentifier!
/// All logs related to data such as decoding error, parsing issues, etc.
static let data = Logger(subsystem: subsystem, category: "🗄️ Data")
/// All logs related to the mesh
static let mesh = Logger(subsystem: subsystem, category: "🕸️ Mesh")
/// All logs related to services such as network calls, location, etc.
static let services = Logger(subsystem: subsystem, category: "🍏 Services")
/// All logs related to tracking and analytics.
static let statistics = Logger(subsystem: subsystem, category: "📈 Stats")
}

View file

@ -7,6 +7,7 @@
import Foundation
import MapKit
import OSLog
class OfflineTileManager: ObservableObject {
static let shared = OfflineTileManager()
@ -20,7 +21,7 @@ class OfflineTileManager: ObservableObject {
}
init() {
print("Documents Directory = \(documentsDirectory)")
Logger.services.debug("Documents Directory = \(self.documentsDirectory.absoluteString)")
createDirectoriesIfNecessary()
}

View file

@ -1,4 +1,5 @@
import Foundation
import OSLog
class MeshLogger {
@ -18,17 +19,22 @@ class MeshLogger {
let formatter = DateFormatter()
formatter.dateFormat = dateFormatString
let timestamp = formatter.string(from: Date())
guard let data = (message + " - " + timestamp + "\n").data(using: String.Encoding.utf8) else { return }
print(message)
guard let data = (message + " - " + timestamp + "\n").data(using: String.Encoding.utf8) else {
Logger.mesh.error("Unable to create mesh log data")
return
}
if FileManager.default.fileExists(atPath: logFile.path) {
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
do {
if FileManager.default.fileExists(atPath: logFile.path) {
let fileHandle = try FileHandle(forWritingTo: logFile)
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
} else {
try data.write(to: logFile, options: .atomicWrite)
}
} else {
try? data.write(to: logFile, options: .atomicWrite)
} catch {
Logger.mesh.error("Error writing mesh log data: \(error.localizedDescription)")
}
}
}

View file

@ -9,6 +9,7 @@ import Foundation
import CoreData
import SwiftUI
import RegexBuilder
import OSLog
#if canImport(ActivityKit)
import ActivityKit
#endif
@ -16,7 +17,9 @@ import ActivityKit
func generateMessageMarkdown (message: String) -> String {
if !message.isEmoji() {
let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber]
let detector = try! NSDataDetector(types: types.rawValue)
guard let detector = try? NSDataDetector(types: types.rawValue) else {
return message
}
let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count))
var messageWithMarkdown = message
if matches.count > 0 {
@ -109,12 +112,12 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO
myInfoEntity.rebootCount = Int32(myInfo.rebootCount)
do {
try context.save()
print("💾 Saved a new myInfo for node number: \(String(myInfo.myNodeNum))")
Logger.data.info("💾 Saved a new myInfo for node number: \(String(myInfo.myNodeNum))")
return myInfoEntity
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Inserting New Core Data MyInfoEntity: \(nsError)")
Logger.data.error("Error Inserting New Core Data MyInfoEntity: \(nsError)")
}
} else {
@ -124,16 +127,16 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO
do {
try context.save()
print("💾 Updated myInfo for node number: \(String(myInfo.myNodeNum))")
Logger.data.info("💾 Updated myInfo for node number: \(String(myInfo.myNodeNum))")
return fetchedMyInfo[0]
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data MyInfoEntity: \(nsError)")
Logger.data.error("Error Updating Core Data MyInfoEntity: \(nsError)")
}
}
} catch {
print("💥 Fetch MyInfo Error")
Logger.data.error("Fetch MyInfo Error")
}
return nil
}
@ -182,16 +185,16 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
do {
try context.save()
} catch {
print("Failed to save channel")
Logger.data.error("Failed to save channel: \(error.localizedDescription)")
}
print("💾 Updated MyInfo channel \(channel.index) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum)")
Logger.data.info("💾 Updated MyInfo channel \(channel.index) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum)")
} else if channel.role.rawValue > 0 {
print("💥 Trying to save a channel to a MyInfo that does not exist: \(fromNum)")
Logger.data.error("Trying to save a channel to a MyInfo that does not exist: \(fromNum)")
}
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError)")
Logger.data.error("Error Saving MyInfo Channel from ADMIN_APP \(nsError)")
}
}
}
@ -226,7 +229,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS
if fetchedNode.count > 0 {
fetchedNode[0].metadata = newMetadata
} else {
if fromNum > 0 {
let newNode = createNodeInfo(num: Int64(fromNum), context: context)
newNode.metadata = newMetadata
@ -235,13 +238,13 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS
do {
try context.save()
} catch {
print("Failed to save device metadata")
Logger.data.error("Failed to save device metadata: \(error.localizedDescription)")
}
print("💾 Updated Device Metadata from Admin App Packet For: \(fromNum)")
Logger.data.info("💾 Updated Device Metadata from Admin App Packet For: \(fromNum)")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError)")
Logger.data.error("Error Saving MyInfo Channel from ADMIN_APP \(nsError)")
}
}
}
@ -284,7 +287,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
newNode.snr = nodeInfo.snr
if nodeInfo.hasUser {
let newUser = UserEntity(context: context)
newUser.userId = nodeInfo.user.id
newUser.num = Int64(nodeInfo.num)
@ -307,7 +310,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
position.longitudeI = nodeInfo.position.longitudeI
position.altitude = nodeInfo.position.altitude
position.satsInView = Int32(nodeInfo.position.satsInView)
position.speed = Int32(nodeInfo.position.groundSpeed)
position.speed = Int32(nodeInfo.position.groundSpeed)
position.heading = Int32(nodeInfo.position.groundTrack)
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time)))
var newPostions = [PositionEntity]()
@ -328,15 +331,15 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
}
do {
try context.save()
print("💾 Saved a new Node Info For: \(String(nodeInfo.num))")
Logger.data.info("💾 Saved a new Node Info For: \(String(nodeInfo.num))")
return newNode
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving Core Data NodeInfoEntity: \(nsError)")
Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError)")
}
} catch {
print("💥 Fetch MyInfo Error")
Logger.data.error("Fetch MyInfo Error")
}
} else if nodeInfo.num > 0 {
@ -349,7 +352,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
fetchedNode[0].hopsAway = Int32(nodeInfo.hopsAway)
if nodeInfo.hasUser {
if (fetchedNode[0].user == nil) {
if fetchedNode[0].user == nil {
fetchedNode[0].user = UserEntity(context: context)
}
fetchedNode[0].user!.userId = nodeInfo.user.id
@ -360,9 +363,9 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed
fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue)
fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
} else {
if (fetchedNode[0].user == nil && nodeInfo.num > Int16.max) {
} else {
if fetchedNode[0].user == nil && nodeInfo.num > Int16.max {
let newUser = createUser(num: Int64(nodeInfo.num), context: context)
fetchedNode[0].user = newUser
}
@ -412,19 +415,19 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
}
do {
try context.save()
print("💾 NodeInfo saved for \(nodeInfo.num)")
Logger.data.info("💾 NodeInfo saved for \(nodeInfo.num)")
return fetchedNode[0]
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving Core Data NodeInfoEntity: \(nsError)")
Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError)")
}
} catch {
print("💥 Fetch MyInfo Error")
Logger.data.error("Fetch MyInfo Error")
}
}
} catch {
print("💥 Fetch NodeInfoEntity Error")
Logger.data.error("Fetch NodeInfoEntity Error")
}
return nil
}
@ -457,15 +460,15 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
fetchedNode[0].cannedMessageConfig?.messages = messages
do {
try context.save()
print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)")
Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError)")
Logger.data.error("Error Saving NodeInfoEntity from POSITION_APP \(nsError)")
}
}
} catch {
print("💥 Error Deserializing ADMIN_APP packet.")
Logger.data.error("Error Deserializing ADMIN_APP packet.")
}
}
}
@ -515,7 +518,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let ringtone = adminMessage.getRingtoneResponse
upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: Int64(packet.from), context: context)
} else {
MeshLogger.log("🕸️ MESH PACKET received for Admin App \(try! packet.decoded.jsonString())")
MeshLogger.log("🕸️ MESH PACKET received Admin App UNHANDLED \((try? packet.decoded.jsonString()) ?? "JSON Decode Failure")")
}
// Save an ack for the admin message log for each admin message response received as we stopped sending acks if there is also a response to reduce airtime.
adminResponseAck(packet: packet, context: context)
@ -542,33 +545,33 @@ func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) {
do {
try context.save()
} catch {
print("Failed to save admin message response as an ack")
Logger.data.error("Failed to save admin message response as an ack: \(error.localizedDescription)")
}
}
} catch {
print("Failed to fetch admin message by requestID")
Logger.data.error("Failed to fetch admin message by requestID: \(error.localizedDescription)")
}
}
func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.paxcounter %@".localized, String(packet.from))
MeshLogger.log("🧑‍🤝‍🧑 \(logString)")
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity]
if let paxMessage = try? Paxcount(serializedData: packet.decoded.payload) {
let newPax = PaxCounterEntity(context: context)
newPax.ble = Int32(truncatingIfNeeded: paxMessage.ble)
newPax.wifi = Int32(truncatingIfNeeded: paxMessage.wifi)
newPax.uptime = Int32(truncatingIfNeeded: paxMessage.uptime)
newPax.time = Date()
if (fetchedNode?.count ?? 0 > 0) {
if fetchedNode?.count ?? 0 > 0 {
guard let mutablePax = fetchedNode?[0].pax!.mutableCopy() as? NSMutableOrderedSet else {
return
}
@ -577,14 +580,14 @@ func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
do {
try context.save()
} catch {
print("Failed to save pax")
Logger.data.error("Failed to save pax: \(error.localizedDescription)")
}
} else {
// Node Info Not Found
Logger.data.info("Node Info Not Found")
}
}
} catch {
}
}
@ -619,7 +622,7 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
}
fetchedMessage![0].ackSNR = packet.rxSnr
fetchedMessage![0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime)
if fetchedMessage![0].toUser != nil {
fetchedMessage![0].toUser!.objectWillChange.send()
} else {
@ -629,27 +632,22 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as? [MyInfoEntity]
if fetchedMyInfo?.count ?? 0 > 0 {
for ch in fetchedMyInfo![0].channels!.array as? [ChannelEntity] ?? [] {
if ch.index == packet.channel {
ch.objectWillChange.send()
}
for ch in fetchedMyInfo![0].channels!.array as? [ChannelEntity] ?? [] where ch.index == packet.channel {
ch.objectWillChange.send()
}
}
} catch {
}
} catch { }
}
} else {
return
}
try context.save()
print("💾 ACK Saved for Message: \(packet.decoded.requestID)")
Logger.data.info("💾 ACK Saved for Message: \(packet.decoded.requestID)")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving ACK for message: \(packet.id) Error: \(nsError)")
Logger.data.error("Error Saving ACK for message: \(packet.id) Error: \(nsError)")
}
}
}
@ -710,7 +708,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
try context.save()
// Only log telemetry from the mesh not the connected device
if connectedNode != Int64(packet.from) {
print("💾 Telemetry Saved for Node: \(packet.from)")
Logger.data.info("💾 Telemetry Saved for Node: \(packet.from)")
} else if telemetry.metricsType == 0 {
// Connected Device Metrics
// ------------------------
@ -743,7 +741,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
Task {
await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration)
// await meshActivity?.update(updatedContent)
print("Updated live activity.")
Logger.services.debug("Updated live activity.")
}
}
#endif
@ -751,10 +749,10 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving Telemetry for Node \(packet.from) Error: \(nsError)")
Logger.data.error("Error Saving Telemetry for Node \(packet.from) Error: \(nsError)")
}
} else {
print("💥 Error Fetching NodeInfoEntity for Node \(packet.from)")
Logger.data.error("Error Fetching NodeInfoEntity for Node \(packet.from)")
}
}
@ -772,20 +770,20 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
}
}
let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false
if !wantRangeTestPackets && rangeTest {
return
}
var storeForwardBroadcast = false
if storeForward {
if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) {
messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8)
messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8)
if storeAndForwardMessage.rr == .routerTextBroadcast {
storeForwardBroadcast = true
}
}
}
if messageText?.count ?? 0 > 0 {
MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)")
@ -833,11 +831,11 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
do {
try context.save()
print("💾 Saved a new message for \(newMessage.messageId)")
Logger.data.info("💾 Saved a new message for \(newMessage.messageId)")
messageSaved = true
if messageSaved {
if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications {
return
}
@ -862,7 +860,7 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
)
]
manager.schedule()
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
}
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
@ -876,7 +874,7 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
if !fetchedMyInfo.isEmpty {
appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
if channel.index == newMessage.channel {
context.refresh(channel, mergeChanges: true)
@ -894,7 +892,7 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
path: "meshtastic://messages?channel=\(newMessage.channel)&messageId=\(newMessage.messageId)")
]
manager.schedule()
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
}
}
}
@ -906,10 +904,10 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Failed to save new MessageEntity \(nsError)")
Logger.data.error("Failed to save new MessageEntity \(nsError)")
}
} catch {
print("💥 Fetch Message To and From Users Error")
Logger.data.error("Fetch Message To and From Users Error")
}
}
}
@ -946,7 +944,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
waypoint.created = Date()
do {
try context.save()
print("💾 Added Node Waypoint App Packet For: \(waypoint.id)")
Logger.data.info("💾 Added Node Waypoint App Packet For: \(waypoint.id)")
let manager = LocalNotificationManager()
let icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍")
let latitude = Double(waypoint.latitudeI) / 1e7
@ -961,12 +959,12 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
path: "meshtastic://map?waypontid=\(waypoint.id)"
)
]
print("meshtastic://map?waypontid=\(waypoint.id)")
Logger.data.debug("meshtastic://map?waypontid=\(waypoint.id)")
manager.schedule()
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving WaypointEntity from WAYPOINT_APP \(nsError)")
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError)")
}
} else {
fetchedWaypoint[0].id = Int64(packet.id)
@ -984,15 +982,15 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
fetchedWaypoint[0].lastUpdated = Date()
do {
try context.save()
print("💾 Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id)")
Logger.data.info("💾 Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id)")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving WaypointEntity from WAYPOINT_APP \(nsError)")
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError)")
}
}
}
} catch {
print("💥 Error Deserializing WAYPOINT_APP packet.")
Logger.mesh.error("Error Deserializing WAYPOINT_APP packet.")
}
}

View file

@ -7,6 +7,7 @@
import Foundation
import CocoaMQTT
import OSLog
protocol MqttClientProxyManagerDelegate: AnyObject {
func onMqttConnected()
@ -39,7 +40,7 @@ class MqttClientProxyManager {
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
let username = node.mqttConfig?.username
@ -80,30 +81,28 @@ class MqttClientProxyManager {
}
}
func subscribe(topic: String, qos: CocoaMQTTQoS) {
print("📲 MQTT Client Proxy subscribed to: " + topic)
Logger.services.info("📲 MQTT Client Proxy subscribed to: \(topic)")
mqttClientProxy?.subscribe(topic, qos: qos)
}
func unsubscribe(topic: String) {
mqttClientProxy?.unsubscribe(topic)
print("📲 MQTT Client Proxy unsubscribe for: " + topic)
Logger.services.info("📲 MQTT Client Proxy unsubscribe for: \(topic)")
}
func publish(message: String, topic: String, qos: CocoaMQTTQoS) {
mqttClientProxy?.publish(topic, withString: message, qos: qos)
if debugLog {
print("📲 MQTT Client Proxy publish for: " + topic)
}
Logger.services.debug("📲 MQTT Client Proxy publish for: \(topic)")
}
func disconnect() {
if let client = mqttClientProxy {
client.disconnect()
print("📲 MQTT Client Proxy Disconnected")
Logger.services.info("📲 MQTT Client Proxy Disconnected")
}
}
}
extension MqttClientProxyManager: CocoaMQTTDelegate {
func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
print("📲 MQTT Client Proxy didConnectAck: \(ack)")
Logger.services.info("📲 MQTT Client Proxy didConnectAck: \(ack)")
if ack == .accept {
delegate?.onMqttConnected()
} else {
@ -125,13 +124,13 @@ extension MqttClientProxyManager: CocoaMQTTDelegate {
default:
errorDescription = "Unknown Error"
}
print(errorDescription)
Logger.services.error("\(errorDescription)")
delegate?.onMqttError(message: errorDescription)
self.disconnect()
}
}
func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
print("mqttDidDisconnect: \(err?.localizedDescription ?? "")")
Logger.services.debug("mqttDidDisconnect: \(err?.localizedDescription ?? "")")
if let error = err {
delegate?.onMqttError(message: error.localizedDescription)
@ -139,32 +138,26 @@ extension MqttClientProxyManager: CocoaMQTTDelegate {
delegate?.onMqttDisconnected()
}
func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {
if debugLog {
print("📲 MQTT Client Proxy didPublishMessage from MqttClientProxyManager: \(message)")
}
Logger.services.debug("📲 MQTT Client Proxy didPublishMessage from MqttClientProxyManager: \(message)")
}
func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) {
if debugLog {
print("📲 MQTT Client Proxy didPublishAck from MqttClientProxyManager: \(id)")
}
Logger.services.debug("📲 MQTT Client Proxy didPublishAck from MqttClientProxyManager: \(id)")
}
public func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) {
delegate?.onMqttMessageReceived(message: message)
if debugLog {
print("📲 MQTT Client Proxy message received on topic: \(message.topic)")
}
Logger.services.debug("📲 MQTT Client Proxy message received on topic: \(message.topic)")
}
func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) {
print("📲 MQTT Client Proxy didSubscribeTopics: \(success.allKeys.count) topics. failed: \(failed.count) topics")
Logger.services.info("📲 MQTT Client Proxy didSubscribeTopics: \(success.allKeys.count) topics. failed: \(failed.count) topics")
}
func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopics topics: [String]) {
print("didUnsubscribeTopics: \(topics.joined(separator: ", "))")
Logger.services.info("didUnsubscribeTopics: \(topics.joined(separator: ", "))")
}
func mqttDidPing(_ mqtt: CocoaMQTT) {
print("📲 MQTT Client Proxy mqttDidPing")
Logger.services.info("📲 MQTT Client Proxy mqttDidPing")
}
func mqttDidReceivePong(_ mqtt: CocoaMQTT) {
print("📲 MQTT Client Proxy mqttDidReceivePong")
Logger.services.info("📲 MQTT Client Proxy mqttDidReceivePong")
}
}

View file

@ -7,6 +7,7 @@
import Foundation
import Network
import OSLog
class NetworkManager {
static let shared = NetworkManager()
@ -16,7 +17,7 @@ class NetworkManager {
pathMonitor.pathUpdateHandler = {
guard $0.status == .satisfied else {
// No network available
print("Network Not available")
Logger.services.info("Network Not available")
return pathMonitor.cancel()
}
pathMonitor.cancel()

View file

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

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23F5074a" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23F79" 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"/>
@ -229,6 +229,7 @@
<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="firstHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<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"/>
@ -275,9 +276,12 @@
</entity>
<entity name="PaxCounterEntity" representedClassName="PaxCounterEntity" syncable="YES" codeGenerationType="class">
<attribute name="ble" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="bleThreshold" optional="YES" attributeType="Integer 32" defaultValueString="-80" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="updateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="uptime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifiThreshold" optional="YES" attributeType="Integer 32" defaultValueString="-80" 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">

View file

@ -0,0 +1,464 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="green" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ledState" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="red" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="ambientLightingConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="ambientLightingConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
<attribute name="downlinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="positionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
<attribute name="psk" optional="YES" attributeType="Binary"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="uplinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
<fetchedProperty name="allPrivateMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="channel == $FETCH_SOURCE.index &amp;&amp; toUser == nil AND isEmoji == false"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="index"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="detectionTriggeredHigh" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="disableTripleClick" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="ledHeartbeatEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" 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"/>
<attribute name="tzdef" optional="YES" attributeType="String"/>
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hwModel" optional="YES" attributeType="String"/>
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<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="firstHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<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="bleThreshold" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="updateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifiThreshold" optional="YES" attributeType="Integer 32" defaultValueString="-80" 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="distance" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="elevationGain" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="notes" optional="YES" attributeType="String"/>
<relationship name="locations" optional="YES" 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="iaq" optional="YES" attributeType="Integer 32" defaultValueString="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="uptimeSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="route" optional="YES" attributeType="Transformable" customClassName="[UInt32]"/>
<attribute name="routeText" optional="YES" attributeType="String"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
</entity>
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
<attribute name="hwModel" attributeType="String"/>
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="longName" attributeType="String"/>
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numString" optional="YES" attributeType="String"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="shortName" attributeType="String"/>
<attribute name="userId" attributeType="String"/>
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="adminMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND isEmoji == false AND admin = true"/>
</fetchedProperty>
<fetchedProperty name="allMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10 "/>
</fetchedProperty>
<fetchedProperty name="detectionSensorMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND portNum = 10"/>
</fetchedProperty>
</entity>
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View file

@ -2,6 +2,7 @@
import SwiftUI
import CoreData
import OSLog
#if canImport(TipKit)
import TipKit
#endif
@ -9,7 +10,7 @@ import TipKit
@available(iOS 17.0, *)
@main
struct MeshtasticAppleApp: App {
@UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) var appDelegate
let persistenceController = PersistenceController.shared
@ObservedObject private var bleManager: BLEManager = BLEManager.shared
@ -28,13 +29,13 @@ struct MeshtasticAppleApp: App {
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(bleManager)
.sheet(isPresented: $saveChannels) {
SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager)
SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager)
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
print("URL received \(userActivity)")
Logger.mesh.debug("URL received \(userActivity)")
self.incomingUrl = userActivity.webpageURL
if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#")) != nil {
@ -44,25 +45,25 @@ struct MeshtasticAppleApp: App {
}
self.channelSettings = cs
self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false
print("Add Channel \(self.addChannels)")
Logger.services.debug("Add Channel \(self.addChannels)")
}
self.saveChannels = true
print("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")")
Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")")
}
if self.saveChannels {
print("User wants to open Channel Settings URL: \(String(describing: self.incomingUrl!.relativeString))")
Logger.mesh.debug("User wants to open Channel Settings URL: \(String(describing: self.incomingUrl!.relativeString))")
}
}
.onOpenURL(perform: { (url) in
print("Some sort of URL was received \(url)")
Logger.mesh.debug("Some sort of URL was received \(url)")
self.incomingUrl = url
if url.absoluteString.lowercased().contains("meshtastic.org/e/#") {
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
self.channelSettings = components.last!
}
self.saveChannels = true
print("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")")
Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")")
} else if url.absoluteString.lowercased().contains("meshtastic://") {
appState.navigationPath = url.absoluteString
let path = appState.navigationPath ?? ""
@ -71,13 +72,12 @@ struct MeshtasticAppleApp: App {
} else if path.starts(with: "meshtastic://nodes") {
AppState.shared.tabSelection = Tab.nodes
}
} else {
saveChannels = false
print("User wants to import a MBTILES offline map file: \(self.incomingUrl?.absoluteString ?? "No Tiles link")")
Logger.mesh.debug("User wants to import a MBTILES offline map file: \(self.incomingUrl?.absoluteString ?? "No Tiles link")")
}
/// Only do the map tiles stuff if it is enabled
if UserDefaults.enableOfflineMapsMBTiles {
/// we are expecting a .mbtiles map file that contains raster data
@ -87,32 +87,32 @@ struct MeshtasticAppleApp: App {
let destination = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false)
if !self.saveChannels {
// tell the system we want the file please
guard url.startAccessingSecurityScopedResource() else {
return
}
// do we need to delete an old one?
if fileManager.fileExists(atPath: destination.path) {
print(" Found an old map file. Deleting it")
Logger.mesh.info("Found an old map file. Deleting it")
try? fileManager.removeItem(atPath: destination.path)
}
do {
try fileManager.copyItem(at: url, to: destination)
} catch {
print("Copy MB Tile file failed. Error: \(error)")
Logger.mesh.error("Copy MB Tile file failed. Error: \(error.localizedDescription)")
}
if fileManager.fileExists(atPath: destination.path) {
print(" Saved the map file")
Logger.mesh.info("Saved the map file")
// need to tell the map view that it needs to update and try loading the new overlay
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastUpdatedLocalMapFile")
} else {
print("💥 Didn't save the map file")
Logger.mesh.error("Didn't save the map file")
}
}
}
@ -140,22 +140,22 @@ struct MeshtasticAppleApp: App {
.onChange(of: scenePhase) { (newScenePhase) in
switch newScenePhase {
case .background:
print(" Scene is in the background")
Logger.services.info("🍏 Scene is in the background")
do {
try persistenceController.container.viewContext.save()
print("💾 Saved CoreData ViewContext when the app went to the background.")
Logger.services.info("💾 Saved CoreData ViewContext when the app went to the background.")
} catch {
print("💥 Failed to save viewContext when the app goes to the background.")
Logger.services.error("💥 Failed to save viewContext when the app goes to the background.")
}
case .inactive:
print(" Scene is inactive")
Logger.services.info("🍏 Scene is inactive")
case .active:
print(" Scene is active")
Logger.services.info("🍏 Scene is active")
@unknown default:
print("💥 Apple must have changed something")
Logger.services.error("🍎 Apple must have changed something")
}
}
}
@ -168,6 +168,5 @@ class AppState: ObservableObject {
@Published var unreadDirectMessages: Int = 0
@Published var unreadChannelMessages: Int = 0
@Published var firmwareVersion: String = "0.0.0"
//@Published var connectedNode: NodeInfoEntity?
@Published var navigationPath: String?
}

View file

@ -6,14 +6,15 @@
//
import SwiftUI
import OSLog
class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("🚀 Meshtstic Apple App launched!")
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
Logger.services.info("🚀 Meshtstic Apple App launched!")
// Default User Default Values
UserDefaults.standard.register(defaults: ["meshMapRecentering" : true])
UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory" : true])
UserDefaults.standard.register(defaults: ["meshMapShowRouteLines" : true])
UserDefaults.standard.register(defaults: ["meshMapRecentering": true])
UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory": true])
UserDefaults.standard.register(defaults: ["meshMapShowRouteLines": true])
UNUserNotificationCenter.current().delegate = self
if #available(iOS 17.0, macOS 14.0, *) {
let locationsHandler = LocationsHandler.shared

View file

@ -6,6 +6,7 @@
//
import CoreData
import OSLog
class PersistenceController {
@ -47,7 +48,7 @@ class PersistenceController {
if let error = error as NSError? {
print("💥 CoreData Error: \(error.localizedDescription). Now attempting to truncate CoreData database. All app data will be lost.")
Logger.data.error("CoreData Error: \(error.localizedDescription). Now attempting to truncate CoreData database. All app data will be lost.")
self.clearDatabase()
}
})
@ -59,16 +60,16 @@ class PersistenceController {
let persistentStoreCoordinator = self.container.persistentStoreCoordinator
do {
try persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil)
print("💥 CoreData database truncated. All app data has been erased.")
Logger.data.error("CoreData database truncated. All app data has been erased.")
do {
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
} catch let error {
print("💣 Failed to re-create CoreData database: " + error.localizedDescription)
Logger.data.error("Failed to re-create CoreData database: \(error.localizedDescription)")
}
} catch let error {
print("💣 Failed to destroy CoreData database, delete the app and re-install to clear data. Attempted to clear persistent store: " + error.localizedDescription)
Logger.data.error("Failed to destroy CoreData database, delete the app and re-install to clear data. Attempted to clear persistent store: \(error.localizedDescription)")
}
}
}

View file

@ -26,7 +26,7 @@ public func getNodeInfo(id: Int64, context: NSManagedObjectContext) -> NodeInfoE
}
public func getStoreAndForwardMessageIds(seconds: Int, context: NSManagedObjectContext) -> [UInt32] {
let time = seconds * -1
let fetchMessagesRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MessageEntity")
let timeRange = Calendar.current.date(byAdding: .minute, value: time, to: Date())

View file

@ -5,6 +5,7 @@
// Copyright(c) Garth Vander Houwen 10/3/22.
import CoreData
import OSLog
public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool {
@ -26,7 +27,7 @@ public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool {
return false
}
} catch {
print("💥 Fetch NodeInfoEntity Error")
Logger.data.error("Fetch NodeInfoEntity Error")
return false
}
}
@ -51,7 +52,7 @@ public func clearPositions(destNum: Int64, context: NSManagedObjectContext) -> B
return false
}
} catch {
print("💥 Fetch NodeInfoEntity Error")
Logger.data.error("Fetch NodeInfoEntity Error")
return false
}
}
@ -76,7 +77,7 @@ public func clearTelemetry(destNum: Int64, metricsType: Int32, context: NSManage
return false
}
} catch {
print("💥 Fetch NodeInfoEntity Error")
Logger.data.error("Fetch NodeInfoEntity Error")
return false
}
}
@ -89,7 +90,7 @@ public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObje
}
try context.save()
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
Logger.data.error("\(error.localizedDescription)")
}
}
@ -102,7 +103,7 @@ public func deleteUserMessages(user: UserEntity, context: NSManagedObjectContext
}
try context.save()
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
Logger.data.error("\(error.localizedDescription)")
}
}
@ -110,12 +111,12 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes
let persistenceController = PersistenceController.shared.container
for i in 0...persistenceController.managedObjectModel.entities.count-1 {
let entity = persistenceController.managedObjectModel.entities[i]
let query = NSFetchRequest<NSFetchRequestResult>(entityName: entity.name!)
var deleteRequest = NSBatchDeleteRequest(fetchRequest: query)
let entityName = entity.name ?? "UNK"
if includeRoutes {
deleteRequest = NSBatchDeleteRequest(fetchRequest: query)
} else if !includeRoutes {
@ -125,8 +126,8 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes
}
do {
try context.executeAndMergeChanges(using: deleteRequest)
} catch let error as NSError {
print(error)
} catch {
Logger.data.error("\(error.localizedDescription)")
}
}
}
@ -149,11 +150,12 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
let newNode = NodeInfoEntity(context: context)
newNode.id = Int64(packet.from)
newNode.num = Int64(packet.from)
newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
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)
}
@ -161,16 +163,16 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
newNode.hopsAway = Int32(nodeInfoMessage.hopsAway)
newNode.favorite = nodeInfoMessage.isFavorite
}
if let newUserMessage = try? User(serializedData: packet.decoded.payload) {
if newUserMessage.id.isEmpty {
if newUserMessage.id.isEmpty {
if packet.from > Int16.max {
let newUser = createUser(num: Int64(packet.from), context: context)
newNode.user = newUser
}
} else {
let newUser = UserEntity(context: context)
newUser.userId = newUserMessage.id
newUser.num = Int64(packet.from)
@ -179,9 +181,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
newUser.role = Int32(newUserMessage.role.rawValue)
newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased()
newNode.user = newUser
if (UserDefaults.newNodeNotifications){
if UserDefaults.newNodeNotifications {
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
@ -202,7 +203,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
fetchedNode[0].user = newUser
}
}
if newNode.user == nil && packet.from > Int16.max {
newNode.user = createUser(num: Int64(packet.from), context: context)
}
@ -212,20 +213,23 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
myInfoEntity.rebootCount = 0
do {
try context.save()
print("💾 Saved a new myInfo for node number: \(String(packet.from))")
Logger.data.info("💾 Saved a new myInfo for node number: \(String(packet.from))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Inserting New Core Data MyInfoEntity: \(nsError)")
Logger.data.error("Error Inserting New Core Data MyInfoEntity: \(nsError)")
}
newNode.myInfo = myInfoEntity
} else {
// Update an existing node
fetchedNode[0].id = Int64(packet.from)
fetchedNode[0].num = Int64(packet.from)
if packet.rxTime > 0 {
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
if fetchedNode[0].firstHeard == nil {
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
}
}
fetchedNode[0].snr = packet.rxSnr
fetchedNode[0].rssi = packet.rxRssi
@ -260,21 +264,21 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
} else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart {
fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit)
}
if (fetchedNode[0].user == nil) {
if fetchedNode[0].user == nil {
let newUser = createUser(num: Int64(packet.from), context: context)
fetchedNode[0].user! = newUser
}
do {
try context.save()
print("💾 Updated NodeInfo from Node Info App Packet For: \(fetchedNode[0].num)")
Logger.data.info("💾 Updated NodeInfo from Node Info App Packet For: \(fetchedNode[0].num)")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving NodeInfoEntity from NODEINFO_APP \(nsError)")
Logger.data.error("Error Saving NodeInfoEntity from NODEINFO_APP \(nsError)")
}
}
} catch {
print("💥 Error Fetching NodeInfoEntity for NODEINFO_APP")
Logger.data.error("Error Fetching NodeInfoEntity for NODEINFO_APP")
}
}
@ -319,7 +323,11 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
position.altitude = positionMessage.altitude
position.satsInView = Int32(positionMessage.satsInView)
position.speed = Int32(positionMessage.groundSpeed)
position.heading = Int32(positionMessage.groundTrack)
let heading = Int32(positionMessage.groundTrack)
// Throw out bad haeadings from the device
if heading >= 0 && heading <= 360 {
position.heading = Int32(positionMessage.groundTrack)
}
position.precisionBits = Int32(positionMessage.precisionBits)
if positionMessage.timestamp != 0 {
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp)))
@ -331,8 +339,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
}
/// 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 || position.precisionBits == 0) {
let mostRecent = mutablePositions.lastObject as! PositionEntity
if mostRecent.coordinate.distance(from: position.coordinate) < 15.0 {
if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 15.0 {
mutablePositions.remove(mostRecent)
}
} else if mutablePositions.count > 0 {
@ -355,11 +362,11 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
do {
try context.save()
print("💾 Updated Node Position Coordinates, SNR and Time from Position App Packet For: \(fetchedNode[0].num)")
Logger.data.info("💾 Updated Node Position Coordinates, SNR and Time from Position App Packet For: \(fetchedNode[0].num)")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError)")
Logger.data.error("Error Saving NodeInfoEntity from POSITION_APP \(nsError)")
}
}
} else {
@ -367,13 +374,12 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
if (try? NodeInfo(serializedData: packet.decoded.payload)) != nil {
upsertNodeInfoPacket(packet: packet, context: context)
} else {
print("💥 Empty POSITION_APP Packet")
print((try? packet.jsonString()) ?? "JSON Decode Failure")
Logger.data.error("Empty POSITION_APP Packet: \((try? packet.jsonString()) ?? "JSON Decode Failure")")
}
}
}
} catch {
print("💥 Error Deserializing POSITION_APP packet.")
Logger.data.error("Error Deserializing POSITION_APP packet.")
}
}
@ -404,18 +410,18 @@ func upsertBluetoothConfigPacket(config: Meshtastic.Config.BluetoothConfig, node
}
do {
try context.save()
print("💾 Updated Bluetooth Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Bluetooth Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data BluetoothConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Bluetooth Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Bluetooth Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data BluetoothConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data BluetoothConfigEntity failed: \(nsError)")
}
}
@ -461,16 +467,16 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I
}
do {
try context.save()
print("💾 Updated Device Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Device Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data DeviceConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data DeviceConfigEntity: \(nsError)")
}
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data DeviceConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data DeviceConfigEntity failed: \(nsError)")
}
}
@ -519,24 +525,24 @@ func upsertDisplayConfigPacket(config: Meshtastic.Config.DisplayConfig, nodeNum:
do {
try context.save()
print("💾 Updated Display Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Display Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data DisplayConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data DisplayConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Display Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Display Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data DisplayConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data DisplayConfigEntity failed: \(nsError)")
}
}
@ -592,18 +598,18 @@ func upsertLoRaConfigPacket(config: Meshtastic.Config.LoRaConfig, nodeNum: Int64
}
do {
try context.save()
print("💾 Updated LoRa Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated LoRa Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data LoRaConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data LoRaConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Lora Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Lora Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data LoRaConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data LoRaConfigEntity failed: \(nsError)")
}
}
@ -638,19 +644,19 @@ func upsertNetworkConfigPacket(config: Meshtastic.Config.NetworkConfig, nodeNum:
do {
try context.save()
print("💾 Updated Network Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Network Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data WiFiConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data WiFiConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Network Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Network Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data NetworkConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data NetworkConfigEntity failed: \(nsError)")
}
}
@ -702,18 +708,18 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu
}
do {
try context.save()
print("💾 Updated Position Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Position Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data PositionConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data PositionConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Position Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Position Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data PositionConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data PositionConfigEntity failed: \(nsError)")
}
}
@ -751,18 +757,18 @@ func upsertPowerConfigPacket(config: Meshtastic.Config.PowerConfig, nodeNum: Int
}
do {
try context.save()
print("💾 Updated Power Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Power Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data PowerConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data PowerConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Power Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Power Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data PowerConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data PowerConfigEntity failed: \(nsError)")
}
}
@ -794,7 +800,7 @@ func upsertAmbientLightingModuleConfigPacket(config: Meshtastic.ModuleConfig.Amb
fetchedNode[0].ambientLightingConfig = newAmbientLightingConfig
} else {
if fetchedNode[0].ambientLightingConfig == nil {
fetchedNode[0].ambientLightingConfig = AmbientLightingConfigEntity(context: context)
}
@ -807,18 +813,18 @@ func upsertAmbientLightingModuleConfigPacket(config: Meshtastic.ModuleConfig.Amb
do {
try context.save()
print("💾 Updated Ambient Lighting Module Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Ambient Lighting Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data AmbientLightingConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data AmbientLightingConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Ambient Lighting Module Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Ambient Lighting Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data AmbientLightingConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data AmbientLightingConfigEntity failed: \(nsError)")
}
}
@ -871,18 +877,18 @@ func upsertCannedMessagesModuleConfigPacket(config: Meshtastic.ModuleConfig.Cann
do {
try context.save()
print("💾 Updated Canned Message Module Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Canned Message Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data CannedMessageConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data CannedMessageConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Canned Message Module Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Canned Message Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data CannedMessageConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data CannedMessageConfigEntity failed: \(nsError)")
}
}
@ -929,21 +935,21 @@ func upsertDetectionSensorModuleConfigPacket(config: Meshtastic.ModuleConfig.Det
do {
try context.save()
print("💾 Updated Detection Sensor Module Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Detection Sensor Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data DetectionSensorConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data DetectionSensorConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Detection Sensor Module Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Detection Sensor Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data DetectionSensorConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data DetectionSensorConfigEntity failed: \(nsError)")
}
}
@ -1002,18 +1008,18 @@ func upsertExternalNotificationModuleConfigPacket(config: Meshtastic.ModuleConfi
do {
try context.save()
print("💾 Updated External Notification Module Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated External Notification Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data ExternalNotificationConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data ExternalNotificationConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save External Notification Module Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save External Notification Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data ExternalNotificationConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data ExternalNotificationConfigEntity failed: \(nsError)")
}
}
@ -1036,29 +1042,29 @@ func upsertPaxCounterModuleConfigPacket(config: Meshtastic.ModuleConfig.Paxcount
if fetchedNode[0].paxCounterConfig == nil {
let newPaxCounterConfig = PaxCounterConfigEntity(context: context)
newPaxCounterConfig.enabled = config.enabled
newPaxCounterConfig.paxcounterUpdateInterval = Int32(config.paxcounterUpdateInterval)
newPaxCounterConfig.updateInterval = Int32(config.paxcounterUpdateInterval)
fetchedNode[0].paxCounterConfig = newPaxCounterConfig
} else {
fetchedNode[0].paxCounterConfig?.enabled = config.enabled
fetchedNode[0].paxCounterConfig?.paxcounterUpdateInterval = Int32(config.paxcounterUpdateInterval)
fetchedNode[0].paxCounterConfig?.updateInterval = Int32(config.paxcounterUpdateInterval)
}
do {
try context.save()
print("💾 Updated PAX Counter Module Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated PAX Counter Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data ExternalNotificationConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data ExternalNotificationConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save PAX Counter Module Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save PAX Counter Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data PaxCounterConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data PaxCounterConfigEntity failed: \(nsError)")
}
}
@ -1086,18 +1092,18 @@ func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManage
}
do {
try context.save()
print("💾 Updated RTTTL Ringtone Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated RTTTL Ringtone Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data RtttlConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data RtttlConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save RTTTL Ringtone Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save RTTTL Ringtone Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data RtttlConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data RtttlConfigEntity failed: \(nsError)")
}
}
@ -1148,18 +1154,18 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no
}
do {
try context.save()
print("💾 Updated MQTT Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated MQTT Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data MQTTConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data MQTTConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save MQTT Module Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save MQTT Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data MQTTConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data MQTTConfigEntity failed: \(nsError)")
}
}
@ -1191,18 +1197,18 @@ func upsertRangeTestModuleConfigPacket(config: Meshtastic.ModuleConfig.RangeTest
}
do {
try context.save()
print("💾 Updated Range Test Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Range Test Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data RangeTestConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data RangeTestConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Range Test Module Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Range Test Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data RangeTestConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data RangeTestConfigEntity failed: \(nsError)")
}
}
@ -1247,25 +1253,25 @@ func upsertSerialModuleConfigPacket(config: Meshtastic.ModuleConfig.SerialConfig
do {
try context.save()
print("💾 Updated Serial Module Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Serial Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data SerialConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data SerialConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Serial Module Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Serial Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data SerialConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data SerialConfigEntity failed: \(nsError)")
}
}
@ -1304,18 +1310,18 @@ func upsertStoreForwardModuleConfigPacket(config: Meshtastic.ModuleConfig.StoreF
}
do {
try context.save()
print("💾 Updated Store & Forward Module Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Store & Forward Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data StoreForwardConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data StoreForwardConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Store & Forward Module Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Store & Forward Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data DetectionSensorConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data DetectionSensorConfigEntity failed: \(nsError)")
}
}
@ -1361,20 +1367,20 @@ func upsertTelemetryModuleConfigPacket(config: Meshtastic.ModuleConfig.Telemetry
do {
try context.save()
print("💾 Updated Telemetry Module Config for node number: \(String(nodeNum))")
Logger.data.info("💾 Updated Telemetry Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data TelemetryConfigEntity: \(nsError)")
Logger.data.error("Error Updating Core Data TelemetryConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Telemetry Module Config")
Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Telemetry Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data TelemetryConfigEntity failed: \(nsError)")
Logger.data.error("Fetching node for core data TelemetryConfigEntity failed: \(nsError)")
}
}

View file

@ -383,11 +383,23 @@ struct GeoChat {
/// Clears the value of `to`. Subsequent reads from it will return its default value.
mutating func clearTo() {self._to = nil}
///
/// Callsign of the recipient for the message
var toCallsign: String {
get {return _toCallsign ?? String()}
set {_toCallsign = newValue}
}
/// Returns true if `toCallsign` has been explicitly set.
var hasToCallsign: Bool {return self._toCallsign != nil}
/// Clears the value of `toCallsign`. Subsequent reads from it will return its default value.
mutating func clearToCallsign() {self._toCallsign = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
fileprivate var _to: String? = nil
fileprivate var _toCallsign: String? = nil
}
///
@ -633,6 +645,7 @@ extension GeoChat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "message"),
2: .same(proto: "to"),
3: .standard(proto: "to_callsign"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -643,6 +656,7 @@ extension GeoChat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.message) }()
case 2: try { try decoder.decodeSingularStringField(value: &self._to) }()
case 3: try { try decoder.decodeSingularStringField(value: &self._toCallsign) }()
default: break
}
}
@ -659,12 +673,16 @@ extension GeoChat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa
try { if let v = self._to {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
} }()
try { if let v = self._toCallsign {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
} }()
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: GeoChat, rhs: GeoChat) -> Bool {
if lhs.message != rhs.message {return false}
if lhs._to != rhs._to {return false}
if lhs._toCallsign != rhs._toCallsign {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View file

@ -116,6 +116,10 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
///
/// AMS TSL25911FN RGB Light Sensor
case tsl25911Fn // = 22
///
/// AHT10 Integrated temperature and humidity sensor
case aht10 // = 23
case UNRECOGNIZED(Int)
init() {
@ -147,6 +151,7 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
case 20: self = .opt3001
case 21: self = .ltr390Uv
case 22: self = .tsl25911Fn
case 23: self = .aht10
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -176,6 +181,7 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
case .opt3001: return 20
case .ltr390Uv: return 21
case .tsl25911Fn: return 22
case .aht10: return 23
case .UNRECOGNIZED(let i): return i
}
}
@ -210,6 +216,7 @@ extension TelemetrySensorType: CaseIterable {
.opt3001,
.ltr390Uv,
.tsl25911Fn,
.aht10,
]
}
@ -535,6 +542,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding {
20: .same(proto: "OPT3001"),
21: .same(proto: "LTR390UV"),
22: .same(proto: "TSL25911FN"),
23: .same(proto: "AHT10"),
]
}

View file

@ -33,11 +33,11 @@ struct ContactsTip: Tip {
return "tip.messages.contacts"
}
var title: Text {
//Text("tip.messages.contacts.title")
// Text("tip.messages.contacts.title")
Text("Contacts")
}
var message: Text? {
//Text("tip.messages.contacts.message")
// Text("tip.messages.contacts.message")
Text("Each node is an available contact. Contacts with recent messages or marked as favorites show up at the top of the list. Select a contact to send or view messages. Long press to favorite or mute the contact or delete the conversation.")
}
var image: Image? {

View file

@ -10,6 +10,7 @@ import MapKit
import CoreData
import CoreLocation
import CoreBluetooth
import OSLog
#if canImport(TipKit)
import TipKit
#endif
@ -34,9 +35,9 @@ struct Connect: View {
if settings.authorizationStatus == .notDetermined {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
print("Notifications are all set!")
Logger.services.info("Notifications are all set!")
} else if let error = error {
print(error.localizedDescription)
Logger.services.error("\(error.localizedDescription)")
}
}
}
@ -71,7 +72,7 @@ struct Connect: View {
Text("subscribed").font(.callout)
.foregroundColor(.green)
} else {
HStack {
if #available(iOS 17.0, macOS 14.0, *) {
Image(systemName: "square.stack.3d.down.forward")
@ -104,12 +105,12 @@ struct Connect: View {
Button {
if !liveActivityStarted {
#if canImport(ActivityKit)
print("Start live activity.")
Logger.services.info("Start live activity.")
startNodeActivity()
#endif
} else {
#if canImport(ActivityKit)
print("Stop live activity.")
Logger.services.info("Stop live activity.")
endActivity()
#endif
}
@ -123,9 +124,9 @@ struct Connect: View {
Text("BLE RSSI: \(bleManager.connectedPeripheral.rssi)")
Button {
if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!, adminIndex: node!.myInfo!.adminIndex) {
print("Shutdown Failed")
Logger.mesh.error("Shutdown Failed")
}
} label: {
Label("Power Off", systemImage: "power")
}
@ -233,7 +234,7 @@ struct Connect: View {
bleManager.disconnectPeripheral()
}
clearCoreDataDatabase(context: context, includeRoutes: false)
let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId })
if radio != nil {
bleManager.connectTo(peripheral: radio!.peripheral)
@ -242,7 +243,7 @@ struct Connect: View {
}
.textCase(nil)
}
} else {
Text("bluetooth.off")
.foregroundColor(.red)
@ -269,7 +270,7 @@ struct Connect: View {
if bleManager.isConnecting {
Button(role: .destructive, action: {
bleManager.cancelPeripheralConnection()
}) {
Label("disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
}
@ -346,18 +347,17 @@ struct Connect: View {
do {
let myActivity = try Activity<MeshActivityAttributes>.request(attributes: activityAttributes, content: activityContent,
pushType: nil)
print(" Requested MyActivity live activity. ID: \(myActivity.id)")
} catch let error {
print("Error requesting live activity: \(error.localizedDescription)")
Logger.services.info("Requested MyActivity live activity. ID: \(myActivity.id)")
} catch {
Logger.services.error("Error requesting live activity: \(error.localizedDescription)")
}
}
func endActivity() {
liveActivityStarted = false
Task {
for activity in Activity<MeshActivityAttributes>.activities {
// Check if this is the activity associated with this order.
if activity.attributes.nodeNum == node?.num ?? 0 { await activity.end(nil, dismissalPolicy: .immediate) }
for activity in Activity<MeshActivityAttributes>.activities where activity.attributes.nodeNum == node?.num ?? 0 {
await activity.end(nil, dismissalPolicy: .immediate)
}
}
}

View file

@ -56,19 +56,19 @@ struct ContentView: View {
}
}
}
//#Preview {
// #Preview {
// if #available(iOS 17.0, *) {
// // ContentView(deepLinkManager: .init())
// } else {
// // Fallback on earlier versions
// }
//}
// }
//struct ContentView_Previews: PreviewProvider {
// struct ContentView_Previews: PreviewProvider {
// static var previews: some View {
// ContentView()
// }
//}
// }
enum Tab: Hashable {
case contacts

View file

@ -9,17 +9,17 @@ import SwiftUI
import Charts
struct BatteryGauge: View {
@ObservedObject var node: NodeInfoEntity
private let minValue = 0.0
private let maxValue = 100.00
var body: some View {
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
let batteryLevel = Double(mostRecent?.batteryLevel ?? 0)
VStack {
if batteryLevel > 100.0 {
// Plugged in

View file

@ -7,7 +7,7 @@
import SwiftUI
struct BatteryLevelCompact: View {
@ObservedObject var node: NodeInfoEntity
var font: Font
@ -26,25 +26,25 @@ struct BatteryLevelCompact: View {
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel < 100 && batteryLevel > 74 {
Image(systemName: "battery.75")
.font(iconFont)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel < 75 && batteryLevel > 49 {
Image(systemName: "battery.50")
.font(iconFont)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel < 50 && batteryLevel > 14 {
Image(systemName: "battery.25")
.font(iconFont)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel < 15 && batteryLevel > 0 {
Image(systemName: "battery.0")
.font(iconFont)
.foregroundColor(color)

View file

@ -9,29 +9,19 @@ struct CircleText: View {
var text: String
var color: Color
var circleSize: CGFloat = 45
var body: some View {
ZStack {
Circle()
.fill(color)
.frame(width: circleSize, height: circleSize)
#if os(macOS)
Text(text)
.frame(width: circleSize * 0.95, height: circleSize * 0.95, alignment: .center)
.frame(width: circleSize * 0.9, height: circleSize * 0.9, alignment: .center)
.foregroundColor(color.isLight() ? .black : .white)
.font(.system(size: 3000))
.minimumScaleFactor(0.001)
#else
Text(text)
.frame(width: circleSize * 0.95, height: circleSize * 0.95, alignment: .center)
.foregroundColor(color.isLight() ? .black : .white)
.font(.system(size: 5000))
.minimumScaleFactor(0.001)
#endif
.font(.system(size: 1300))
}
.aspectRatio(1, contentMode: .fit)
}
}
@ -59,8 +49,7 @@ struct CircleText_Previews: PreviewProvider {
.previewLayout(.fixed(width: 300, height: 100))
}
HStack {
CircleText(text: "CW-A", color: Color.secondary)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "CW-A", color: Color.secondary, circleSize: 80)
@ -71,7 +60,7 @@ struct CircleText_Previews: PreviewProvider {
.previewLayout(.fixed(width: 300, height: 100))
}
HStack {
CircleText(text: "🚗", color: Color.orange)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "🔋", color: Color.indigo, circleSize: 80)

View file

@ -5,7 +5,6 @@ A view draws the indicator used in the upper right corner for views using BLE
import SwiftUI
struct ConnectedDevice: View {
var bluetoothOn: Bool
var deviceConnected: Bool
@ -22,7 +21,7 @@ struct ConnectedDevice: View {
if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly {
if bluetoothOn {
if deviceConnected {
if (mqttUplinkEnabled || mqttDownlinkEnabled) {
if mqttUplinkEnabled || mqttDownlinkEnabled {
MQTTIcon(connected: mqttProxyConnected, uplink: mqttUplinkEnabled, downlink: mqttDownlinkEnabled, topic: mqttTopic)
}
Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
@ -44,12 +43,9 @@ struct ConnectedDevice: View {
}
}
struct ConnectedDevice_Previews: PreviewProvider {
static var previews: some View {
VStack (alignment: .trailing) {
VStack(alignment: .trailing) {
ConnectedDevice(bluetoothOn: true, deviceConnected: true, name: "MEMO", mqttProxyConnected: true)
ConnectedDevice(bluetoothOn: true, deviceConnected: true, name: "MEMO", mqttProxyConnected: false, mqttUplinkEnabled: true, mqttDownlinkEnabled: true)
ConnectedDevice(bluetoothOn: true, deviceConnected: true, name: "MEMO", mqttProxyConnected: true, mqttUplinkEnabled: true, mqttDownlinkEnabled: true, mqttTopic: "msh/US/2/e/#")

View file

@ -17,7 +17,7 @@ struct DateTimeText: View {
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssa", options: 0, locale: Locale.current)
var body: some View {
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss a")

View file

@ -8,13 +8,13 @@ import SwiftUI
struct LastHeardText: View {
var lastHeard: Date?
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
static let formatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
return formatter
}()
var body: some View {
if lastHeard != nil && lastHeard! >= sixMonthsAgo! {
Text(lastHeard?.formatted() ?? "unknown.age".localized)

View file

@ -13,8 +13,8 @@ struct LoRaSignalStrengthMeter: View {
var preset: ModemPresets
var compact: Bool
var body: some View {
if (snr != 0.0 && rssi != 0) {
if snr != 0.0 && rssi != 0 {
let signalStrength = getLoRaSignalStrength(snr: snr, rssi: rssi, preset: preset)
let gradient = Gradient(colors: [.red, .orange, .yellow, .green])
if !compact {

View file

@ -18,8 +18,8 @@ struct MQTTIcon: View {
var body: some View {
Button( action: {
if(topic.length > 0) {self.isPopoverOpen.toggle()}
} ) {
if topic.length > 0 {self.isPopoverOpen.toggle()}
}) {
// the last one defaults to just showing up/down if it isn't specified b/c on the mqtt config screen, there's no information about uplink/downlink and no good alternative icon
Image(systemName: uplink && downlink ? "arrow.up.arrow.down.circle.fill" : uplink ? "arrow.up.circle.fill" : downlink ? "arrow.down.circle.fill" : "arrow.up.arrow.down.circle.fill")
.imageScale(.large)

View file

@ -21,13 +21,13 @@ struct AirQualityIndex: View {
var aqi: Int
var displayMode: IaqDisplayMode = .pill
let gradient = Gradient(colors: [.green, .yellow, .orange, .red, .purple, .magenta])
var body: some View {
let aqiEnum = Aqi.getAqi(for: aqi)
switch displayMode {
case .pill:
ZStack (alignment: .leading) {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.fill(aqiEnum.color)
.frame(width: 125, height: 30)
@ -48,7 +48,7 @@ struct AirQualityIndex: View {
.font(.caption)
case .gauge:
Gauge(value: Double(aqi), in: 0...500) {
Text("IAQ")
.foregroundColor(aqiEnum.color)
} currentValueLabel: {
@ -115,19 +115,19 @@ struct AirQualityIndex_Previews: PreviewProvider {
}
Text(".gauge")
.font(.title2)
HStack (alignment: .top) {
HStack(alignment: .top) {
AirQualityIndex(aqi: 6, displayMode: .gauge)
AirQualityIndex(aqi: 51, displayMode: .gauge)
AirQualityIndex(aqi: 101, displayMode: .gauge)
AirQualityIndex(aqi: 151, displayMode: .gauge)
}
HStack (alignment: .top) {
HStack(alignment: .top) {
AirQualityIndex(aqi: 201, displayMode: .gauge)
AirQualityIndex(aqi: 251, displayMode: .gauge)
AirQualityIndex(aqi: 301, displayMode: .gauge)
AirQualityIndex(aqi: 351, displayMode: .gauge)
}
HStack (alignment: .top) {
HStack(alignment: .top) {
AirQualityIndex(aqi: 401, displayMode: .gauge)
AirQualityIndex(aqi: 500, displayMode: .gauge)
}
@ -140,7 +140,7 @@ struct AirQualityIndex_Previews: PreviewProvider {
AirQualityIndex(aqi: 351, displayMode: .gradient)
AirQualityIndex(aqi: 401, displayMode: .gradient)
AirQualityIndex(aqi: 500, displayMode: .gradient)
}.previewLayout(.fixed(width: 300, height: 800))
}
}

View file

@ -10,7 +10,7 @@ import SwiftUI
struct IAQScale: View {
var body: some View {
VStack(alignment:.leading) {
VStack(alignment: .leading) {
ForEach(Iaq.allCases) { iaq in
HStack {
RoundedRectangle(cornerRadius: 5)

View file

@ -28,7 +28,7 @@ struct IndoorAirQuality: View {
let iaqEnum = Iaq.getIaq(for: iaq)
switch displayMode {
case .pill:
ZStack (alignment: .leading) {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.fill(iaqEnum.color)
.frame(width: 125, height: 30)
@ -49,7 +49,7 @@ struct IndoorAirQuality: View {
.font(.caption)
case .gauge:
Gauge(value: Double(iaq), in: 0...500) {
Text("IAQ")
.foregroundColor(iaqEnum.color)
} currentValueLabel: {
@ -117,19 +117,19 @@ struct IndoorAirQuality_Previews: PreviewProvider {
}
Text(".gauge")
.font(.title2)
HStack (alignment: .top) {
HStack(alignment: .top) {
IndoorAirQuality(iaq: 6, displayMode: .gauge)
IndoorAirQuality(iaq: 51, displayMode: .gauge)
IndoorAirQuality(iaq: 101, displayMode: .gauge)
IndoorAirQuality(iaq: 151, displayMode: .gauge)
}
HStack (alignment: .top) {
HStack(alignment: .top) {
IndoorAirQuality(iaq: 201, displayMode: .gauge)
IndoorAirQuality(iaq: 251, displayMode: .gauge)
IndoorAirQuality(iaq: 301, displayMode: .gauge)
IndoorAirQuality(iaq: 351, displayMode: .gauge)
}
HStack (alignment: .top) {
HStack(alignment: .top) {
IndoorAirQuality(iaq: 401, displayMode: .gauge)
IndoorAirQuality(iaq: 500, displayMode: .gauge)
}
@ -142,7 +142,7 @@ struct IndoorAirQuality_Previews: PreviewProvider {
IndoorAirQuality(iaq: 351, displayMode: .gradient)
IndoorAirQuality(iaq: 401, displayMode: .gradient)
IndoorAirQuality(iaq: 500, displayMode: .gradient)
}.previewLayout(.fixed(width: 300, height: 800))
}
}

View file

@ -9,6 +9,7 @@ import SwiftUI
import CoreLocation
import Charts
import WeatherKit
import OSLog
struct NodeWeatherForecastView: View {
var location: CLLocation
@ -34,7 +35,7 @@ struct NodeWeatherForecastView: View {
)
})
} catch {
print("Could not load weather", error.localizedDescription)
Logger.services.error("Could not load weather: \(error.localizedDescription)")
}
}
}

View file

@ -8,6 +8,7 @@
import UIKit
import MapKit
import SQLite
import OSLog
extension MKMapRect {
init(coordinates: [CLLocationCoordinate2D]) {
@ -83,7 +84,7 @@ class LocalMBTileOverlay: MKTileOverlay {
self._boundingMapRect = MKMapRect(coordinates: coords)
} catch {
print("💥 Map tile error: \(error)")
Logger.services.error("Map tile error: \(error)")
return nil
}
}
@ -102,7 +103,7 @@ class LocalMBTileOverlay: MKTileOverlay {
let data = Data(bytes: dataQuery[tileData].bytes, count: dataQuery[tileData].bytes.count)// dataQuery![tileData].bytes
result(data, nil)
} else {
print("💥 No tile here: x:\(tileX) y:\(tileY) z:\(tileZ)")
Logger.services.error("No tile here: x:\(tileX) y:\(tileY) z:\(tileZ)")
let error = NSError(domain: "LocalMBTileOverlay", code: 1, userInfo: ["reason": "no_tile"])
result(nil, error)
}

View file

@ -49,7 +49,7 @@ struct MapButtons: View {
}
// MARK: Previews
//struct MapControl_Previews: PreviewProvider {
// struct MapControl_Previews: PreviewProvider {
// @State static var tracking: UserTrackingModes = .none
// @State static var isPresentingInfoSheet = false
// static var previews: some View {
@ -61,4 +61,4 @@ struct MapButtons: View {
// }
// .previewLayout(.fixed(width: 60, height: 100))
// }
//}
// }

View file

@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import MapKit
import OSLog
struct PolygonInfo: Codable {
let stroke: String?
@ -75,7 +76,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
mapView.showsBuildings = true
mapView.showsScale = true
mapView.showsTraffic = true
mapView.showsCompass = false
let compass = MKCompassButton(mapView: mapView)
compass.translatesAutoresizingMaskIntoConstraints = false
@ -98,10 +99,8 @@ struct MapViewSwiftUI: UIViewRepresentable {
// Avoid refreshing UI if selectedLayer has not changed
guard currentMapLayer != selectedMapLayer else { return }
currentMapLayer = selectedMapLayer
for overlay in mapView.overlays {
if overlay is MKTileOverlay {
mapView.removeOverlay(overlay)
}
for overlay in mapView.overlays where overlay is MKTileOverlay {
mapView.removeOverlay(overlay)
}
switch selectedMapLayer {
case .offline:
@ -144,13 +143,13 @@ struct MapViewSwiftUI: UIViewRepresentable {
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path
if fileManager.fileExists(atPath: tilePath) {
print("Loading local map file")
Logger.services.info("Loading local map file")
if let overlay = LocalMBTileOverlay(mbTilePath: tilePath) {
overlay.canReplaceMapContent = false// customMapOverlay.canReplaceMapContent
mapView.addOverlay(overlay)
}
} else {
print("Couldn't find a local map file to load")
Logger.services.info("Couldn't find a local map file to load")
}
}
DispatchQueue.main.async {
@ -179,10 +178,8 @@ struct MapViewSwiftUI: UIViewRepresentable {
// Node Route Lines
if showRouteLines {
// Remove all existing PolyLine Overlays
for overlay in mapView.overlays {
if overlay is MKPolyline {
mapView.removeOverlay(overlay)
}
for overlay in mapView.overlays where overlay is MKPolyline {
mapView.removeOverlay(overlay)
}
var lineIndex = 0
for position in latest {
@ -201,15 +198,13 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
} else {
// Remove all existing PolyLine Overlays
for overlay in mapView.overlays {
if overlay is MKPolyline {
mapView.removeOverlay(overlay)
}
for overlay in mapView.overlays where overlay is MKPolyline {
mapView.removeOverlay(overlay)
}
}
let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count)
if annotationCount != mapView.annotations.count {
print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)")
Logger.services.info("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)")
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(waypoints)
}

View file

@ -8,9 +8,10 @@ import SwiftUI
import CoreLocation
import MapKit
import WeatherKit
import OSLog
struct NodeMapMapkit: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
/// Weather
@ -21,7 +22,7 @@ struct NodeMapMapkit: View {
@State private var symbolName: String = "cloud.fill"
@State private var attributionLink: URL?
@State private var attributionLogo: URL?
@Environment(\.colorScheme) var colorScheme: ColorScheme
@AppStorage("meshMapType") private var meshMapType = 0
@AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
@ -40,10 +41,9 @@ struct NodeMapMapkit: View {
), animation: .none)
private var waypoints: FetchedResults<WaypointEntity>
@ObservedObject var node: NodeInfoEntity
var body: some View {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
NavigationStack {
GeometryReader { bounds in
VStack {
@ -90,7 +90,7 @@ struct NodeMapMapkit: View {
VStack {
Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
.font(.caption)
Label("\(humidity ?? 0)%", systemImage: "humidity")
.font(.caption2)
@ -103,12 +103,12 @@ struct NodeMapMapkit: View {
.controlSize(.mini)
}
.frame(height: 10)
Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!)
.font(.caption2)
}
.padding(5)
}
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.padding(5)
@ -126,7 +126,7 @@ struct NodeMapMapkit: View {
attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL
}
} catch {
print("Could not gather weather information...", error.localizedDescription)
Logger.services.error("Could not gather weather information: \(error.localizedDescription)")
condition = .clear
symbolName = "cloud.fill"
}

View file

@ -7,6 +7,7 @@
import SwiftUI
import CoreLocation
import OSLog
struct WaypointFormMapKit: View {
@ -152,7 +153,7 @@ struct WaypointFormMapKit: View {
dismiss()
} else {
dismiss()
print("Send waypoint failed")
Logger.mesh.error("Send waypoint failed")
}
} label: {
Label("Send", systemImage: "arrow.up")
@ -212,7 +213,7 @@ struct WaypointFormMapKit: View {
dismiss()
} else {
dismiss()
print("Send waypoint failed")
Logger.mesh.error("Send waypoint failed")
}
})
}

View file

@ -7,9 +7,10 @@
import SwiftUI
import CoreData
import OSLog
struct ChannelList: View {
@StateObject var appState = AppState.shared
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ -20,133 +21,138 @@ struct ChannelList: View {
@State private var isPresentingDeleteChannelMessagesConfirm: Bool = false
@State private var isPresentingTraceRouteSentAlert = false
var restrictedChannels = ["gpio", "mqtt", "serial"]
var body: some View {
@ViewBuilder
private func makeNavigationLink(
myInfo: MyInfoEntity,
channel: ChannelEntity
) -> some View {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY")
NavigationLink(destination: ChannelMessageList(myInfo: myInfo, channel: channel)) {
let mostRecent = channel.allPrivateMessages.last(where: { $0.channel == channel.index })
let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 ))))
let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0
let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0
ZStack {
Image(systemName: "circle.fill")
.opacity(channel.unreadMessages > 0 ? 1 : 0)
.font(.system(size: 10))
.foregroundColor(.accentColor)
.brightness(0.2)
}
CircleText(text: String(channel.index), color: .accentColor)
.brightness(0.2)
VStack(alignment: .leading) {
HStack {
if channel.name?.isEmpty ?? false {
if channel.role == 1 {
Text(String("PrimaryChannel").camelCaseToWords())
.font(.headline)
} else {
Text(String("Channel \(channel.index)").camelCaseToWords())
.font(.headline)
}
} else {
Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords())
.font(.headline)
}
Spacer()
if channel.allPrivateMessages.count > 0 {
if lastMessageDay == currentDay {
Text(lastMessageTime, style: .time )
.font(.footnote)
.foregroundColor(.secondary)
} else if lastMessageDay == (currentDay - 1) {
Text("Yesterday")
.font(.footnote)
.foregroundColor(.secondary)
} else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) {
Text(lastMessageTime.formattedDate(format: dateFormatString))
.font(.footnote)
.foregroundColor(.secondary)
} else if lastMessageDay < (currentDay - 1800) {
Text(lastMessageTime.formattedDate(format: dateFormatString))
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
if channel.allPrivateMessages.count > 0 {
HStack(alignment: .top) {
Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")")
// .font(.system(size: 16))
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
}
}
var body: some View {
VStack {
// Display Contacts for the rest of the non admin channels
if node != nil && node!.myInfo != nil && node!.myInfo!.channels != nil {
List(node!.myInfo!.channels!.array as! [ChannelEntity], id: \.self, selection: $channelSelection) { (channel: ChannelEntity) in
if let node, let myInfo = node.myInfo, let channels = myInfo.channels?.array as? [ChannelEntity] {
List(channels, id: \.self, selection: $channelSelection) { (channel: ChannelEntity) in
if !restrictedChannels.contains(channel.name?.lowercased() ?? "") {
NavigationLink(destination: ChannelMessageList(myInfo: node!.myInfo!, channel: channel)) {
let mostRecent = channel.allPrivateMessages.last(where: { $0.channel == channel.index })
let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 ))))
let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0
let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0
ZStack {
Image(systemName: "circle.fill")
.opacity(channel.unreadMessages > 0 ? 1 : 0)
.font(.system(size: 10))
.foregroundColor(.accentColor)
.brightness(0.2)
}
CircleText(text: String(channel.index), color: .accentColor)
.brightness(0.2)
VStack(alignment: .leading){
HStack{
if channel.name?.isEmpty ?? false {
if channel.role == 1 {
Text(String("PrimaryChannel").camelCaseToWords())
.font(.headline)
} else {
Text(String("Channel \(channel.index)").camelCaseToWords())
.font(.headline)
}
} else {
Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords())
.font(.headline)
}
Spacer()
if channel.allPrivateMessages.count > 0 {
if lastMessageDay == currentDay {
Text(lastMessageTime, style: .time )
.font(.footnote)
.foregroundColor(.secondary)
} else if lastMessageDay == (currentDay - 1) {
Text("Yesterday")
.font(.footnote)
.foregroundColor(.secondary)
} else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) {
Text(lastMessageTime.formattedDate(format: dateFormatString))
.font(.footnote)
.foregroundColor(.secondary)
} else if lastMessageDay < (currentDay - 1800) {
Text(lastMessageTime.formattedDate(format: dateFormatString))
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
makeNavigationLink(myInfo: myInfo, channel: channel)
.frame(height: 62)
.contextMenu {
if channel.allPrivateMessages.count > 0 {
HStack(alignment: .top) {
Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")")
//.font(.system(size: 16))
.font(.footnote)
.foregroundColor(.secondary)
Button(role: .destructive) {
isPresentingDeleteChannelMessagesConfirm = true
channelSelection = channel
} label: {
Label("Delete Messages", systemImage: "trash")
}
}
}
}
.frame(height: 62)
.contextMenu {
if channel.allPrivateMessages.count > 0 {
Button(role: .destructive) {
isPresentingDeleteChannelMessagesConfirm = true
channelSelection = channel
Button {
channel.mute = !channel.mute
do {
let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!)
if adminMessageId > 0 {
context.refresh(channel, mergeChanges: true)
}
try context.save()
} catch {
context.rollback()
Logger.data.error("💥 Save Channel Mute Error")
}
} label: {
Label("Delete Messages", systemImage: "trash")
Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash")
}
}
Button {
channel.mute = !channel.mute
do {
let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
context.refresh(channel, mergeChanges: true)
}
try context.save()
} catch {
context.rollback()
print("💥 Save Channel Mute Error")
.confirmationDialog(
"This conversation will be deleted.",
isPresented: $isPresentingDeleteChannelMessagesConfirm,
titleVisibility: .visible
) {
Button(role: .destructive) {
deleteChannelMessages(channel: channelSelection!, context: context)
context.refresh(myInfo, mergeChanges: true)
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
channelSelection = nil
} label: {
Text("delete")
}
} label: {
Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash")
}
}
.confirmationDialog(
"This conversation will be deleted.",
isPresented: $isPresentingDeleteChannelMessagesConfirm,
titleVisibility: .visible
) {
Button(role: .destructive) {
deleteChannelMessages(channel: channelSelection!, context: context)
context.refresh(node!.myInfo!, mergeChanges: true)
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
channelSelection = nil
} label: {
Text("delete")
.onAppear {
if self.bleManager.context == nil {
self.bleManager.context = context
}
}
}
.onAppear {
if self.bleManager.context == nil {
self.bleManager.context = context
}
}
}
}
.padding([.top, .bottom])

View file

@ -7,6 +7,7 @@
import SwiftUI
import CoreData
import OSLog
struct ChannelMessageList: View {
@StateObject var appState = AppState.shared
@ -60,7 +61,7 @@ struct ChannelMessageList: View {
.foregroundColor(.gray)
.offset(y: 8)
}
HStack {
MessageText(
message: message,
@ -75,13 +76,13 @@ struct ChannelMessageList: View {
RetryButton(message: message, destination: .channel(channel))
}
}
TapbackResponses(message: message) {
appState.unreadChannelMessages = myInfo.unreadMessages
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
context.refresh(myInfo, mergeChanges: true)
}
HStack {
if currentUser && message.receivedACK {
// Ack Received
@ -114,12 +115,12 @@ struct ChannelMessageList: View {
message.read = true
do {
try context.save()
print("📖 Read message \(message.messageId) ")
Logger.data.info("📖 Read message \(message.messageId) ")
appState.unreadChannelMessages = myInfo.unreadMessages
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
context.refresh(myInfo, mergeChanges: true)
} catch {
print("Failed to read message \(message.messageId)")
Logger.data.error("Failed to read message \(message.messageId): \(error.localizedDescription)")
}
}
}
@ -142,7 +143,7 @@ struct ChannelMessageList: View {
}
})
}
TextMessageField(
destination: .channel(channel),
replyMessageId: $replyMessageId,

View file

@ -4,7 +4,7 @@ import CoreData
struct MessageContextMenuItems: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
let message: MessageEntity
let tapBackDestination: MessageDestination
let isCurrentUser: Bool
@ -47,7 +47,7 @@ struct MessageContextMenuItems: View {
Text("copy")
Image(systemName: "doc.on.doc")
}
Menu("message.details") {
VStack {
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))

View file

@ -1,4 +1,5 @@
import SwiftUI
import OSLog
struct MessageText: View {
static let linkBlue = Color(red: 0.4627, green: 0.8392, blue: 1) /* #76d6ff */
@ -10,7 +11,7 @@ struct MessageText: View {
static let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss:a")
@Environment(\.managedObjectContext) var context
let message: MessageEntity
let tapBackDestination: MessageDestination
let isCurrentUser: Bool
@ -71,7 +72,7 @@ struct MessageText: View {
do {
try context.save()
} catch {
print("Failed to delete message \(message.messageId)")
Logger.data.error("Failed to delete message \(message.messageId): \(error.localizedDescription)")
}
}
Button("Cancel", role: .cancel) {}

View file

@ -7,12 +7,13 @@
import SwiftUI
import CoreData
import OSLog
#if canImport(TipKit)
import TipKit
#endif
struct Messages: View {
@StateObject var appState = AppState.shared
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ -20,9 +21,9 @@ struct Messages: View {
@State var node: NodeInfoEntity?
@State private var userSelection: UserEntity? // Nothing selected by default.
@State private var channelSelection: ChannelEntity? // Nothing selected by default.
@State private var columnVisibility = NavigationSplitViewVisibility.all
enum MessagesSidebar {
case groupMessages
case directMessages
@ -67,21 +68,20 @@ struct Messages: View {
.navigationBarTitleDisplayMode(.large)
.navigationBarItems(leading: MeshtasticLogo())
.onChange(of: (appState.navigationPath)) { newPath in
if ((newPath?.hasPrefix("meshtastic://messages")) != nil) {
if (newPath?.hasPrefix("meshtastic://messages")) != nil {
if let urlComponent = URLComponents(string: newPath ?? "") {
let queryItems = urlComponent.queryItems
let messageId = queryItems?.first(where: { $0.name == "messageId" })?.value
let channel = queryItems?.first(where: { $0.name == "channel" })?.value
if channel == nil {
print("Channel not found")
}
else {
print("Channel \(channel)")
// selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") })
// AppState.shared.navigationPath = nil
if let channel {
Logger.services.info("Deep Link Channel \(channel)")
// selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") })
// AppState.shared.navigationPath = nil
} else {
Logger.services.info("Channel Deep Link not found")
}
}
}
@ -106,7 +106,7 @@ struct Messages: View {
}
}
}
} content: {
} detail: {

View file

@ -1,4 +1,5 @@
import SwiftUI
import OSLog
struct RetryButton: View {
@Environment(\.managedObjectContext) var context
@ -36,7 +37,7 @@ struct RetryButton: View {
do {
try context.save()
} catch {
print("Failed to delete message \(messageID)")
Logger.data.error("Failed to delete message \(messageID): \(error.localizedDescription)")
}
if !bleManager.sendMessage(
message: payload,
@ -46,7 +47,7 @@ struct RetryButton: View {
replyID: replyID
) {
// Best effort, unlikely since we already checked BLE state
print("Failed to resend message \(messageID)")
Logger.services.warning("Failed to resend message \(messageID)")
} else {
switch destination {
case .user:

View file

@ -1,10 +1,11 @@
import SwiftUI
import OSLog
struct TapbackResponses: View {
@Environment(\.managedObjectContext) var context
let message: MessageEntity
let onRead: () -> Void
let onRead: () -> Void
@ViewBuilder
var body: some View {
@ -30,10 +31,10 @@ struct TapbackResponses: View {
tapback.read = true
do {
try context.save()
print("📖 Read tapback \(tapback.messageId) ")
Logger.data.info("📖 Read tapback \(tapback.messageId) ")
onRead()
} catch {
print("Failed to read tapback \(tapback.messageId)")
Logger.data.error("Failed to read tapback \(tapback.messageId): \(error.localizedDescription)")
}
}
}

View file

@ -1,14 +1,15 @@
import SwiftUI
import OSLog
struct TextMessageField: View {
static let maxbytes = 228
@EnvironmentObject var bleManager: BLEManager
let destination: MessageDestination
@Binding var replyMessageId: Int64
@FocusState.Binding var isFocused: Bool
let onSubmit: () -> Void
@State private var typingMessage: String = ""
@State private var totalBytes = 0
@State private var sendPositionWithMessage = false
@ -25,7 +26,7 @@ struct TextMessageField: View {
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
}
#endif
HStack(alignment: .top) {
ZStack {
TextField("message", text: $typingMessage, axis: .vertical)
@ -80,13 +81,13 @@ struct TextMessageField: View {
}
.padding(.all, 15)
}
private func requestPosition() {
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
sendPositionWithMessage = true
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
}
private func sendMessage() {
let messageSent = bleManager.sendMessage(
message: typingMessage,
@ -107,7 +108,7 @@ struct TextMessageField: View {
wantResponse: destination.wantPositionResponse
)
if positionSent {
print("Location Sent")
Logger.mesh.info("Location Sent")
}
}
}
@ -121,7 +122,7 @@ private extension MessageDestination {
case .channel: return "has shared their position with you"
}
}
var positionDestNum: Int64 {
switch self {
case let .user(user): return user.num

View file

@ -3,7 +3,7 @@ import SwiftUI
struct TextMessageSize: View {
let maxbytes: Int
let totalBytes: Int
var body: some View {
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
.frame(width: 130)

View file

@ -7,12 +7,13 @@
import SwiftUI
import CoreData
import OSLog
#if canImport(TipKit)
import TipKit
#endif
struct UserList: View {
@StateObject var appState = AppState.shared
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ -26,7 +27,7 @@ struct UserList: View {
@State private var hopsAway: Int = -1
@State private var deviceRole: Int = -1
@State var isEditingFilters = false
@FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false),
NSSortDescriptor(key: "userNode.favorite", ascending: false),
@ -38,7 +39,7 @@ struct UserList: View {
@State var selectedUserNum: Int64?
@State private var userSelection: UserEntity? // Nothing selected by default.
@State private var isPresentingDeleteUserMessagesConfirm: Bool = false
var body: some View {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY")
@ -61,15 +62,15 @@ struct UserList: View {
.foregroundColor(.accentColor)
.brightness(0.2)
}
CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num))))
VStack(alignment: .leading){
HStack{
VStack(alignment: .leading) {
HStack {
Text(user.longName ?? "unknown".localized)
.font(.headline)
Spacer()
if (user.userNode?.favorite ?? false) {
if user.userNode?.favorite ?? false {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
@ -93,7 +94,7 @@ struct UserList: View {
}
}
}
if user.messageList.count > 0 {
HStack(alignment: .top) {
Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")")
@ -106,18 +107,18 @@ struct UserList: View {
.frame(height: 62)
.contextMenu {
Button {
if node != nil && !(user.userNode?.favorite ?? false) {
let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num))
if success {
user.userNode?.favorite = !(user.userNode?.favorite ?? true)
print("Favorited a node")
Logger.data.info("Favorited a node")
}
} else {
let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num))
if success {
user.userNode?.favorite = !(user.userNode?.favorite ?? true)
print("Favorited a node")
Logger.data.info("Un Favorited a node")
}
}
context.refresh(user, mergeChanges: true)
@ -125,7 +126,7 @@ struct UserList: View {
try context.save()
} catch {
context.rollback()
print("💥 Save Node Favorite Error")
Logger.data.error("Save Node Favorite Error")
}
} label: {
Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill")
@ -136,7 +137,7 @@ struct UserList: View {
try context.save()
} catch {
context.rollback()
print("💥 Save User Mute Error")
Logger.data.error("Save User Mute Error")
}
} label: {
Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash")
@ -206,7 +207,6 @@ struct UserList: View {
}
.onChange(of: selectedUserNum) { newUserNum in
userSelection = users.first(where: { $0.num == newUserNum })
print(userSelection)
}
.onAppear {
if self.bleManager.context == nil {
@ -238,9 +238,9 @@ struct UserList: View {
.scrollDismissesKeyboard(.immediately)
}
}
private func searchUserList() {
/// Case Insensitive Search Text Predicates
let searchPredicates = ["userId", "numString", "hwModel", "longName", "shortName"].map { property in
return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)
@ -269,7 +269,7 @@ struct UserList: View {
let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway))
predicates.append(hopsAwayPredicate)
}
/// Online
if isOnline {
let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
@ -283,22 +283,22 @@ struct UserList: View {
/// 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 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 deltaLatitude = d / r * 180 / .pi
let deltaLongitude = d / (r * cos(meanLatitidue)) * 180 / .pi
let minLatitude: Double = pointOfInterest.latitude - deltaLatitude
let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude
let minLongitude: Double = pointOfInterest.longitude - deltaLongitude
let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude
let distancePredicate = NSPredicate(format: "(SUBQUERY(userNode.positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude,minLatitude, maxLatitude)
let distancePredicate = NSPredicate(format: "(SUBQUERY(userNode.positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude, minLatitude, maxLatitude)
predicates.append(distancePredicate)
}
}
if predicates.count > 0 || !searchText.isEmpty {
if !searchText.isEmpty {
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)

View file

@ -7,6 +7,7 @@
import SwiftUI
import CoreData
import OSLog
struct UserMessageList: View {
@StateObject var appState = AppState.shared
@ -99,12 +100,12 @@ struct UserMessageList: View {
message.read = true
do {
try context.save()
print("📖 Read message \(message.messageId) ")
Logger.data.info("📖 Read message \(message.messageId) ")
appState.unreadDirectMessages = user.unreadMessages
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
} catch {
print("Failed to read message \(message.messageId)")
Logger.data.error("Failed to read message \(message.messageId): \(error.localizedDescription)")
}
}
}

View file

@ -7,6 +7,7 @@
import SwiftUI
import Charts
import OSLog
struct DetectionSensorLog: View {
@Environment(\.managedObjectContext) var context
@ -133,11 +134,12 @@ struct DetectionSensorLog: View {
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") \("detection.sensor.log".localized)"),
onCompletion: { result in
if case .success = result {
print("Detections metrics log download succeeded.")
switch result {
case .success:
self.isExporting = false
} else {
print("Detections log download failed: \(result).")
Logger.services.info("Detection Sensor metrics log download succeeded.")
case .failure(let error):
Logger.services.error("Detection Sensor log download failed: \(error.localizedDescription).")
}
}
)

View file

@ -6,6 +6,7 @@
//
import SwiftUI
import Charts
import OSLog
struct DeviceMetricsLog: View {
@ -19,7 +20,7 @@ struct DeviceMetricsLog: View {
@State private var batteryChartColor: Color = .blue
@State private var airtimeChartColor: Color = .orange
@State private var channelUtilizationChartColor: Color = .green
@ObservedObject var node: NodeInfoEntity
@ObservedObject var node: NodeInfoEntity
var body: some View {
VStack {
@ -188,9 +189,9 @@ struct DeviceMetricsLog: View {
) {
Button("device.metrics.delete", role: .destructive) {
if clearTelemetry(destNum: node.num, metricsType: 0, context: context) {
print("Cleared Device Metrics for \(node.num)")
Logger.data.notice("Cleared Device Metrics for \(node.num)")
} else {
print("Clear Device Metrics Log Failed")
Logger.data.error("Clear Device Metrics Log Failed")
}
}
}
@ -232,11 +233,12 @@ struct DeviceMetricsLog: View {
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") \("device.metrics.log".localized)"),
onCompletion: { result in
if case .success = result {
print("Device metrics log download succeeded.")
switch result {
case .success:
self.isExporting = false
} else {
print("Device metrics log download failed: \(result).")
Logger.services.info("Device metrics log download succeeded.")
case .failure(let error):
Logger.services.error("Device metrics log download failed: \(error.localizedDescription)")
}
}
)

View file

@ -6,6 +6,7 @@
//
import SwiftUI
import Charts
import OSLog
struct EnvironmentMetricsLog: View {
@ -113,7 +114,7 @@ struct EnvironmentMetricsLog: View {
GridItem(spacing: 0)
]
LazyVGrid(columns: columns, alignment: .leading, spacing: 1, pinnedViews: [.sectionHeaders]) {
GridRow {
Text("Temp")
.font(.caption)
@ -132,9 +133,9 @@ struct EnvironmentMetricsLog: View {
.fontWeight(.bold)
}
ForEach(environmentMetrics, id: \.self) { em in
GridRow {
Text(em.temperature.formattedTemperature())
.font(.caption)
Text("\(String(format: "%.0f", em.relativeHumidity))%")
@ -154,7 +155,7 @@ struct EnvironmentMetricsLog: View {
}
}
HStack {
Button(role: .destructive) {
isPresentingClearLogConfirm = true
} label: {
@ -172,7 +173,7 @@ struct EnvironmentMetricsLog: View {
) {
Button("Delete all environment metrics?", role: .destructive) {
if clearTelemetry(destNum: node.num, metricsType: 1, context: context) {
print("Clear Environment Metrics Log Failed")
Logger.services.error("Clear Environment Metrics Log Failed")
}
}
}
@ -188,7 +189,7 @@ struct EnvironmentMetricsLog: View {
.padding(.bottom)
.padding(.trailing)
}
} else {
if #available (iOS 17, *) {
ContentUnavailableView("No Environment Metrics", systemImage: "slash.circle")
@ -197,7 +198,7 @@ struct EnvironmentMetricsLog: View {
}
}
}
.navigationTitle("Environment Metrics Log")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
@ -215,11 +216,12 @@ struct EnvironmentMetricsLog: View {
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") Environment Metrics Log"),
onCompletion: { result in
if case .success = result {
print("Environment metrics log download succeeded.")
switch result {
case .success:
self.isExporting = false
} else {
print("Environment metrics log download failed: \(result).")
Logger.services.info("Environment metrics log download succeeded.")
case .failure(let error):
Logger.services.error("Environment metrics log download failed: \(error.localizedDescription)")
}
}
)

View file

@ -10,7 +10,7 @@ import MapKit
@available(iOS 17.0, macOS 14.0, *)
struct MeshMapContent: MapContent {
@StateObject var appState = AppState.shared
/// Parameters
@Binding var showUserLocation: Bool
@ -24,13 +24,13 @@ struct MeshMapContent: MapContent {
@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>
@ -39,26 +39,13 @@ struct MeshMapContent: MapContent {
@State private var scale: CGFloat = 0.5
@MapContentBuilder
var meshMap: some MapContent {
let loraNodes = positions.filter { $0.nodePosition?.viaMqtt ?? true == false }
let loraCoords = Array(loraNodes).compactMap({(position) -> CLLocationCoordinate2D in
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
})
/// Convex Hull
if showConvexHull {
if loraCoords.count > 0 {
let hull = loraCoords.getConvexHull()
MapPolygon(coordinates: hull)
.stroke(.blue, lineWidth: 3)
.foregroundStyle(.indigo.opacity(0.4))
}
}
/// Position Annotations
ForEach(Array(positions), id: \.id) { position in
var positionAnnotations: some MapContent {
ForEach(positions, id: \.id) { position in
/// Node color from node.num
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
let positionName = position.nodePosition?.user?.longName ?? "?"
/// Latest Position Anotations
Annotation(position.nodePosition?.user?.longName ?? "?", coordinate: position.coordinate) {
Annotation(positionName, coordinate: position.coordinate) {
LazyVStack {
ZStack {
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
@ -93,12 +80,12 @@ struct MeshMapContent: MapContent {
selectedPosition = (selectedPosition == position ? nil : position)
}
}
/// Node History and Route Lines for favorites
if position.nodePosition?.favorite ?? false {
if let nodePosition = position.nodePosition,
nodePosition.favorite,
let positions = nodePosition.positions,
let nodePositions = Array(positions) as? [PositionEntity] {
if showRouteLines {
let nodePositions = Array(position.nodePosition!.positions!) as! [PositionEntity]
let routeCoords = nodePositions.compactMap({(pos) -> CLLocationCoordinate2D in
return pos.nodeCoordinate ?? LocationHelper.DefaultLocation
})
@ -114,7 +101,7 @@ struct MeshMapContent: MapContent {
.stroke(gradient, style: dashed)
}
if showNodeHistory {
ForEach(Array(position.nodePosition!.positions!) as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
ForEach(nodePositions, id: \.self) { (mappin: PositionEntity) in
if mappin.latest == false && mappin.nodePosition?.favorite ?? false {
let pf = PositionFlags(rawValue: Int(mappin.nodePosition?.metadata?.positionFlags ?? 771))
let headingDegrees = Angle.degrees(Double(mappin.heading))
@ -129,11 +116,11 @@ struct MeshMapContent: MapContent {
.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)
.strokeBorder(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white, lineWidth: 2)
.frame(width: 12, height: 12)
}
}
@ -147,19 +134,24 @@ struct MeshMapContent: MapContent {
/// Reduced Precision Map Circles
if 10...19 ~= position.precisionBits {
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
let radius : CLLocationDistance = pp?.precisionMeters ?? 0
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)
.tag(position.nodePosition?.num ?? 0)
}
}
/// Routes
ForEach(Array(routes)) { route in
let routeLocations = Array(route.locations!) as! [LocationEntity]
let routeCoords = routeLocations.compactMap({(loc) -> CLLocationCoordinate2D in
}
}
@MapContentBuilder
var routeAnnotations: some MapContent {
ForEach(routes) { route in
if let routeLocations = route.locations, let locations = Array(routeLocations) as? [LocationEntity] {
let routeCoords = locations.compactMap {(loc) -> CLLocationCoordinate2D in
return loc.locationCoordinate ?? LocationHelper.DefaultLocation
})
}
Annotation("Start", coordinate: routeCoords.first ?? LocationHelper.DefaultLocation) {
ZStack {
Circle()
@ -184,18 +176,19 @@ struct MeshMapContent: MapContent {
)
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
}
@MapContentBuilder
var waypointAnnotations: some MapContent {
if waypoints.count > 0, showWaypoints, let waypoints = Array(waypoints) as? [WaypointEntity] {
ForEach(waypoints, 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
.onTapGesture(perform: { _ in
selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
})
}
@ -204,7 +197,27 @@ struct MeshMapContent: MapContent {
}
}
}
@MapContentBuilder
var meshMap: some MapContent {
let loraNodes = positions.filter { $0.nodePosition?.viaMqtt ?? true == false }
let loraCoords = Array(loraNodes).compactMap({(position) -> CLLocationCoordinate2D in
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
})
/// Convex Hull
if showConvexHull {
if loraCoords.count > 0 {
let hull = loraCoords.getConvexHull()
MapPolygon(coordinates: hull)
.stroke(.blue, lineWidth: 3)
.foregroundStyle(.indigo.opacity(0.4))
}
}
positionAnnotations
routeAnnotations
waypointAnnotations
}
@MapContentBuilder
var body: some MapContent {
meshMap

View file

@ -10,7 +10,7 @@ import CoreData
@available(iOS 17.0, macOS 14.0, *)
struct NodeMapContent: MapContent {
@ObservedObject var node: NodeInfoEntity
@State var showUserLocation: Bool = false
@State var positions: [PositionEntity] = []
@ -22,7 +22,7 @@ struct NodeMapContent: MapContent {
@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)
@ -33,26 +33,26 @@ struct NodeMapContent: MapContent {
@State var isEditingSettings = false
@State var selectedPosition: PositionEntity?
@State var isMeshMap = false
@MapContentBuilder
var nodeMap: some MapContent {
let positionArray = node.positions?.array as? [PositionEntity] ?? []
let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
})
/// Node Color from node.num
let nodeColor = UIColor(hex: UInt32(node.num))
/// Node Annotations
ForEach(node.positions?.array as? [PositionEntity] ?? [], id: \.id) { position in
let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771))
let headingDegrees = Angle.degrees(Double(position.heading))
/// Reduced Precision Map Circle
if position.latest && 10...19 ~= position.precisionBits {
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
let radius : CLLocationDistance = pp?.precisionMeters ?? 0
let radius: CLLocationDistance = pp?.precisionMeters ?? 0
if radius > 0.0 {
MapCircle(center: position.coordinate, radius: radius)
.foregroundStyle(Color(nodeColor).opacity(0.25))
@ -73,7 +73,7 @@ struct NodeMapContent: MapContent {
}
}
/// Route Lines
if showRouteLines {
if showRouteLines {
let gradient = LinearGradient(
colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)],
startPoint: .leading, endPoint: .trailing
@ -153,11 +153,11 @@ struct NodeMapContent: MapContent {
.clipShape(Circle())
.rotationEffect(headingDegrees)
.frame(width: 16, height: 16)
} else {
Circle()
.fill(Color(UIColor(hex: UInt32(position.nodePosition?.num ?? 0))))
.strokeBorder(Color(UIColor(hex: UInt32(position.nodePosition?.num ?? 0))).isLight() ? .black : .white ,lineWidth: 2)
.strokeBorder(Color(UIColor(hex: UInt32(position.nodePosition?.num ?? 0))).isLight() ? .black : .white, lineWidth: 2)
.frame(width: 12, height: 12)
}
}
@ -168,7 +168,7 @@ struct NodeMapContent: MapContent {
}
}
}
@MapContentBuilder
var body: some MapContent {
if node.positions?.count ?? 0 > 0 {

View file

@ -24,7 +24,7 @@ struct MapSettingsForm: View {
@Binding var meshMap: Bool
var body: some View {
NavigationStack {
Form {
Section(header: Text("Map Options")) {
@ -63,7 +63,7 @@ struct MapSettingsForm: View {
UserDefaults.enableMapWaypoints = !waypoints
}
}
Toggle(isOn: $nodeHistory) {
Label("Node History", systemImage: "building.columns.fill")
}
@ -75,7 +75,7 @@ struct MapSettingsForm: View {
Toggle(isOn: $routeLines) {
Label("Route Lines", systemImage: "road.lanes")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onTapGesture {
self.routeLines.toggle()
@ -123,6 +123,6 @@ Spacer()
}
.presentationDetents([.fraction(meshMap ? 0.55 : 0.45), .fraction(0.65)])
.presentationDragIndicator(.visible)
}
}

View file

@ -32,21 +32,21 @@ struct NodeMapSwiftUI: View {
@State var isShowingAltitude = false
@State var isEditingSettings = false
@State var isMeshMap = false
@State private var mapRegion = MKCoordinateRegion.init()
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
predicate: NSPredicate(
format: "expire == nil || expire >= %@", Date() as NSDate
), animation: .none)
private var waypoints: FetchedResults<WaypointEntity>
var body: some View {
var mostRecent = node.positions?.lastObject as? PositionEntity
if node.hasPositions {
ZStack {
MapReader { reader in
MapReader { _ in
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
NodeMapContent(node: node)
}

View file

@ -21,14 +21,26 @@ struct PositionAltitudeChart: View {
@Environment(\.dismiss) private var dismiss
@ObservedObject var node: NodeInfoEntity
@State private var lineWidth = 2.0
var body: some View {
var data: [PositionAltitude] {
let fiveYearsAgo = Calendar.current.date(byAdding: .year, value: -5, to: Date())
let nodePositions = Array(node.positions!) as! [PositionEntity]
let filteredPositions = nodePositions.filter({$0.time != nil && ($0.time ?? fiveYearsAgo!) > fiveYearsAgo!})
let data = filteredPositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) }
guard let nodePositions = node.positions,
let positions = Array(nodePositions) as? [PositionEntity]
else {
return []
}
let filteredPositions = positions.filter({$0.time != nil && ($0.time ?? fiveYearsAgo!) > fiveYearsAgo!})
return filteredPositions.map {
PositionAltitude(
time: $0.time ?? Date(),
altitude: Measurement(value: Double($0.altitude), unit: .meters)
)
}
}
var body: some View {
GroupBox(label: Label("Altitude", systemImage: "mountain.2")) {
Chart(data, id: \.time) {
LineMark(
x: .value("Time", $0.time),

View file

@ -25,7 +25,7 @@ struct PositionPopover: View {
VStack {
HStack {
ZStack {
if position.nodePosition?.isOnline ?? false {
Circle()
.fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5)))
@ -42,13 +42,13 @@ struct PositionPopover: View {
}
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 65)
}
Text(position.nodePosition?.user?.longName ?? "Unknown")
.font(.largeTitle)
}
Divider()
HStack (alignment: .center) {
VStack (alignment: .leading) {
HStack(alignment: .center) {
VStack(alignment: .leading) {
/// Time
Label {
Text("heard".localized + ":")
@ -131,9 +131,8 @@ struct PositionPopover: View {
}
.padding(.bottom, 5)
if position.nodePosition?.viaMqtt ?? false {
Label {
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
Text("MQTT")
} icon: {
Image(systemName: "network")
@ -146,7 +145,7 @@ struct PositionPopover: View {
if let lastLocation = locationsHandler.locationsArray.last {
/// Distance
if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
let metersAway = position.coordinate.distance(from:CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude))
let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude))
Label {
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
.foregroundColor(.primary)
@ -160,7 +159,7 @@ struct PositionPopover: View {
Spacer()
}
Spacer()
VStack (alignment: .center) {
VStack(alignment: .center) {
if position.nodePosition != nil {
if position.nodePosition?.favorite ?? false {
Image(systemName: "star.fill")
@ -214,7 +213,7 @@ struct PositionPopover: View {
#endif
}
}
.presentationDetents([.fraction(0.55), .fraction(0.65), .fraction(0.75)])
.presentationDetents([.fraction(0.65), .fraction(0.75), .fraction(0.85)])
.presentationDragIndicator(.visible)
}
}

View file

@ -1,4 +1,3 @@
//
// WaypointForm.swift
// Meshtastic
@ -9,9 +8,10 @@
import SwiftUI
import MapKit
import CoreLocation
import OSLog
struct WaypointForm: View {
@EnvironmentObject var bleManager: BLEManager
@Environment(\.dismiss) private var dismiss
@State var waypoint: WaypointEntity
@ -27,7 +27,7 @@ struct WaypointForm: View {
@State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours
@State private var locked: Bool = false
@State private var lockedTo: Int64 = 0
var body: some View {
NavigationStack {
if editMode {
@ -35,7 +35,7 @@ struct WaypointForm: View {
.font(.largeTitle)
Divider()
Form {
let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude , longitude: waypoint.coordinate.longitude ))
let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
Section(header: Text("Coordinate") ) {
HStack {
Text("Location: \(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
@ -91,14 +91,14 @@ struct WaypointForm: View {
.font(.title)
.focused($iconIsFocused)
.onChange(of: icon) { value in
// If you have anything other than emojis in your string make it empty
if !value.onlyEmojis() {
icon = ""
}
// If a second emoji is entered delete the first one
if value.count >= 1 {
if value.count > 1 {
let index = value.index(value.startIndex, offsetBy: 1)
icon = String(value[index])
@ -106,7 +106,7 @@ struct WaypointForm: View {
iconIsFocused = false
}
}
}
Toggle(isOn: $expires) {
Label("Expires", systemImage: "clock.badge.xmark")
@ -158,7 +158,7 @@ struct WaypointForm: View {
dismiss()
} else {
dismiss()
print("Send waypoint failed")
Logger.mesh.warning("Send waypoint failed")
}
} label: {
Label("Send", systemImage: "arrow.up")
@ -168,7 +168,7 @@ struct WaypointForm: View {
.controlSize(.regular)
.disabled(bleManager.connectedPeripheral == nil)
.padding(.bottom)
Button(role: .cancel) {
dismiss()
} label: {
@ -178,9 +178,9 @@ struct WaypointForm: View {
.buttonBorderShape(.capsule)
.controlSize(.regular)
.padding(.bottom)
if waypoint.id > 0 && bleManager.isConnected {
Menu {
Button("For me", action: {
bleManager.context!.delete(waypoint)
@ -211,7 +211,7 @@ struct WaypointForm: View {
}
newWaypoint.expire = UInt32(1)
if bleManager.sendWaypoint(waypoint: newWaypoint) {
bleManager.context!.delete(waypoint)
do {
try bleManager.context!.save()
@ -221,7 +221,7 @@ struct WaypointForm: View {
dismiss()
} else {
dismiss()
print("Send waypoint failed")
Logger.mesh.warning("Send waypoint failed")
}
})
}
@ -237,7 +237,7 @@ struct WaypointForm: View {
}
} else {
VStack {
HStack {
HStack {
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 65)
Spacer()
Text(waypoint.name ?? "?")
@ -258,7 +258,7 @@ struct WaypointForm: View {
}
}
Divider()
VStack (alignment: .leading) {
VStack(alignment: .leading) {
// Description
if (waypoint.longDescription ?? "").count > 0 {
Label {

View file

@ -7,6 +7,7 @@ import SwiftUI
import WeatherKit
import MapKit
import CoreLocation
import OSLog
struct NodeDetail: View {
@ -22,7 +23,7 @@ struct NodeDetail: View {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
NavigationStack {
GeometryReader { bounds in
GeometryReader { _ in
VStack {
ScrollView {
NodeInfoItem(node: node)
@ -58,7 +59,7 @@ struct NodeDetail: View {
Button {
let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context)
if adminMessageId > 0 {
print("Sent node metadata request from node details")
Logger.mesh.info("Sent node metadata request from node details")
}
} label: {
Image(systemName: "arrow.clockwise")
@ -78,12 +79,12 @@ struct NodeDetail: View {
Image(systemName: "flipphone")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Device Metrics Log")
.font(.title3)
}
.disabled(!node.hasDeviceMetrics)
Divider()
NavigationLink {
if #available (iOS 17, macOS 14, *) {
@ -91,12 +92,12 @@ struct NodeDetail: View {
} else {
NodeMapMapkit(node: node)
}
} label: {
Image(systemName: "map")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Node Map")
.font(.title3)
}
@ -108,7 +109,7 @@ struct NodeDetail: View {
Image(systemName: "mappin.and.ellipse")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Position Log")
.font(.title3)
}
@ -120,7 +121,7 @@ struct NodeDetail: View {
Image(systemName: "cloud.sun.rain")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Environment Metrics Log")
.font(.title3)
}
@ -133,7 +134,7 @@ struct NodeDetail: View {
Image(systemName: "signpost.right.and.left")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Trace Route Log")
.font(.title3)
}
@ -146,7 +147,7 @@ struct NodeDetail: View {
Image(systemName: "sensor")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Detection Sensor Log")
.font(.title3)
}
@ -159,7 +160,7 @@ struct NodeDetail: View {
Image(systemName: "figure.walk.motion")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("paxcounter.log")
.font(.title3)
}
@ -186,7 +187,7 @@ struct NodeDetail: View {
) {
Button("Shutdown Node?", role: .destructive) {
if !bleManager.sendShutdown(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) {
print("Shutdown Failed")
Logger.mesh.warning("Shutdown Failed")
}
}
}
@ -206,7 +207,7 @@ struct NodeDetail: View {
) {
Button("reboot.node", role: .destructive) {
if !bleManager.sendReboot(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) {
print("Reboot Failed")
Logger.mesh.warning("Reboot Failed")
}
}
}

View file

@ -14,9 +14,9 @@ struct NodeInfoItem: View {
@ObservedObject var node: NodeInfoEntity
var body: some View {
Divider()
HStack {
VStack(alignment: .center) {

View file

@ -15,9 +15,9 @@ struct NodeInfoItem: View {
var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast
var body: some View {
Divider()
HStack {
VStack(alignment: .center) {
@ -25,15 +25,26 @@ struct NodeInfoItem: View {
}
if node.user != nil {
Divider()
VStack {
Image(node.user!.hwModel ?? "unset".localized)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 75, height: 75)
.cornerRadius(5)
Text(String(node.user!.hwModel ?? "unset".localized))
.font(.caption2)
.frame(maxWidth: 125)
VStack(alignment: .center) {
if node.user?.hwModel != "UNSET" {
Image(node.user!.hwModel ?? "unset".localized)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 75, height: 75)
.cornerRadius(5)
Text(String(node.user!.hwModel ?? "unset".localized))
.font(.caption2)
.frame(maxWidth: 100)
} else {
Image(systemName: "person.crop.circle.badge.questionmark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 65, height: 65)
.cornerRadius(5)
Text(String("incomplete".localized))
.font(.caption)
.frame(maxWidth: 80)
}
}
}
if node.snr != 0 && !node.viaMqtt {

View file

@ -20,14 +20,14 @@ struct NodeListFilter: View {
@Binding var maximumDistance: Double
@Binding var hopsAway: Int
@Binding var deviceRole: Int
var body: some View {
NavigationStack {
Form {
Section(header: Text(filterTitle)) {
Toggle(isOn: $viaLora) {
Label {
Text("Via Lora")
} icon: {
@ -37,7 +37,7 @@ struct NodeListFilter: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $viaMqtt) {
Label {
Text("Via Mqtt")
} icon: {
@ -46,9 +46,9 @@ struct NodeListFilter: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.listRowSeparator(.visible)
Toggle(isOn: $isOnline) {
Label {
Text("Online")
} icon: {
@ -59,13 +59,13 @@ struct NodeListFilter: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.listRowSeparator(.visible)
Toggle(isOn: $isFavorite) {
Label {
Text("Favorites")
} icon: {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
.symbolRenderingMode(.hierarchical)
@ -73,9 +73,9 @@ struct NodeListFilter: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.listRowSeparator(.visible)
Toggle(isOn: $distanceFilter) {
Label {
Text("Distance")
} icon: {

View file

@ -9,14 +9,14 @@ import SwiftUI
import CoreLocation
struct NodeListItem: View {
@ObservedObject var node: NodeInfoEntity
var connected: Bool
var connectedNode: Int64
var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast
var body: some View {
NavigationLink(value: node) {
LazyVStack(alignment: .leading) {
HStack {
@ -69,7 +69,7 @@ struct NodeListItem: View {
Text("Role: \(role?.name ?? "unknown".localized)")
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
}
if node.isStoreForwardRouter {
HStack {
@ -82,14 +82,29 @@ struct NodeListItem: View {
.foregroundColor(.gray)
}
}
if node.positions?.count ?? 0 > 0 && connectedNode != node.num {
HStack {
let lastPostion = node.positions!.reversed()[0] as! PositionEntity
if #available(iOS 17.0, macOS 14.0, *) {
if let currentLocation = LocationsHandler.shared.locationsArray.last {
let myCoord = CLLocation(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude)
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude {
if let lastPostion = node.positions?.lastObject as? PositionEntity {
if #available(iOS 17.0, macOS 14.0, *) {
if let currentLocation = LocationsHandler.shared.locationsArray.last {
let myCoord = CLLocation(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude)
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude {
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
let metersAway = nodeCoord.distance(from: myCoord)
Image(systemName: "lines.measurement.horizontal")
.font(.callout)
.symbolRenderingMode(.hierarchical)
.frame(width: 30)
DistanceText(meters: metersAway)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
}
}
} else {
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
let metersAway = nodeCoord.distance(from: myCoord)
Image(systemName: "lines.measurement.horizontal")
@ -101,20 +116,6 @@ struct NodeListItem: View {
.foregroundColor(.gray)
}
}
} else {
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
let metersAway = nodeCoord.distance(from: myCoord)
Image(systemName: "lines.measurement.horizontal")
.font(.callout)
.symbolRenderingMode(.hierarchical)
.frame(width: 30)
DistanceText(meters: metersAway)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
}
}
}
}
@ -131,7 +132,7 @@ struct NodeListItem: View {
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
}
}
if node.viaMqtt && connectedNode != node.num {
Image(systemName: "dot.radiowaves.up.forward")
.symbolRenderingMode(.hierarchical)

View file

@ -9,15 +9,14 @@ import SwiftUI
import CoreData
import CoreLocation
import Foundation
import OSLog
#if canImport(MapKit)
import MapKit
#endif
@available(iOS 17.0, macOS 14.0, *)
struct MeshMap: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@StateObject var appState = AppState.shared
@ -29,7 +28,7 @@ struct MeshMap: View {
@AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .standard
// Map Configuration
@Namespace var mapScope
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .excludingAll, showsTraffic: false)
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .excludingAll, showsTraffic: false)
@State var position = MapCameraPosition.automatic
@State var isEditingSettings = false
@State var selectedPosition: PositionEntity?
@ -39,15 +38,14 @@ struct MeshMap: View {
@State var newWaypointCoord: CLLocationCoordinate2D?
@State var isMeshMap = true
var body: some View {
NavigationStack {
ZStack {
MapReader { reader in
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
MeshMapContent(showUserLocation: $showUserLocation, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, selectedWaypoint: $selectedWaypoint)
}
.mapScope(mapScope)
.mapStyle(mapStyle)
@ -60,7 +58,7 @@ struct MeshMap: View {
.mapControlVisibility(.automatic)
}
.controlSize(.regular)
.onTapGesture(count: 1, perform: { position in
.onTapGesture(count: 1, perform: { position in
newWaypointCoord = reader.convert(position, from: .local) ?? CLLocationCoordinate2D.init()
})
.gesture(
@ -68,27 +66,27 @@ struct MeshMap: View {
.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
}
case let .second(_, tapValue):
guard let point = tapValue?.location else {
Logger.services.error("Unable to retreive tap location from gesture data.")
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
guard let coordinate = reader.convert(point, from: .local) else {
Logger.services.error("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
Logger.services.debug("Long press occured at Lat: \(coordinate.latitude) Long: \(coordinate.longitude)")
default: return
}
})
}
@ -112,25 +110,25 @@ struct MeshMap: View {
//
// if ((newPath?.hasPrefix("meshtastic://open-waypoint")) != nil) {
// guard let url = URL(string: appState.navigationPath ?? "NONE") else {
// print("Invalid URL")
// logger.error("Invalid URL")
// return
// }
// guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
// print("Invalid URL Components")
// logger.error("Invalid URL Components")
// return
// }
// guard let action = components.host, action == "open-waypoint" else {
// print("Unknown waypoint URL action")
// logger.error("Unknown waypoint URL action")
// return
// }
// guard let waypointId = components.queryItems?.first(where: { $0.name == "id" })?.value else {
// print("Waypoint id not found")
// logger.error("Waypoint id not found")
// return
// }
// guard let waypoint = waypoints.first(where: { $0.id == Int64(waypointId) }) else {
// logger.error("Waypoint 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))
// }
@ -162,7 +160,7 @@ struct MeshMap: View {
}
.tint(Color(UIColor.secondarySystemBackground))
.foregroundColor(.accentColor)
.buttonStyle(.borderedProminent)
.buttonStyle(.borderedProminent)
}
.controlSize(.regular)
.padding(5)
@ -173,12 +171,9 @@ struct MeshMap: View {
})
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
if self.bleManager.context == nil {
self.bleManager.context = context
}
// let wayPointEntity = getWaypoint(id: Int64(deepLinkManager.waypointId) ?? -1, context: context)
//if wayPointEntity.id > 0 {
// if wayPointEntity.id > 0 {
// position = .camera(MapCamera(centerCoordinate: wayPointEntity.coordinate, distance: 1000, heading: 0, pitch: 60))
switch selectedMapLayer {
case .standard:

View file

@ -6,9 +6,10 @@
//
import SwiftUI
import CoreLocation
import OSLog
struct NodeList: View {
@StateObject var appState = AppState.shared
@State private var columnVisibility = NavigationSplitViewVisibility.all
@State private var selectedNode: NodeInfoEntity?
@ -26,25 +27,25 @@ struct NodeList: View {
@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?
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@FetchRequest(
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false),
NSSortDescriptor(key: "lastHeard", ascending: false),
NSSortDescriptor(key: "user.longName", ascending: true)],
animation: .default)
var nodes: FetchedResults<NodeInfoEntity>
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
// HStack {
// Button("Open Node") {
// UIApplication
@ -52,19 +53,19 @@ struct NodeList: View {
// .open(URL(string: "meshtastic://nodes?nodeNum=530606484")!)
// }
// }
let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
List(nodes, id: \.self, selection: $selectedNode) { node in
NodeListItem(node: node,
NodeListItem(node: node,
connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num,
connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1))
.contextMenu {
Button {
if !node.favorite {
let success = bleManager.setFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum))
if success {
node.favorite = !node.favorite
@ -72,9 +73,9 @@ struct NodeList: View {
try context.save()
} catch {
context.rollback()
print("💥 Save Node Favorite Error")
Logger.data.error("Save Node Favorite Error")
}
print("Favorited a node")
Logger.data.debug("Favorited a node")
}
} else {
let success = bleManager.removeFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum))
@ -84,12 +85,12 @@ struct NodeList: View {
try context.save()
} catch {
context.rollback()
print("💥 Save Node Favorite Error")
Logger.data.error("Save Node Favorite Error")
}
print("Favorited a node")
Logger.data.debug("Favorited a node")
}
}
} label: {
Label(node.favorite ? "Un-Favorite" : "Favorite", systemImage: node.favorite ? "star.slash.fill" : "star.fill")
}
@ -101,7 +102,7 @@ struct NodeList: View {
try context.save()
} catch {
context.rollback()
print("💥 Save User Mute Error")
Logger.data.error("Save User Mute Error")
}
} label: {
Label(node.user!.mute ? "Show Alerts" : "Hide Alerts", systemImage: node.user!.mute ? "bell" : "bell.slash")
@ -132,14 +133,14 @@ struct NodeList: View {
isPresentingTraceRouteSentAlert = false
}
}
} label: {
Label("Trace Route", systemImage: "signpost.right.and.left")
}
if node.isStoreForwardRouter {
Button {
let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!)
let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!)
if success {
isPresentingClientHistorySentAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
@ -152,7 +153,7 @@ struct NodeList: View {
}
}
if bleManager.connectedPeripheral != nil {
Button (role: .destructive) {
Button(role: .destructive) {
deleteNodeId = node.num
isPresentingDeleteNodeAlert = true
} label: {
@ -212,7 +213,7 @@ struct NodeList: View {
.disableAutocorrection(true)
.scrollDismissesKeyboard(.immediately)
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
.listStyle(.plain)
.confirmationDialog(
@ -226,7 +227,7 @@ struct NodeList: View {
if deleteNode != nil {
let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(connectedNodeNum))
if !success {
print("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized)")
Logger.data.error("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized)")
}
}
}
@ -251,7 +252,7 @@ struct NodeList: View {
.navigationBarItems(
trailing:
ZStack {
if (UIDevice.current.userInterfaceIdiom != .phone) {
if UIDevice.current.userInterfaceIdiom != .phone {
Button {
columnVisibility = .detailOnly
} label: {
@ -264,7 +265,7 @@ struct NodeList: View {
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true)
})
}
} else {
if #available (iOS 17, *) {
ContentUnavailableView("select.node", systemImage: "flipphone")
@ -278,7 +279,7 @@ struct NodeList: View {
} else {
Text("Select something to view")
}
}
.navigationSplitViewStyle(.balanced)
.onChange(of: searchText) { _ in
@ -315,19 +316,18 @@ struct NodeList: View {
searchNodeList()
}
.onChange(of: (appState.navigationPath)) { newPath in
guard let deepLink = newPath else {
return
}
if deepLink.hasPrefix("meshtastic://nodes") {
if let urlComponent = URLComponents(string: deepLink) {
let queryItems = urlComponent.queryItems
let nodeNum = queryItems?.first(where: { $0.name == "nodenum" })?.value
if nodeNum == nil {
print("nodeNum not found")
}
else {
Logger.data.debug("nodeNum not found")
} else {
selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") })
AppState.shared.navigationPath = nil
}
@ -371,7 +371,7 @@ struct NodeList: View {
let hopsAwayPredicate = NSPredicate(format: "hopsAway == %i", Int32(hopsAway))
predicates.append(hopsAwayPredicate)
}
/// Online
if isOnline {
let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
@ -385,22 +385,22 @@ struct NodeList: View {
/// 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 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 deltaLatitude = d / r * 180 / .pi
let deltaLongitude = d / (r * cos(meanLatitidue)) * 180 / .pi
let minLatitude: Double = pointOfInterest.latitude - deltaLatitude
let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude
let minLongitude: Double = pointOfInterest.longitude - deltaLongitude
let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude
let distancePredicate = NSPredicate(format: "(SUBQUERY(positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude,minLatitude, maxLatitude)
let distancePredicate = NSPredicate(format: "(SUBQUERY(positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude, minLatitude, maxLatitude)
predicates.append(distancePredicate)
}
}
if predicates.count > 0 || !searchText.isEmpty {
if !searchText.isEmpty {
let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates)

View file

@ -7,6 +7,7 @@
import SwiftUI
import Charts
import OSLog
struct PaxCounterLog: View {
@ -25,13 +26,13 @@ struct PaxCounterLog: View {
var body: some View {
VStack {
if node.hasPax {
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
let pax = node.pax?.reversed() as? [PaxCounterEntity] ?? []
let chartData = pax
.filter { $0.time != nil && $0.time! >= oneWeekAgo! }
.sorted { $0.time! < $1.time! }
let maxValue = (chartData.map{ $0.wifi }.max() ?? 0) + (chartData.map{ $0.ble }.max() ?? 0) + 5
let maxValue = (chartData.map { $0.wifi }.max() ?? 0) + (chartData.map { $0.ble }.max() ?? 0) + 5
if chartData.count > 0 {
GroupBox(label: Label("\(pax.count) Readings Total", systemImage: "chart.xyaxis.line")) {
@ -47,7 +48,7 @@ struct PaxCounterLog: View {
.accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)")
.foregroundStyle(paxChartColor)
.interpolationMethod(.cardinal)
Plot {
PointMark(
x: .value("x", point.time!),
@ -175,9 +176,9 @@ struct PaxCounterLog: View {
) {
Button("paxcounter.delete", role: .destructive) {
if clearPax(destNum: node.num, context: context) {
print("Cleared Pax Counter for \(node.num)")
Logger.services.info("Cleared Pax Counter for \(node.num)")
} else {
print("Clear Pax Counter Log Failed")
Logger.services.error("Clear Pax Counter Log Failed")
}
}
}
@ -219,11 +220,12 @@ struct PaxCounterLog: View {
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") \("paxcounter.log".localized)"),
onCompletion: { result in
if case .success = result {
print("PAX Counter log download succeeded.")
switch result {
case .success:
self.isExporting = false
} else {
print("PAX Counter log download failed: \(result).")
Logger.services.info("PAX Counter log download succeeded")
case .failure(let error):
Logger.services.error("PAX Counter log download failed: \(error.localizedDescription)")
}
}
)

View file

@ -5,6 +5,7 @@
// Copyright(c) Garth Vander Houwen 7/5/22.
//
import SwiftUI
import OSLog
struct PositionLog: View {
@Environment(\.managedObjectContext) var context
@ -20,7 +21,7 @@ struct PositionLog: View {
@ObservedObject var node: NodeInfoEntity
@State private var isPresentingClearLogConfirm = false
@State private var sortOrder = [KeyPathComparator(\PositionEntity.time)]
var body: some View {
VStack {
if node.hasPositions {
@ -62,7 +63,7 @@ struct PositionLog: View {
}
.width(min: 180)
}
} else {
ScrollView {
// Use a grid on iOS as a table only shows a single column
@ -91,19 +92,21 @@ struct PositionLog: View {
.font(.caption2)
.fontWeight(.bold)
}
ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in
let altitude = Measurement(value: Double(mappin.altitude), unit: UnitLength.meters)
GridRow {
Text(String(format: "%.5f", mappin.latitude ?? 0))
.font(.caption2)
Text(String(format: "%.5f", mappin.longitude ?? 0))
.font(.caption2)
Text(String(mappin.satsInView))
.font(.caption2)
Text(altitude.formatted())
.font(.caption2)
Text(mappin.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
.font(.caption2)
if let positions = node.positions?.reversed() as? [PositionEntity] {
ForEach(positions, id: \.self) { (mappin: PositionEntity) in
let altitude = Measurement(value: Double(mappin.altitude), unit: UnitLength.meters)
GridRow {
Text(String(format: "%.5f", mappin.latitude ?? 0))
.font(.caption2)
Text(String(format: "%.5f", mappin.longitude ?? 0))
.font(.caption2)
Text(String(mappin.satsInView))
.font(.caption2)
Text(altitude.formatted())
.font(.caption2)
Text(mappin.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
.font(.caption2)
}
}
}
}
@ -128,9 +131,9 @@ struct PositionLog: View {
) {
Button("Delete all positions?", role: .destructive) {
if clearPositions(destNum: node.num, context: context) {
print("Successfully Cleared Position Log")
Logger.services.info("Successfully Cleared Position Log")
} else {
print("Clear Position Log Failed")
Logger.services.error("Clear Position Log Failed")
}
}
}
@ -152,15 +155,16 @@ struct PositionLog: View {
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") Position Log"),
onCompletion: { result in
if case .success = result {
print("Position log download succeeded.")
switch result {
case .success:
Logger.services.info("Position log download succeeded.")
self.isExporting = false
} else {
print("Position log download failed: \(result).")
case .failure(let error):
Logger.services.error("Position log download failed: \(error.localizedDescription)")
}
}
)
} else {
if #available (iOS 17, *) {
ContentUnavailableView("No Positions", systemImage: "mappin.slash")

View file

@ -15,7 +15,7 @@ struct TraceRouteLog: View {
@ObservedObject var locationsHandler = LocationsHandler.shared
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State private var isPresentingClearLogConfirm: Bool = false
@State var isExporting = false
@State var exportString = ""
@ -23,16 +23,16 @@ struct TraceRouteLog: View {
@State private var selectedRoute: TraceRouteEntity?
// 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: .realistic, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .all, showsTraffic: true)
@State var position = MapCameraPosition.automatic
let distanceFormatter = MKDistanceFormatter()
var body: some View {
HStack (alignment: .top) {
HStack(alignment: .top) {
VStack {
VStack {
List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in
Label {
Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : "No Response")")
} icon: {
@ -63,12 +63,8 @@ struct TraceRouteLog: View {
}
.font(.title2)
}
let hopsArray = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? []
let lineCoords = hopsArray.compactMap({(hop) -> CLLocationCoordinate2D in
return hop.coordinate ?? LocationHelper.DefaultLocation
})
if selectedRoute?.response ?? false {
if selectedRoute?.response ?? false {
if selectedRoute?.hasPositions ?? false {
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) {
@ -82,9 +78,8 @@ struct TraceRouteLog: View {
.annotationTitles(.automatic)
// Direct Trace Route
if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 {
if selectedRoute?.node?.positions?.count ?? 0 > 0 {
let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity
var traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate]
if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity {
let traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate]
Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) {
ZStack {
Circle()
@ -101,19 +96,21 @@ struct TraceRouteLog: View {
.stroke(.blue, style: dashed)
}
} else if selectedRoute?.hops?.count ?? 0 == 0 {
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
VStack {
/// Distance
if selectedRoute?.node?.positions?.count ?? 0 > 0 && selectedRoute?.coordinate != nil {
let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity
if selectedRoute?.node?.positions?.count ?? 0 > 0,
selectedRoute?.coordinate != nil,
let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity {
let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude)
if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
let metersAway = selectedRoute?.coordinate?.distance(from:CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude))
let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude))
Label {
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))")
.foregroundColor(.primary)

Some files were not shown because too many files have changed in this diff Show more