mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #655 from meshtastic/2.3.10_Working_Changes
2.3.10 Working Changes
This commit is contained in:
commit
369623d234
144 changed files with 3616 additions and 2032 deletions
|
|
@ -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
|
||||
|
|
@ -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>";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
LastUpgradeVersion = "1540"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
LastUpgradeVersion = "1540"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 ?? "⚠️" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -39,4 +39,3 @@ extension LocationEntity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
19
Meshtastic/Helpers/Logger.swift
Normal file
19
Meshtastic/Helpers/Logger.swift
Normal 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")
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV 36.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV 37.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="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">
|
||||
|
|
|
|||
|
|
@ -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 && 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>
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/#")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
// }
|
||||
//}
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ struct NodeInfoItem: View {
|
|||
@ObservedObject var node: NodeInfoEntity
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
HStack {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue