diff --git a/.swiftlint.yml b/.swiftlint.yml index 9f3ff810..f5682e26 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -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 \ No newline at end of file diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index a96e3707..cb1f6722 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -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 = ""; }; 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = ""; }; 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = ""; }; 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; @@ -437,6 +439,8 @@ DDCDC6CC29481FCC004C1DDA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DDCDC6CE294821AD004C1DDA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConfig.swift; sourceTree = ""; }; + DDD28D362C0CCCD10063CFA3 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; + DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 37.xcdatamodel"; sourceTree = ""; }; DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = ""; }; DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClientProxyManager.swift; sourceTree = ""; }; DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = ""; }; @@ -943,6 +947,7 @@ DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, DDDB443C29F6592F00EE2349 /* NetworkManager.swift */, DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */, + 25183D452C0A6D97001E31D5 /* Logger.swift */, ); path = Helpers; sourceTree = ""; @@ -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 = ""; @@ -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 = ""; diff --git a/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme b/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme index cd0cb664..68ab4aa3 100644 --- a/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme +++ b/Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic.xcscheme @@ -1,6 +1,6 @@ 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 { switch self { case .excellent: diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index 1048846c..521dc937 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -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 { diff --git a/Meshtastic/Extensions/Bundle.swift b/Meshtastic/Extensions/Bundle.swift index b3bd85bf..84353a51 100644 --- a/Meshtastic/Extensions/Bundle.swift +++ b/Meshtastic/Extensions/Bundle.swift @@ -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 ?? "⚠️" } } diff --git a/Meshtastic/Extensions/CLLocationCoordinate2D.swift b/Meshtastic/Extensions/CLLocationCoordinate2D.swift index 58287add..2d681159 100644 --- a/Meshtastic/Extensions/CLLocationCoordinate2D.swift +++ b/Meshtastic/Extensions/CLLocationCoordinate2D.swift @@ -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) diff --git a/Meshtastic/Extensions/Color.swift b/Meshtastic/Extensions/Color.swift index 252eb361..7b7980f0 100644 --- a/Meshtastic/Extensions/Color.swift +++ b/Meshtastic/Extensions/Color.swift @@ -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) } } diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index e5a4d95a..2568943a 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -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 diff --git a/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift b/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift index 310fe626..5b35f620 100644 --- a/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift @@ -39,4 +39,3 @@ extension LocationEntity { } } } - diff --git a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift index 888df18e..80ba19c8 100644 --- a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift @@ -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 } } diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 2f42ad06..9258b424 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -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)" diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index bac4f840..0c634779 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -11,7 +11,7 @@ import MapKit import SwiftUI extension PositionEntity { - + static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = 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 diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index e4887c78..a8adeaaf 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -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)" diff --git a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift index 0c3d6854..b17ccecf 100644 --- a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift @@ -10,13 +10,13 @@ import MapKit import SwiftUI extension WaypointEntity { - + static func allWaypointssFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = 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) diff --git a/Meshtastic/Extensions/Date.swift b/Meshtastic/Extensions/Date.swift index 2cb46f53..7fc72b33 100644 --- a/Meshtastic/Extensions/Date.swift +++ b/Meshtastic/Extensions/Date.swift @@ -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 } } diff --git a/Meshtastic/Extensions/FileManager.swift b/Meshtastic/Extensions/FileManager.swift index 175034ed..5323b71b 100644 --- a/Meshtastic/Extensions/FileManager.swift +++ b/Meshtastic/Extensions/FileManager.swift @@ -5,6 +5,7 @@ // Copyright(c) Garth Vander Houwen 5/5/23. // import Foundation +import OSLog let allocatedSizeResourceKeys: Set = [ .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 diff --git a/Meshtastic/Extensions/Measurement.swift b/Meshtastic/Extensions/Measurement.swift index 9fb3eec0..ca867c09 100644 --- a/Meshtastic/Extensions/Measurement.swift +++ b/Meshtastic/Extensions/Measurement.swift @@ -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( diff --git a/Meshtastic/Extensions/UIColor.swift b/Meshtastic/Extensions/UIColor.swift index d9160015..558631de 100644 --- a/Meshtastic/Extensions/UIColor.swift +++ b/Meshtastic/Extensions/UIColor.swift @@ -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), diff --git a/Meshtastic/Extensions/Url.swift b/Meshtastic/Extensions/Url.swift index 20d3ca6e..6d594dd9 100644 --- a/Meshtastic/Extensions/Url.swift +++ b/Meshtastic/Extensions/Url.swift @@ -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 } diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index d4b7c098..b94a0776 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -29,7 +29,7 @@ struct UserDefault { 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 diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 729a6fa0..caab235d 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -4,21 +4,15 @@ import CoreBluetooth import SwiftUI import MapKit import CocoaMQTT +import OSLog // --------------------------------------------------------------------------------------- // Meshtastic BLE Device Manager // --------------------------------------------------------------------------------------- class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate, ObservableObject { - - private static var documentsFolder: URL { - do { - return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - } catch { - fatalError("Can't find documents directory.") - } - } + var context: NSManagedObjectContext? - + static let shared = BLEManager() private var centralManager: CBCentralManager! @Published var peripherals: [Peripheral] = [] @@ -52,9 +46,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let FROMRADIO_UUID = CBUUID(string: "0x2C55E69E-4993-11ED-B878-0242AC120002") let EOL_FROMRADIO_UUID = CBUUID(string: "0x8BA2BCC2-EE02-4A55-A531-C525C5E454D5") let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453") - - let meshLog = documentsFolder.appendingPathComponent("meshlog.txt") - + // MARK: init BLEManager override init() { self.lastConnectionError = "" @@ -64,24 +56,24 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate mqttManager.delegate = self // centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: restoreKey]) } - + // MARK: Scanning for BLE Devices // Scan for nearby BLE devices using the Meshtastic BLE service ID func startScanning() { if isSwitchedOn { centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) - print("✅ Scanning Started") + Logger.services.info("✅ Scanning Started") } } - + // Stop Scanning For BLE Devices func stopScanning() { if centralManager.isScanning { centralManager.stopScan() - print("🛑 Stopped Scanning") + Logger.services.info("🛑 Stopped Scanning") } } - + // MARK: BLE Connect functions /// The action after the timeout-timer has fired /// @@ -91,17 +83,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate @objc func timeoutTimerFired(timer: Timer) { guard let timerContext = timer.userInfo as? [String: String] else { return } let name: String = timerContext["name", default: "Unknown"] - + self.timeoutTimerCount += 1 self.lastConnectionError = "" - + if timeoutTimerCount == 10 { if connectedPeripheral != nil { self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) } connectedPeripheral = nil if self.timeoutTimer != nil { - + self.timeoutTimer!.invalidate() } self.isConnected = false @@ -111,10 +103,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate self.timeoutTimerCount = 0 self.startScanning() } else { - print("🚨 BLE Connecting 2 Second Timeout Timer Fired \(timeoutTimerCount) Time(s): \(name)") + Logger.services.info("🚨 BLE Connecting 2 Second Timeout Timer Fired \(self.timeoutTimerCount) Time(s): \(name)") } } - + // Connect to a specific peripheral func connectTo(peripheral: CBPeripheral) { stopScanning() @@ -124,10 +116,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate self.automaticallyReconnect = true } if connectedPeripheral != nil { - print("ℹ️ BLE Disconnecting from: \(connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")") + Logger.services.info("ℹ️ BLE Disconnecting from: \(self.connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")") disconnectPeripheral() } - + centralManager?.connect(peripheral) // Invalidate any existing timer if timeoutTimer != nil { @@ -138,12 +130,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let context = ["name": "\(peripheral.name ?? "Unknown")"] timeoutTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(timeoutTimerFired), userInfo: context, repeats: true) RunLoop.current.add(timeoutTimer!, forMode: .common) - print("ℹ️ BLE Connecting: \(peripheral.name ?? "Unknown")") + Logger.services.info("ℹ️ BLE Connecting: \(peripheral.name ?? "Unknown")") } - + // Disconnect Connected Peripheral func cancelPeripheralConnection() { - + if mqttProxyConnected { mqttManager.mqttClientProxy?.disconnect() } @@ -162,10 +154,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate stopScanning() startScanning() } - + // Disconnect Connected Peripheral func disconnectPeripheral(reconnect: Bool = true) { - + guard let connectedPeripheral = connectedPeripheral else { return } if mqttProxyConnected { mqttManager.mqttClientProxy?.disconnect() @@ -180,7 +172,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate stopScanning() startScanning() } - + // Called each time a peripheral is connected func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { isConnecting = false @@ -193,7 +185,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if timeoutTimer != nil { timeoutTimer!.invalidate() } - + // remove any connection errors self.lastConnectionError = "" // Map the peripheral to the connectedPeripheral ObservedObjects @@ -208,15 +200,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } // Discover Services peripheral.discoverServices([meshtasticServiceCBUUID]) - print("✅ BLE Connected: \(peripheral.name ?? "Unknown")") + Logger.services.info("✅ BLE Connected: \(peripheral.name ?? "Unknown")") } - + // Called when a Peripheral fails to connect func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { cancelPeripheralConnection() - print("🚫 BLE Failed to Connect: \(peripheral.name ?? "Unknown")") + Logger.services.error("🚫 BLE Failed to Connect: \(peripheral.name ?? "Unknown")") } - + // Disconnect Peripheral Event func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { self.connectedPeripheral = nil @@ -230,7 +222,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if errorCode == 6 { // CBError.Code.connectionTimeout The connection has timed out unexpectedly. // Happens when device is manually reset / powered off lastConnectionError = "🚨" + String.localizedStringWithFormat("ble.errorcode.6 %@".localized, e.localizedDescription) - print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") + Logger.services.error("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") } else if errorCode == 7 { // CBError.Code.peripheralDisconnected The specified device has disconnected from us. // Seems to be what is received when a tbeam sleeps, immediately recconnecting does not work. if UserDefaults.preferredPeripheralId == peripheral.identifier.uuidString { @@ -247,11 +239,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate manager.schedule() } lastConnectionError = "🚨 \(e.localizedDescription)" - print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") + Logger.services.error("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") } else if errorCode == 14 { // Peer removed pairing information // Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that lastConnectionError = "🚨 " + String.localizedStringWithFormat("ble.errorcode.14 %@".localized, e.localizedDescription) - print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(lastConnectionError)") + Logger.services.error("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(self.lastConnectionError)") } else { if UserDefaults.preferredPeripheralId == peripheral.identifier.uuidString { manager.notifications = [ @@ -267,60 +259,58 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate manager.schedule() } lastConnectionError = "🚨 \(e.localizedDescription)" - print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") + Logger.services.error("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") } } else { // Disconnected without error which indicates user intent to disconnect // Happens when swiping to disconnect - print("ℹ️ BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect") + Logger.services.info("ℹ️ BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect") } // Start a scan so the disconnected peripheral is moved to the peripherals[] if it is awake self.startScanning() } - + // MARK: Peripheral Services functions func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - if let e = error { - print("🚫 Discover Services error \(e)") + if let error { + Logger.services.error("🚫 Discover Services error \(error.localizedDescription)") } guard let services = peripheral.services else { return } - for service in services { - if service.uuid == meshtasticServiceCBUUID { - peripheral.discoverCharacteristics([TORADIO_UUID, FROMRADIO_UUID, FROMNUM_UUID], for: service) - print("✅ BLE Service for Meshtastic discovered by \(peripheral.name ?? "Unknown")") - } + for service in services where service.uuid == meshtasticServiceCBUUID { + peripheral.discoverCharacteristics([TORADIO_UUID, FROMRADIO_UUID, FROMNUM_UUID], for: service) + Logger.services.info("✅ BLE Service for Meshtastic discovered by \(peripheral.name ?? "Unknown")") } } - + // MARK: Discover Characteristics Event func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - - if let e = error { - print("🚫 BLE Discover Characteristics error for \(peripheral.name ?? "Unknown") \(e) disconnecting device") + + if let error { + Logger.services.error("🚫 BLE Discover Characteristics error for \(peripheral.name ?? "Unknown") \(error.localizedDescription) disconnecting device") // Try and stop crashes when this error occurs disconnectPeripheral() return } - + guard let characteristics = service.characteristics else { return } - + for characteristic in characteristics { switch characteristic.uuid { - + case TORADIO_UUID: - print("✅ BLE did discover TORADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") + Logger.services.info("✅ BLE did discover TORADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") TORADIO_characteristic = characteristic - + case FROMRADIO_UUID: - print("✅ BLE did discover FROMRADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") + Logger.services.info("✅ BLE did discover FROMRADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") FROMRADIO_characteristic = characteristic peripheral.readValue(for: FROMRADIO_characteristic) - + case FROMNUM_UUID: - print("✅ BLE did discover FROMNUM (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") + Logger.services.info("✅ BLE did discover FROMNUM (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") FROMNUM_characteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) - + default: break } @@ -332,22 +322,22 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate sendWantConfig() } } - - // MARK: MqttClientProxyManagerDelegate Methods + + // MARK: MqttClientProxyManagerDelegate Methods func onMqttConnected() { mqttProxyConnected = true mqttError = "" - print("📲 Mqtt Client Proxy onMqttConnected now subscribing to \(mqttManager.topic).") + Logger.services.info("📲 Mqtt Client Proxy onMqttConnected now subscribing to \(self.mqttManager.topic).") mqttManager.mqttClientProxy?.subscribe(mqttManager.topic) } - + func onMqttDisconnected() { mqttProxyConnected = false - print("MQTT Disconnected") + Logger.services.info("MQTT Disconnected") } - + func onMqttMessageReceived(message: CocoaMQTTMessage) { - + if message.topic.contains("/stat/") { return } @@ -355,28 +345,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate proxyMessage.topic = message.topic proxyMessage.data = Data(message.payload) proxyMessage.retained = message.retained - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.mqttClientProxyMessage = proxyMessage - let binaryData: Data = try! toRadio.serializedData() + guard let binaryData: Data = try? toRadio.serializedData() else { + return + } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - } } - + func onMqttError(message: String) { mqttProxyConnected = false mqttError = message - print("📲 Mqtt Client Proxy onMqttError: \(message)") + Logger.services.info("📲 Mqtt Client Proxy onMqttError: \(message)") } - + // MARK: Protobuf Methods func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32, context: NSManagedObjectContext) -> Int64 { - + guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return 0 } - + var adminPacket = AdminMessage() adminPacket.getDeviceMetadataRequest = true var meshPacket: MeshPacket = MeshPacket() @@ -387,22 +378,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() - dataMessage.portnum = PortNum.adminApp - dataMessage.wantResponse = true - meshPacket.decoded = dataMessage + if let serializedData: Data = try? adminPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.adminApp + dataMessage.wantResponse = true + meshPacket.decoded = dataMessage + } else { + return 0 + } + let messageDescription = "🛎️ Requested Device Metadata for node \(toUser.longName ?? "unknown".localized) by \(fromUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return Int64(meshPacket.id) } return 0 } - + func sendTraceRouteRequest(destNum: Int64, wantResponse: Bool) -> Bool { - + var success = false guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return success } - + let fromNodeNum = connectedPeripheral.num let routePacket = RouteDiscovery() var meshPacket = MeshPacket() @@ -411,20 +407,24 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = UInt32(fromNodeNum) meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! routePacket.serializedData() - dataMessage.portnum = PortNum.tracerouteApp - dataMessage.wantResponse = wantResponse - meshPacket.decoded = dataMessage - + if let serializedData: Data = try? routePacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.tracerouteApp + dataMessage.wantResponse = true + meshPacket.decoded = dataMessage + } else { + return false + } var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() - + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) success = true - + let traceRoute = TraceRouteEntity(context: context!) let nodes: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") nodes.predicate = NSPredicate(format: "num IN %@", [destNum, self.connectedPeripheral.num]) @@ -438,8 +438,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRoute.time = Date() traceRoute.node = receivingNode // Grab the most recent postion, within the last hour - if connectedNode?.positions?.count ?? 0 > 0 { - let mostRecent = connectedNode?.positions?.lastObject as! PositionEntity + if connectedNode?.positions?.count ?? 0 > 0, let mostRecent = connectedNode?.positions?.lastObject as? PositionEntity { if mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRoute.altitude = mostRecent.altitude traceRoute.latitudeI = mostRecent.latitudeI @@ -449,32 +448,32 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } do { try context!.save() - print("💾 Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "unknown".localized))") + Logger.data.info("💾 Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "unknown".localized))") } catch { context!.rollback() let nsError = error as NSError - print("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)") + Logger.data.error("Error Updating Core Data BluetoothConfigEntity: \(nsError)") } - + let logString = String.localizedStringWithFormat("mesh.log.traceroute.sent %@".localized, String(destNum)) MeshLogger.log("🪧 \(logString)") - + } catch { - + } } return success } - + func sendWantConfig() { guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return } - + if FROMRADIO_characteristic == nil { MeshLogger.log("🚨 \("firmware.version.unsupported".localized)") invalidVersion = true return } else { - + let nodeName = connectedPeripheral?.peripheral.name ?? "unknown".localized let logString = String.localizedStringWithFormat("mesh.log.wantconfig %@".localized, nodeName) MeshLogger.log("🛎️ \(logString)") @@ -482,77 +481,77 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var toRadio: ToRadio = ToRadio() configNonce += 1 toRadio.wantConfigID = configNonce - let binaryData: Data = try! toRadio.serializedData() + guard let binaryData: Data = try? toRadio.serializedData() else { + return + } connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) // Either Read the config complete value or from num notify value guard connectedPeripheral != nil else { return } connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) } } - + func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { - if let errorText = error?.localizedDescription { - print("🚫 didUpdateNotificationStateFor error: \(errorText)") - } + Logger.services.error("didUpdateNotificationStateFor error: \(error?.localizedDescription ?? "Unknown")") } - + // MARK: Data Read / Update Characteristic Event func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { - - if let e = error { - - print("🚫 didUpdateValueFor Characteristic error \(e)") - let errorCode = (e as NSError).code + + if let error { + + Logger.services.error("🚫 didUpdateValueFor Characteristic error \(error.localizedDescription)") + let errorCode = (error as NSError).code if errorCode == 5 || errorCode == 15 { // BLE PIN connection errors // 5 CBATTErrorDomain Code=5 "Authentication is insufficient." // 15 CBATTErrorDomain Code=15 "Encryption is insufficient." - lastConnectionError = "🚨" + String.localizedStringWithFormat("ble.errorcode.pin %@".localized, e.localizedDescription) - print("🚨 \(e.localizedDescription) Please try connecting again and check the PIN carefully.") + lastConnectionError = "🚨" + String.localizedStringWithFormat("ble.errorcode.pin %@".localized, error.localizedDescription) + Logger.services.error("\(error.localizedDescription) Please try connecting again and check the PIN carefully.") self.disconnectPeripheral(reconnect: false) } return } - + switch characteristic.uuid { - + case FROMRADIO_UUID: - + if characteristic.value == nil || characteristic.value!.isEmpty { return } var decodedInfo = FromRadio() - + do { decodedInfo = try FromRadio(serializedData: characteristic.value!) - + } catch { - print(characteristic.value!) + Logger.services.error("\(error.localizedDescription) \(characteristic.value!)") } - + // Publish mqttClientProxyMessages received on the from radio if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.mqttClientProxyMessage(decodedInfo.mqttClientProxyMessage) { - let message = CocoaMQTTMessage ( + let message = CocoaMQTTMessage( topic: decodedInfo.mqttClientProxyMessage.topic, - payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), + payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), retained: decodedInfo.mqttClientProxyMessage.retained ) mqttManager.mqttClientProxy?.publish(message) } - + switch decodedInfo.packet.decoded.portnum { - + // Handle Any local only packets we get over BLE case .unknownApp: var nowKnown = false guard let ctx = context else { return } - + // MyInfo from initial connection if decodedInfo.myInfo.isInitialized && decodedInfo.myInfo.myNodeNum > 0 { let myInfo = myInfoPacket(myInfo: decodedInfo.myInfo, peripheralId: self.connectedPeripheral.id, context: ctx) - + if myInfo != nil { UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0) connectedPeripheral.num = myInfo?.myNodeNum ?? 0 @@ -584,7 +583,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate localConfig(config: decodedInfo.config, context: ctx, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral.longName) } // Module Config - if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0{ + if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0 { nowKnown = true moduleConfig(config: decodedInfo.moduleConfig, context: ctx, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral.longName) if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { @@ -636,10 +635,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate MeshLogger.log("🕸️ MESH PACKET received for Reply App handling as a text message") textMessageAppPacket(packet: decodedInfo.packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) case .ipTunnelApp: - //MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + // MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED UNHANDLED") case .serialApp: - //MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + // MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED UNHANDLED") case .storeForwardApp: if wantStoreAndForwardPackets { @@ -650,29 +649,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .rangeTestApp: if wantRangeTestPackets { textMessageAppPacket(packet: decodedInfo.packet, wantRangeTestPackets: true, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) - } - else { + } else { MeshLogger.log("🕸️ MESH PACKET received for Range Test App Range testing is disabled.") } case .telemetryApp: if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } case .textMessageCompressedApp: - //MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + // MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED") case .zpsApp: // MeshLogger.log("🕸️ MESH PACKET received for Zero Positioning System App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for Zero Positioning System App UNHANDLED") case .privateApp: - //MeshLogger.log("🕸️ MESH PACKET received for Private App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + // MeshLogger.log("🕸️ MESH PACKET received for Private App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for Private App UNHANDLED UNHANDLED") case .atakForwarder: - //MeshLogger.log("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + // MeshLogger.log("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED UNHANDLED") case .simulatorApp: - //MeshLogger.log("🕸️ MESH PACKET received for Simulator App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + // MeshLogger.log("🕸️ MESH PACKET received for Simulator App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for Simulator App UNHANDLED UNHANDLED") case .audioApp: - //MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + // MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED UNHANDLED") case .tracerouteApp: if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { @@ -682,7 +680,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if routingMessage.route.count == 0 { let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(decodedInfo.packet.from)) MeshLogger.log("🪧 \(logString)") - + } else { var routeString = "You --> " var hopNodes: [TraceRouteHopEntity] = [] @@ -695,8 +693,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRouteHop.time = Date() if hopNode?.hasPositions ?? false { traceRoute?.hasPositions = true - let mostRecent = hopNode?.positions?.lastObject as! PositionEntity - if mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { + if let mostRecent = hopNode?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { traceRouteHop.altitude = mostRecent.altitude traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI @@ -721,11 +718,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRoute?.hops = NSOrderedSet(array: hopNodes) do { try context!.save() - print("💾 Saved Trace Route") + Logger.data.info("💾 Saved Trace Route") } catch { context!.rollback() let nsError = error as NSError - print("💥 Error Updating Core Data TraceRouteHOp: \(nsError)") + Logger.data.error("Error Updating Core Data TraceRouteHOp: \(nsError)") } let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString) MeshLogger.log("🪧 \(logString)") @@ -733,33 +730,32 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } case .neighborinfoApp: if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) { - MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED") - // MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED \(neighborInfo)") + // MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED") + MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED \(neighborInfo)") } case .paxcounterApp: paxCounterPacket(packet: decodedInfo.packet, context: context!) case .mapReportApp: - MeshLogger.log("🕸️ MESH PACKET received for Map Report App UNHANDLED\(try! decodedInfo.packet.jsonString())") + MeshLogger.log("🕸️ MESH PACKET received Map Report App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .UNRECOGNIZED: - MeshLogger.log("🕸️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())") + MeshLogger.log("🕸️ MESH PACKET received UNRECOGNIZED App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .max: - print("MAX PORT NUM OF 511") + Logger.services.info("MAX PORT NUM OF 511") case .atakPlugin: MeshLogger.log("🕸️ MESH PACKET received for ATAK Plugin App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") - } - + if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { invalidVersion = false lastConnectionError = "" isSubscribed = true - print("🤜 Want Config Complete. ID:\(decodedInfo.configCompleteID)") + Logger.mesh.info("🤜 Want Config Complete. ID:\(decodedInfo.configCompleteID)") peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) // Config conplete returns so we don't read the characteristic again - + /// MQTT Client Proxy and RangeTest and Store and Forward interest if connectedPeripheral.num > 0 { - + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(connectedPeripheral.num)) do { @@ -777,22 +773,22 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let appState = AppState.shared appState.unreadChannelMessages = fetchedNodeInfo[0].myInfo?.unreadMessages ?? 0 appState.unreadDirectMessages = fetchedNodeInfo[0].user?.unreadMessages ?? 0 - //appState.connectedNode = fetchedNodeInfo[0] + // appState.connectedNode = fetchedNodeInfo[0] UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages - + } if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].rangeTestConfig?.enabled == true { - wantRangeTestPackets = true; + wantRangeTestPackets = true } if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].storeForwardConfig?.enabled == true { - wantStoreAndForwardPackets = true; + wantStoreAndForwardPackets = true } - + } catch { - print("Failed to find a node info for the connected node") + Logger.data.error("Failed to find a node info for the connected node \(error.localizedDescription)") } } - + // MARK: Share Location Position Update Timer // Use context to pass the radio name with the timer // Use a RunLoop to prevent the timer from running on the main UI thread @@ -805,27 +801,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return } - + case FROMNUM_UUID: - print("🗞️ BLE (Notify) characteristic, value will be read next") + Logger.services.info("🗞️ BLE (Notify) characteristic, value will be read next") default: - print("🚨 Unhandled Characteristic UUID: \(characteristic.uuid)") + Logger.services.error("Unhandled Characteristic UUID: \(characteristic.uuid)") } if FROMRADIO_characteristic != nil { // Either Read the config complete value or from num notify value peripheral.readValue(for: FROMRADIO_characteristic) } } - + public func sendMessage(message: String, toUserNum: Int64, channel: Int32, isEmoji: Bool, replyID: Int64) -> Bool { var success = false - + // Return false if we are not properly connected to a device, handle retry logic in the view for now if connectedPeripheral == nil || connectedPeripheral!.peripheral.state != CBPeripheralState.connected { - + self.disconnectPeripheral() self.startScanning() - + // Try and connect to the preferredPeripherial first let preferredPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == UserDefaults.preferredPeripheralId as String }).first if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil { @@ -834,32 +830,32 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let nodeName = connectedPeripheral?.peripheral.name ?? "unknown".localized let logString = String.localizedStringWithFormat("mesh.log.textmessage.send.failed %@".localized, nodeName) MeshLogger.log("🚫 \(logString)") - + success = false } else if message.count < 1 { - + // Don't send an empty message - print("🚫 Don't Send an Empty Message") + Logger.mesh.info("🚫 Don't Send an Empty Message") success = false - + } else { - + let fromUserNum: Int64 = self.connectedPeripheral.num - + let messageUsers: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") messageUsers.predicate = NSPredicate(format: "num IN %@", [fromUserNum, Int64(toUserNum)]) - + do { - + guard let fetchedUsers = try context?.fetch(messageUsers) as? [UserEntity] else { return false } if fetchedUsers.isEmpty { - - print("🚫 Message Users Not Found, Fail") + + Logger.data.error("🚫 Message Users Not Found, Fail") success = false } else if fetchedUsers.count >= 1 { - + let newMessage = MessageEntity(context: context!) newMessage.messageId = Int64(UInt32.random(in: UInt32(UInt8.max).. 0 { @@ -904,36 +900,38 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded.replyID = UInt32(replyID) } meshPacket.wantAck = true - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) let logString = String.localizedStringWithFormat("mesh.log.textmessage.sent %@ %@ %@".localized, String(newMessage.messageId), String(fromUserNum), String(toUserNum)) - + MeshLogger.log("💬 \(logString)") do { try context!.save() - print("💾 Saved a new sent message from \(connectedPeripheral.num) to \(toUserNum)") + Logger.data.info("💾 Saved a new sent message from \(self.connectedPeripheral.num) to \(toUserNum)") success = true - + } catch { context!.rollback() let nsError = error as NSError - print("💥 Unresolved Core Data error in Send Message Function your database is corrupted running a node db reset should clean up the data. Error: \(nsError)") + Logger.data.error("Unresolved Core Data error in Send Message Function your database is corrupted running a node db reset should clean up the data. Error: \(nsError)") } } } - + } catch { - + } } return success } - + public func sendWaypoint(waypoint: Waypoint) -> Bool { if waypoint.latitudeI == 373346000 && waypoint.longitudeI == -1220090000 { return false @@ -947,18 +945,19 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var dataMessage = DataMessage() do { dataMessage.payload = try waypoint.serializedData() - } - catch { + } catch { // Could not serialiaze the payload return false } - + dataMessage.portnum = PortNum.waypointApp meshPacket.decoded = dataMessage var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } let logString = String.localizedStringWithFormat("mesh.log.waypoint.sent %@".localized, String(fromNodeNum)) MeshLogger.log("📍 \(logString)") if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { @@ -988,20 +987,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } do { try context!.save() - print("💾 Updated Waypoint from Waypoint App Packet From: \(fromNodeNum)") + Logger.data.info("💾 Updated Waypoint from Waypoint App Packet From: \(fromNodeNum)") } catch { context!.rollback() let nsError = error as NSError - print("💥 Error Saving NodeInfoEntity from WAYPOINT_APP \(nsError)") + Logger.data.error("Error Saving NodeInfoEntity from WAYPOINT_APP \(nsError)") } } return success } - + public func getPositionFromPhoneGPS(destNum: Int64) -> Position? { var positionPacket = Position() if #available(iOS 17.0, macOS 14.0, *) { - + guard let lastLocation = LocationsHandler.shared.locationsArray.last else { return nil } @@ -1014,7 +1013,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate positionPacket.satsInView = UInt32(LocationsHandler.satsInView) let currentSpeed = lastLocation.speed - if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { + if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { positionPacket.groundSpeed = UInt32(currentSpeed) } let currentHeading = lastLocation.course @@ -1023,7 +1022,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } else { - + positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date() @@ -1032,7 +1031,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0) positionPacket.satsInView = UInt32(LocationHelper.satsInView) let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0 - if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { + if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { positionPacket.groundSpeed = UInt32(currentSpeed) } let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0 @@ -1042,7 +1041,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return positionPacket } - + public func setFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool { var adminPacket = AdminMessage() guard let positionPacket = getPositionFromPhoneGPS(destNum: fromUser.num) else { @@ -1057,16 +1056,21 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.wantAck = true meshPacket.channel = UInt32(channel) var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() - dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage + if let serializedData: Data = try? adminPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.adminApp + meshPacket.decoded = dataMessage + } else { + return false + } let messageDescription = "🚀 Sent Set Fixed Postion Admin Message to: \(fromUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: fromUser) { return true } return false } - + public func removeFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.removeFixedPosition = true @@ -1078,42 +1082,52 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.wantAck = true meshPacket.channel = UInt32(channel) var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() - dataMessage.portnum = PortNum.adminApp - meshPacket.decoded = dataMessage + if let serializedData: Data = try? adminPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.adminApp + meshPacket.decoded = dataMessage + } else { + return false + } let messageDescription = "🚀 Sent Remove Fixed Position Admin Message to: \(fromUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: fromUser) { return true } return false } - + public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool { var success = false let fromNodeNum = connectedPeripheral.num guard let positionPacket = getPositionFromPhoneGPS(destNum: destNum) else { return false } - + var meshPacket = MeshPacket() meshPacket.to = UInt32(destNum) meshPacket.channel = UInt32(channel) meshPacket.from = UInt32(fromNodeNum) var dataMessage = DataMessage() - dataMessage.payload = try! positionPacket.serializedData() - dataMessage.portnum = PortNum.positionApp - dataMessage.wantResponse = wantResponse - meshPacket.decoded = dataMessage - + if let serializedData: Data = try? positionPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.positionApp + dataMessage.wantResponse = wantResponse + meshPacket.decoded = dataMessage + } else { + return false + } + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) success = true let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum)) - print("📍 \(logString)") + Logger.services.debug("📍 \(logString)") } return success } @@ -1122,11 +1136,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if connectedPeripheral != nil { // Send a position out to the mesh if "share location with the mesh" is enabled in settings if UserDefaults.provideLocation { - let _ = sendPosition(channel: 0, destNum: connectedPeripheral.num, wantResponse: false) + _ = sendPosition(channel: 0, destNum: connectedPeripheral.num, wantResponse: false) } } } - + public func sendShutdown(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.shutdownSeconds = 5 @@ -1138,16 +1152,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.wantAck = true meshPacket.channel = UInt32(adminIndex) var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() - dataMessage.portnum = PortNum.adminApp - meshPacket.decoded = dataMessage + if let serializedData: Data = try? adminPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.adminApp + meshPacket.decoded = dataMessage + } else { + return false + } let messageDescription = "🚀 Sent Shutdown Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func sendReboot(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootSeconds = 5 @@ -1159,16 +1177,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.wantAck = true meshPacket.channel = UInt32(adminIndex) var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() - dataMessage.portnum = PortNum.adminApp - meshPacket.decoded = dataMessage + if let serializedData: Data = try? adminPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.adminApp + meshPacket.decoded = dataMessage + } else { + return false + } let messageDescription = "🚀 Sent Reboot Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func sendRebootOta(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootOtaSeconds = 5 @@ -1180,16 +1202,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.wantAck = true meshPacket.channel = UInt32(adminIndex) var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() - dataMessage.portnum = PortNum.adminApp - meshPacket.decoded = dataMessage + if let serializedData: Data = try? adminPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.adminApp + meshPacket.decoded = dataMessage + } else { + return false + } let messageDescription = "🚀 Sent Reboot OTA Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func sendEnterDfuMode(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.enterDfuModeRequest = true @@ -1201,9 +1227,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.wantAck = true meshPacket.channel = UInt32(0) var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() - dataMessage.portnum = PortNum.adminApp - meshPacket.decoded = dataMessage + if let serializedData: Data = try? adminPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.adminApp + meshPacket.decoded = dataMessage + } else { + return false + } automaticallyReconnect = false let messageDescription = "🚀 Sent enter DFU mode Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { @@ -1211,7 +1241,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return false } - + public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.factoryReset = 5 @@ -1222,17 +1252,21 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() - dataMessage.portnum = PortNum.adminApp - meshPacket.decoded = dataMessage - + if let serializedData: Data = try? adminPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.adminApp + meshPacket.decoded = dataMessage + } else { + return false + } + let messageDescription = "🚀 Sent Factory Reset Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func sendNodeDBReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.nodedbReset = 5 @@ -1243,9 +1277,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp - + meshPacket.decoded = dataMessage let messageDescription = "🚀 Sent NodeDB Reset Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { @@ -1253,7 +1290,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return false } - + public func connectToPreferredPeripheral() -> Bool { var success = false // Return false if we are not properly connected to a device, handle retry logic in the view for now @@ -1271,9 +1308,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return success } - + public func getChannel(channel: Channel, fromUser: UserEntity, toUser: UserEntity) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.getChannelRequest = UInt32(channel.index + 1) var meshPacket: MeshPacket = MeshPacket() @@ -1282,11 +1319,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setChannel = channel var meshPacket: MeshPacket = MeshPacket() @@ -1303,28 +1343,31 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { if isConnected { - + var i: Int32 = 0 var myInfo: MyInfoEntity // Before we get started delete the existing channels from the myNodeInfo if !addChannels { tryClearExistingChannels() } - + let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { do { @@ -1352,10 +1395,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } } catch { - print("Failed to find a node MyInfo to save these channels to") + Logger.data.error("Failed to find a node MyInfo to save these channels to: \(error.localizedDescription)") } } - + var chan = Channel() if i == 0 { chan.role = Channel.Role.primary @@ -1376,13 +1419,18 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.wantAck = true meshPacket.channel = 0 var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse) let logString = String.localizedStringWithFormat("mesh.log.channel.sent %@ %d".localized, String(connectedPeripheral.num), chan.index) @@ -1400,24 +1448,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.wantAck = true meshPacket.channel = 0 var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse) let logString = String.localizedStringWithFormat("mesh.log.lora.config.sent %@".localized, String(connectedPeripheral.num)) MeshLogger.log("📻 \(logString)") } - + if self.connectedPeripheral != nil { self.sendWantConfig() return true } - + } catch { return false } @@ -1425,7 +1478,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return false } - + public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config @@ -1437,16 +1490,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() - dataMessage.portnum = PortNum.adminApp - meshPacket.decoded = dataMessage + if let serializedData: Data = try? adminPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.adminApp + meshPacket.decoded = dataMessage + } else { + return 0 + } let messageDescription = "🛟 Saved User Config for \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return Int64(meshPacket.id) } return 0 } - + public func removeNode(node: NodeInfoEntity, connectedNodeNum: Int64) -> Bool { var adminPacket = AdminMessage() adminPacket.removeByNodenum = UInt32(node.num) @@ -1456,15 +1513,21 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() - dataMessage.portnum = PortNum.adminApp - meshPacket.decoded = dataMessage + if let serializedData: Data = try? adminPacket.serializedData() { + dataMessage.payload = serializedData + dataMessage.portnum = PortNum.adminApp + meshPacket.decoded = dataMessage + } else { + return false + } var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() - - if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected{ + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } + + if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { do { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) context!.delete(node.user!) @@ -1474,12 +1537,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } catch { context!.rollback() let nsError = error as NSError - print("💥 Error deleting node from core data: \(nsError)") + Logger.data.error("Error deleting node from core data: \(nsError)") } } return false } - + public func setFavoriteNode(node: NodeInfoEntity, connectedNodeNum: Int64) -> Bool { var adminPacket = AdminMessage() adminPacket.setFavoriteNode = UInt32(node.num) @@ -1489,21 +1552,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() - - if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected{ + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } + + if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) return true } return false } - + public func removeFavoriteNode(node: NodeInfoEntity, connectedNodeNum: Int64) -> Bool { var adminPacket = AdminMessage() adminPacket.removeFavoriteNode = UInt32(node.num) @@ -1513,21 +1581,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() - - if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected{ + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } + + if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) return true } return false } - + public func saveLicensedUser(ham: HamParameters, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setHamMode = ham @@ -1539,7 +1612,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved Ham Parameters for \(toUser.longName ?? "unknown".localized)" @@ -1559,24 +1635,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved Bluetooth Config for \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveDeviceConfig(config: Config.DeviceConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.device = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1585,7 +1664,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved Device Config for \(toUser.longName ?? "unknown".localized)" @@ -1595,7 +1677,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return 0 } - + public func saveDisplayConfig(config: Config.DisplayConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.display = config @@ -1609,7 +1691,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved Display Config for \(toUser.longName ?? "unknown".localized)" @@ -1619,9 +1704,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } return 0 } - + public func saveLoRaConfig(config: Config.LoRaConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.lora = config var meshPacket: MeshPacket = MeshPacket() @@ -1632,24 +1717,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func savePositionConfig(config: Config.PositionConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.position = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1658,26 +1746,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp - + meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Position Config for \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertPositionConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func savePowerConfig(config: Config.PowerConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.power = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1686,26 +1777,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp - + meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Power Config for \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertPowerConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveNetworkConfig(config: Config.NetworkConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.network = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1714,26 +1808,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp - + meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Network Config for \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertNetworkConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveAmbientLightingModuleConfig(config: ModuleConfig.AmbientLightingConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.ambientLighting = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1742,25 +1839,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Ambient Lighting Module Config for \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveCannedMessageModuleConfig(config: ModuleConfig.CannedMessageConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.cannedMessage = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1769,25 +1869,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Canned Message Module Config for \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveCannedMessageModuleMessages(messages: String, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setCannedMessageModuleMessages = messages - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1796,26 +1899,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return 0 + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Canned Message Module Messages for \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { - + return Int64(meshPacket.id) } - + return 0 } - + public func saveDetectionSensorModuleConfig(config: ModuleConfig.DetectionSensorConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.detectionSensor = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.externalNotification = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1849,12 +1958,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.paxcounter = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1875,26 +1987,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setRingtoneMessage = ringtone - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.mqtt = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.rangeTest = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.serial = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.storeForward = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.telemetry = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { - + var adminPacket = AdminMessage() adminPacket.getChannelRequest = channelIndex - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { - + var adminPacket = AdminMessage() adminPacket.getCannedMessageModuleMessagesRequest = true - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(destNum) meshPacket.from = UInt32(connectedPeripheral.num) @@ -2090,35 +2226,40 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true meshPacket.decoded.wantResponse = wantResponse - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = wantResponse - + meshPacket.decoded = dataMessage - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - - let binaryData: Data = try! toRadio.serializedData() - + + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } + if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) let logString = String.localizedStringWithFormat("mesh.log.cannedmessages.messages.get %@".localized, String(connectedPeripheral.num)) MeshLogger.log("🥫 \(logString)") return true } - + return false } - + public func requestBluetoothConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.bluetoothConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2126,27 +2267,30 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Bluetooth Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestDeviceConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.deviceConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2154,27 +2298,30 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Device Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestDisplayConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.displayConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2182,27 +2329,30 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Display Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestLoRaConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.loraConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2210,29 +2360,32 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested LoRa Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { - + return true } - + return false } - + public func requestNetworkConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.networkConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2240,26 +2393,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Network Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestPositionConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.positionConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2267,26 +2423,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Position Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestPowerConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.powerConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2294,26 +2453,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Power Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestAmbientLightingConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.ambientlightingConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2321,26 +2483,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Ambient Lighting Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestCannedMessagesModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.cannedmsgConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2348,26 +2513,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Canned Messages Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestExternalNotificationModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.extnotifConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2375,26 +2543,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested External Notificaiton Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestPaxCounterModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.paxcounterConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2402,26 +2573,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested PAX Counter Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestRtttlConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getRingtoneRequest = true - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2429,26 +2603,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested RTTTL Ringtone Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestRangeTestModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.rangetestConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2456,26 +2633,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Range Test Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestMqttModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.mqttConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2483,26 +2663,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested MQTT Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestDetectionSensorModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.detectionsensorConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2510,27 +2693,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Detection Sensor Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - - + public func requestSerialModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.serialConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2538,26 +2723,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Serial Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestStoreAndForwardModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.storeforwardConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2565,26 +2753,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Store and Forward Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestTelemetryModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.telemetryConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2592,30 +2783,35 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() - dataMessage.payload = try! adminPacket.serializedData() + guard let adminData: Data = try? adminPacket.serializedData() else { + return false + } + dataMessage.payload = adminData dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Telemetry Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + // Send an admin message to a radio, save a message to core data for logging private func sendAdminMessageToRadio(meshPacket: MeshPacket, adminDescription: String, fromUser: UserEntity, toUser: UserEntity) -> Bool { - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() - - if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected{ + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } + + if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { let newMessage = MessageEntity(context: context!) newMessage.messageId = Int64(meshPacket.id) newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) @@ -2624,24 +2820,23 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate newMessage.adminDescription = adminDescription newMessage.fromUser = fromUser newMessage.toUser = toUser - - + do { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) try context!.save() - print(adminDescription) + Logger.mesh.debug("\(adminDescription)") return true } catch { context!.rollback() let nsError = error as NSError - print("💥 Error inserting new core data MessageEntity: \(nsError)") + Logger.data.error("Error inserting new core data MessageEntity: \(nsError)") } } return false } - + public func requestStoreAndForwardClientHistory(fromUser: UserEntity, toUser: UserEntity) -> Bool { - + /// send a request for ClientHistory with a time period matching the heartbeat var sfPacket = StoreAndForward() sfPacket.rr = StoreAndForward.RequestResponse.clientHistory @@ -2654,23 +2849,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! sfPacket.serializedData() + guard let sfData: Data = try? sfPacket.serializedData() else { + return false + } + dataMessage.payload = sfData dataMessage.portnum = PortNum.storeForwardApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - let binaryData: Data = try! toRadio.serializedData() + guard let binaryData: Data = try? toRadio.serializedData() else { + return false + } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - print("📮 Sent a request for a Store & Forward Client History to \(toUser.num) for the last \(120) minutes.") + Logger.mesh.debug("📮 Sent a request for a Store & Forward Client History to \(toUser.num) for the last \(120) minutes.") return true } return false } - + func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) { // Handle each of the store and forward request / response messages @@ -2682,8 +2882,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .routerHeartbeat: /// When we get a router heartbeat we know there is a store and forward node on the network /// Check if it is the primary S&F Router and save the timestamp of the last heartbeat so that we can show the request message history menu item on node long press if the router has been seen recently - if (storeAndForwardMessage.heartbeat.secondary == 0) { - + if storeAndForwardMessage.heartbeat.secondary == 0 { + guard let routerNode = getNodeInfo(id: Int64(packet.from), context: context) else { return } @@ -2698,12 +2898,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate newConfig.lastHeartbeat = Date() routerNode.storeForwardConfig = newConfig } - + do { try context.save() } catch { context.rollback() - print("💥 Save Store and Forward Router Error") + Logger.data.error("Save Store and Forward Router Error") } } MeshLogger.log("💓 Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from)") @@ -2725,12 +2925,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate newConfig.lastRequest = Int32(storeAndForwardMessage.history.lastRequest) routerNode.storeForwardConfig = newConfig } - + do { try context.save() } catch { context.rollback() - print("💥 Save Store and Forward Router Error") + Logger.data.error("Save Store and Forward Router Error") } MeshLogger.log("📜 Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from)") case .routerStats: @@ -2758,12 +2958,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } } - + public func tryClearExistingChannels() { // Before we get started delete the existing channels from the myNodeInfo let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) - + do { let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as? [MyInfoEntity] ?? [] if fetchedMyInfo.count == 1 { @@ -2773,30 +2973,30 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate do { try context!.save() } catch { - print("Failed to clear existing channels from local app database") + Logger.data.error("Failed to clear existing channels from local app database: \(error.localizedDescription)") } } } catch { - print("Failed to find a node MyInfo to save these channels to") + Logger.data.error("Failed to find a node MyInfo to save these channels to: \(error.localizedDescription)") } } } // MARK: - CB Central Manager implmentation extension BLEManager: CBCentralManagerDelegate { - + // MARK: Bluetooth enabled/disabled func centralManagerDidUpdateState(_ central: CBCentralManager) { if central.state == CBManagerState.poweredOn { - print("BLE powered on") + Logger.services.debug("🔌 BLE powered on") isSwitchedOn = true startScanning() } else { isSwitchedOn = false } - + var status = "" - + switch central.state { case .poweredOff: status = "BLE is powered off" @@ -2813,20 +3013,20 @@ extension BLEManager: CBCentralManagerDelegate { default: status = "default" } - print("BLEManager status: \(status)") + Logger.services.debug("📜 BLEManager status: \(status)") } - + // Called each time a peripheral is discovered func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { - + if self.automaticallyReconnect && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" { self.connectTo(peripheral: peripheral) - print("ℹ️ BLE Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown")") + Logger.services.info("BLE Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown")") } let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "?", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral) let index = peripherals.map { $0.peripheral }.firstIndex(of: peripheral) - + if let peripheralIndex = index { peripherals[peripheralIndex] = device } else { diff --git a/Meshtastic/Helpers/EmojiOnlyTextField.swift b/Meshtastic/Helpers/EmojiOnlyTextField.swift index 04736020..0982ab33 100644 --- a/Meshtastic/Helpers/EmojiOnlyTextField.swift +++ b/Meshtastic/Helpers/EmojiOnlyTextField.swift @@ -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 } diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index 5ae3df98..baee82de 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -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)") } } } diff --git a/Meshtastic/Helpers/LocationHelper.swift b/Meshtastic/Helpers/LocationHelper.swift index 8d0100c0..270e6d2c 100644 --- a/Meshtastic/Helpers/LocationHelper.swift +++ b/Meshtastic/Helpers/LocationHelper.swift @@ -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)") } } diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 4d80e99f..32fc88d9 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -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 { diff --git a/Meshtastic/Helpers/Logger.swift b/Meshtastic/Helpers/Logger.swift new file mode 100644 index 00000000..c94a6003 --- /dev/null +++ b/Meshtastic/Helpers/Logger.swift @@ -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") +} diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index e3250e12..89e9bc16 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -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() } diff --git a/Meshtastic/Helpers/MeshLogger.swift b/Meshtastic/Helpers/MeshLogger.swift index f8699376..accebd02 100644 --- a/Meshtastic/Helpers/MeshLogger.swift +++ b/Meshtastic/Helpers/MeshLogger.swift @@ -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)") } } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 78121fb0..3250334b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -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 = 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.") } } diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index 4de5f67e..a2e363d0 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -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") } } diff --git a/Meshtastic/Helpers/NetworkManager.swift b/Meshtastic/Helpers/NetworkManager.swift index 8e24cf49..39a66b1d 100644 --- a/Meshtastic/Helpers/NetworkManager.swift +++ b/Meshtastic/Helpers/NetworkManager.swift @@ -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() diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 9ac5587b..63ed7101 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 36.xcdatamodel + MeshtasticDataModelV 37.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents index b9eddf6d..337f230f 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -229,6 +229,7 @@ + @@ -275,9 +276,12 @@ + + + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 37.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 37.xcdatamodel/contents new file mode 100644 index 00000000..aa6d6a5c --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 37.xcdatamodel/contents @@ -0,0 +1,464 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 2a8e37d1..da3383d2 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -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? } diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index c44a7f7d..1bfa73ed 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -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 diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 997fdbea..0da6a264 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -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)") } } } diff --git a/Meshtastic/Persistence/QueryCoreData.swift b/Meshtastic/Persistence/QueryCoreData.swift index 502aa61a..b68c36d6 100644 --- a/Meshtastic/Persistence/QueryCoreData.swift +++ b/Meshtastic/Persistence/QueryCoreData.swift @@ -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 = NSFetchRequest.init(entityName: "MessageEntity") let timeRange = Calendar.current.date(byAdding: .minute, value: time, to: Date()) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 46e27062..1ef04ff7 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -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(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)") } } diff --git a/Meshtastic/Protobufs/meshtastic/atak.pb.swift b/Meshtastic/Protobufs/meshtastic/atak.pb.swift index 77c35e7c..49229221 100644 --- a/Meshtastic/Protobufs/meshtastic/atak.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/atak.pb.swift @@ -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(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 } diff --git a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift index c778bf49..7ddb75dc 100644 --- a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift @@ -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"), ] } diff --git a/Meshtastic/Tips/MessagesTips.swift b/Meshtastic/Tips/MessagesTips.swift index c50af1b6..d78daa0e 100644 --- a/Meshtastic/Tips/MessagesTips.swift +++ b/Meshtastic/Tips/MessagesTips.swift @@ -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? { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index c8f422b7..63537d86 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -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.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.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.activities where activity.attributes.nodeNum == node?.num ?? 0 { + await activity.end(nil, dismissalPolicy: .immediate) } } } diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index da24c465..46f7f903 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -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 diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index 431eee25..952c9768 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -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 diff --git a/Meshtastic/Views/Helpers/BatteryLevelCompact.swift b/Meshtastic/Views/Helpers/BatteryLevelCompact.swift index e3fc1100..7bb2542c 100644 --- a/Meshtastic/Views/Helpers/BatteryLevelCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryLevelCompact.swift @@ -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) diff --git a/Meshtastic/Views/Helpers/CircleText.swift b/Meshtastic/Views/Helpers/CircleText.swift index 47b4ebbf..b7e4238d 100644 --- a/Meshtastic/Views/Helpers/CircleText.swift +++ b/Meshtastic/Views/Helpers/CircleText.swift @@ -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) diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index 2a96d526..c57a4c5d 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -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/#") diff --git a/Meshtastic/Views/Helpers/DateTimeText.swift b/Meshtastic/Views/Helpers/DateTimeText.swift index 6b61afd0..517772fe 100644 --- a/Meshtastic/Views/Helpers/DateTimeText.swift +++ b/Meshtastic/Views/Helpers/DateTimeText.swift @@ -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") diff --git a/Meshtastic/Views/Helpers/LastHeardText.swift b/Meshtastic/Views/Helpers/LastHeardText.swift index 02c23f9f..4d5deba4 100644 --- a/Meshtastic/Views/Helpers/LastHeardText.swift +++ b/Meshtastic/Views/Helpers/LastHeardText.swift @@ -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) diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift index 60feeacf..829fd837 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -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 { diff --git a/Meshtastic/Views/Helpers/MQTTIcon.swift b/Meshtastic/Views/Helpers/MQTTIcon.swift index 171f743e..4cd615be 100644 --- a/Meshtastic/Views/Helpers/MQTTIcon.swift +++ b/Meshtastic/Views/Helpers/MQTTIcon.swift @@ -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) diff --git a/Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift b/Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift index 160caae5..66307a91 100644 --- a/Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift +++ b/Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift @@ -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)) } } diff --git a/Meshtastic/Views/Helpers/Weather/IAQScale.swift b/Meshtastic/Views/Helpers/Weather/IAQScale.swift index 58de0bb8..e83d5e87 100644 --- a/Meshtastic/Views/Helpers/Weather/IAQScale.swift +++ b/Meshtastic/Views/Helpers/Weather/IAQScale.swift @@ -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) diff --git a/Meshtastic/Views/Helpers/Weather/IndoorAirQuality.swift b/Meshtastic/Views/Helpers/Weather/IndoorAirQuality.swift index 5e12aa9a..8e3773b9 100644 --- a/Meshtastic/Views/Helpers/Weather/IndoorAirQuality.swift +++ b/Meshtastic/Views/Helpers/Weather/IndoorAirQuality.swift @@ -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)) } } diff --git a/Meshtastic/Views/Helpers/Weather/NodeWeatherForecast.swift b/Meshtastic/Views/Helpers/Weather/NodeWeatherForecast.swift index 28dcfeb7..15198c50 100644 --- a/Meshtastic/Views/Helpers/Weather/NodeWeatherForecast.swift +++ b/Meshtastic/Views/Helpers/Weather/NodeWeatherForecast.swift @@ -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)") } } } diff --git a/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift b/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift index 4c9dc828..6f534baa 100644 --- a/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift +++ b/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift @@ -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) } diff --git a/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift b/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift index bc1e12e7..2d45b4f4 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift @@ -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)) // } -//} +// } diff --git a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift index 0a5edf70..5c7291a5 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift @@ -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) } diff --git a/Meshtastic/Views/MapKitMap/NodeMapMapkit.swift b/Meshtastic/Views/MapKitMap/NodeMapMapkit.swift index eeba0318..59c0e9cd 100644 --- a/Meshtastic/Views/MapKitMap/NodeMapMapkit.swift +++ b/Meshtastic/Views/MapKitMap/NodeMapMapkit.swift @@ -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 @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" } diff --git a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift index f32300ea..dce0df4a 100644 --- a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift +++ b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift @@ -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") } }) } diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index d8a4e96d..30738fa3 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -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]) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 7d7a7962..312a9ab3 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -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, diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 4fa31421..ab363f59 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -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)) diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 67500bc2..31fa2dec 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -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) {} diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index 482f538f..13ecbfe7 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -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: { diff --git a/Meshtastic/Views/Messages/RetryButton.swift b/Meshtastic/Views/Messages/RetryButton.swift index 1e2065bd..afda173a 100644 --- a/Meshtastic/Views/Messages/RetryButton.swift +++ b/Meshtastic/Views/Messages/RetryButton.swift @@ -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: diff --git a/Meshtastic/Views/Messages/TapbackResponses.swift b/Meshtastic/Views/Messages/TapbackResponses.swift index a7685697..fb61b44b 100644 --- a/Meshtastic/Views/Messages/TapbackResponses.swift +++ b/Meshtastic/Views/Messages/TapbackResponses.swift @@ -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)") } } } diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift index 49580f02..7ae0fb84 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -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 diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift index d7428110..2b7b1e5e 100644 --- a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -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) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 2d13d28f..a7bab2a2 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -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) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index bc9ef791..dd635c5f 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -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)") } } } diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index c59fd318..54395c99 100644 --- a/Meshtastic/Views/Nodes/DetectionSensorLog.swift +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -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).") } } ) diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index aa55a2b3..396b6098 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -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)") } } ) diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 53dd8280..86eebf71 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -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)") } } ) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 4270343c..2c943be9 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -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 - + @FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none) var waypoints: FetchedResults - + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], predicate: NSPredicate(format: "enabled == true", ""), animation: .none) private var routes: FetchedResults @@ -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 diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift index 78d20407..a0c2f63b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift @@ -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 { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift index 9a775911..2b703506 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift @@ -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) - + } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index bab5aa81..f8ee5165 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -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 - + 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) } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift index 4f69bd2a..2bbeaea1 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift @@ -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), diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index daa43ac9..221bf24f 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -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) } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 0869ff2b..31138855 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -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 { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index d93a6ab2..2ff960c5 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -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") } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift index 3c230e27..920f4335 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift @@ -14,9 +14,9 @@ struct NodeInfoItem: View { @ObservedObject var node: NodeInfoEntity var body: some View { - + Divider() - + HStack { VStack(alignment: .center) { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 1c9739b0..df40aaa5 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -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 { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 321054c1..2c49a7df 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -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: { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index e9c1ef65..172795e4 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -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) diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 35b67397..caa3f5d6 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -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: diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index a6e1368d..f31755ff 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -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 - + 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) diff --git a/Meshtastic/Views/Nodes/PaxCounterLog.swift b/Meshtastic/Views/Nodes/PaxCounterLog.swift index 567b4d4c..9042f091 100644 --- a/Meshtastic/Views/Nodes/PaxCounterLog.swift +++ b/Meshtastic/Views/Nodes/PaxCounterLog.swift @@ -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)") } } ) diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index f4979cb7..1c7ef170 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -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") diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index dde3db46..cd44cd30 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -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) diff --git a/Meshtastic/Views/Settings/About.swift b/Meshtastic/Views/Settings/About.swift index 0ab25c67..e7135408 100644 --- a/Meshtastic/Views/Settings/About.swift +++ b/Meshtastic/Views/Settings/About.swift @@ -22,7 +22,7 @@ struct AboutMeshtastic: View { } Section(header: Text("Apple Apps")) { - + if locale.region?.identifier ?? "US" == "US" { HStack { Image("SOLAR_NODE") @@ -48,7 +48,7 @@ struct AboutMeshtastic: View { } } .font(.title2) - + Text("Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild)) ") } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index ba5d46f9..7eb03933 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -3,6 +3,7 @@ import Combine import SwiftUI import SwiftProtobuf import MapKit +import OSLog struct AppSettings: View { @Environment(\.managedObjectContext) var context @@ -16,7 +17,7 @@ struct AppSettings: View { VStack { Form { Section(header: Text("App Settings")) { - Button("Open Settings", systemImage: "gear") { + Button("Open Settings", systemImage: "gear") { // Get the settings URL and open it if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) @@ -97,7 +98,7 @@ struct AppSettings: View { Button("Delete all map tiles?", role: .destructive) { tileManager.removeAll() totalDownloadedTileSize = tileManager.getAllDownloadedSize() - print("delete all tiles") + Logger.services.debug("delete all tiles") } } } diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 797ee714..689a009e 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -8,6 +8,7 @@ import SwiftUI import CoreData import MapKit +import OSLog #if canImport(TipKit) import TipKit #endif @@ -44,10 +45,10 @@ struct Channels: View { @State var positionsEnabled = true @State var supportedVersion = true @State var selectedChannel: ChannelEntity? - + /// Minimum Version for granular position configuration @State var minimumVersion = "2.2.24" - + @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false), @@ -90,7 +91,7 @@ struct Channels: View { positionPrecision = 32 preciseLocation = true positionsEnabled = true - + } else if !supportedVersion && channelRole == 2 { positionPrecision = 0 preciseLocation = false @@ -135,7 +136,7 @@ struct Channels: View { } } } - .sheet(item: $selectedChannel) { selection in + .sheet(item: $selectedChannel) { _ in #if targetEnvironment(macCatalyst) Text("channel") .font(.largeTitle) @@ -159,7 +160,7 @@ struct Channels: View { channel.settings.uplinkEnabled = uplink channel.settings.downlinkEnabled = downlink channel.settings.moduleSettings.positionPrecision = UInt32(positionPrecision) - + selectedChannel!.role = Int32(channelRole) selectedChannel!.index = channelIndex selectedChannel!.name = channelName @@ -180,34 +181,33 @@ struct Channels: View { context.refresh(selectedChannel!, mergeChanges: true) do { try context.save() - print("💾 Saved Channel: \(channel.settings.name)") + Logger.data.info("💾 Saved Channel: \(channel.settings.name)") } catch { context.rollback() let nsError = error as NSError - print("💥 Unresolved Core Data error in the channel editor. Error: \(nsError)") + Logger.data.error("Unresolved Core Data error in the channel editor. Error: \(nsError)") } } else { - guard let channelEntity = node?.myInfo?.channels?.first(where: { ($0 as! ChannelEntity).index == channelIndex }) else { + guard let channelEntities = node?.myInfo?.channels as? [ChannelEntity], + let channelEntity = channelEntities.first(where: { $0.index == channelIndex }) else { return } - - let objects = (channelEntity as! ChannelEntity).allPrivateMessages + + let objects = channelEntity.allPrivateMessages for object in objects { context.delete(object) } - for node in nodes { - if node.channel == (channelEntity as AnyObject).index { - context.delete(node) - } + for node in nodes where node.channel == channelEntity.index { + context.delete(node) } - context.delete(channelEntity as! ChannelEntity) + context.delete(channelEntity) do { try context.save() - print("💾 Deleted Channel: \(channel.settings.name)") + Logger.data.info("💾 Deleted Channel: \(channel.settings.name)") } catch { context.rollback() let nsError = error as NSError - print("💥 Unresolved Core Data error in the channel editor. Error: \(nsError)") + Logger.data.error("Unresolved Core Data error in the channel editor. Error: \(nsError)") } } @@ -259,7 +259,7 @@ struct Channels: View { uplink = false downlink = false hasChanges = true - + let newChannel = ChannelEntity(context: context) newChannel.id = channelIndex newChannel.index = channelIndex @@ -294,13 +294,11 @@ struct Channels: View { } func firstMissingChannelIndex(_ indexes: [Int]) -> Int { - var smallestIndex = 1 + let smallestIndex = 1 if indexes.isEmpty { return smallestIndex } if smallestIndex <= indexes.count { - for element in smallestIndex...indexes.count { - if !indexes.contains(element) { - return element - } + for element in smallestIndex...indexes.count where !indexes.contains(element) { + return element } } return indexes.count + 1 @@ -333,7 +331,7 @@ enum PositionPrecision: Int, CaseIterable, Identifiable { case twentyfour = 24 var id: Int { self.rawValue } - + var precisionMeters: Double { switch self { case .two: @@ -384,7 +382,7 @@ enum PositionPrecision: Int, CaseIterable, Identifiable { return 1.413763999910884 } } - + var description: String { let distanceFormatter = MKDistanceFormatter() return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters))) diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 560d1d70..476695bb 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -5,7 +5,6 @@ // Copyright(c) Garth Vander Houwen 3/17/24. // - import SwiftUI #if canImport(MapKit) import MapKit @@ -26,9 +25,9 @@ struct ChannelForm: View { @Binding var hasChanges: Bool @Binding var hasValidKey: Bool @Binding var supportedVersion: Bool - + var body: some View { - + NavigationStack { Form { Section(header: Text("channel details")) { @@ -98,15 +97,14 @@ struct ChannelForm: View { Color.clear : Color.red , lineWidth: 2.0) - + ) .onChange(of: channelKey, perform: { _ in - + let tempKey = Data(base64Encoded: channelKey) ?? Data() - if tempKey.count == channelKeySize || channelKeySize == -1{ + if tempKey.count == channelKeySize || channelKeySize == -1 { hasValidKey = true - } - else { + } else { hasValidKey = false } hasChanges = true @@ -131,9 +129,9 @@ struct ChannelForm: View { } } } - + Section(header: Text("position")) { - + VStack(alignment: .leading) { Toggle(isOn: $positionsEnabled) { Label(channelRole == 1 ? "Positions Enabled" : "Allow Position Requests", systemImage: positionsEnabled ? "mappin" : "mappin.slash") @@ -141,7 +139,7 @@ struct ChannelForm: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .disabled(!supportedVersion) } - + if positionsEnabled { VStack(alignment: .leading) { Toggle(isOn: $preciseLocation) { @@ -156,7 +154,7 @@ struct ChannelForm: View { } } } - + if !preciseLocation { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") @@ -236,8 +234,7 @@ struct ChannelForm: View { let tempKey = Data(base64Encoded: channelKey) ?? Data() if tempKey.count == channelKeySize || channelKeySize == -1 { hasValidKey = true - } - else { + } else { hasValidKey = false } } diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 6c1167a3..0b71dcb3 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -6,6 +6,7 @@ // import SwiftUI +import OSLog struct BluetoothConfig: View { @Environment(\.managedObjectContext) var context @@ -100,7 +101,7 @@ struct BluetoothConfig: View { setBluetoothValues() // Need to request a BluetoothConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.bluetoothConfig == nil { - print("empty bluetooth config") + Logger.mesh.info("empty bluetooth config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestBluetoothConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index c4256f58..020f9710 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -5,6 +5,7 @@ // Copyright (c) Garth Vander Houwen 6/13/22. // import SwiftUI +import OSLog struct DeviceConfig: View { @@ -46,7 +47,7 @@ struct DeviceConfig: View { .font(.callout) } .pickerStyle(DefaultPickerStyle()) - + VStack(alignment: .leading) { Picker("Rebroadcast Mode", selection: $rebroadcastMode ) { ForEach(RebroadcastModes.allCases) { rm in @@ -58,13 +59,13 @@ struct DeviceConfig: View { .font(.callout) } .pickerStyle(DefaultPickerStyle()) - + Toggle(isOn: $isManaged) { Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Picker("Node Info Broadcast Interval", selection: $nodeInfoBroadcastSecs ) { ForEach(UpdateIntervals.allCases) { ui in if ui.rawValue >= 3600 { @@ -75,13 +76,13 @@ struct DeviceConfig: View { .pickerStyle(DefaultPickerStyle()) } Section(header: Text("Hardware")) { - + Toggle(isOn: $doubleTapAsButtonPress) { Label("Double Tap as Button", systemImage: "hand.tap") Text("Treat double tap on supported accelerometers as a user button press.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $ledHeartbeatEnabled) { Label("LED Heartbeat", systemImage: "waveform.path.ecg") Text("Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable.") @@ -110,7 +111,7 @@ struct DeviceConfig: View { } }) .foregroundColor(.gray) - + } .keyboardType(.default) .disableAutocorrection(true) @@ -165,9 +166,9 @@ struct DeviceConfig: View { bleManager.disconnectPeripheral() clearCoreDataDatabase(context: context, includeRoutes: false) } - + } else { - print("NodeDB Reset Failed") + Logger.mesh.error("NodeDB Reset Failed") } } } @@ -191,7 +192,7 @@ struct DeviceConfig: View { clearCoreDataDatabase(context: context, includeRoutes: false) } } else { - print("Factory Reset Failed") + Logger.mesh.error("Factory Reset Failed") } } } @@ -241,7 +242,7 @@ struct DeviceConfig: View { setDeviceValues() // Need to request a LoRaConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.deviceConfig == nil { - print("empty device config") + Logger.mesh.info("empty device config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) if node != nil && connectedNode != nil && connectedNode?.user != nil { _ = bleManager.requestDeviceConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 829d644f..9a78ade8 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -6,6 +6,7 @@ // import SwiftUI +import OSLog struct DisplayConfig: View { @@ -37,7 +38,7 @@ struct DisplayConfig: View { Text(dm.description) } } - + Text("Override automatic OLED screen detection.") .foregroundColor(.gray) .font(.callout) @@ -54,13 +55,13 @@ struct DisplayConfig: View { Text("Requires that there be an accelerometer on your device.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $flipScreen) { Label("Flip Screen", systemImage: "pip.swap") Text("Flip screen vertically") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + VStack(alignment: .leading) { Picker("OLED Type", selection: $oledType ) { ForEach(OledTypes.allCases) { ot in @@ -85,20 +86,20 @@ struct DisplayConfig: View { .font(.callout) } .pickerStyle(DefaultPickerStyle()) - + VStack(alignment: .leading) { Picker("Carousel Interval", selection: $screenCarouselInterval ) { ForEach(ScreenCarouselIntervals.allCases) { sci in Text(sci.description) } } - + Text("Automatically toggles to the next page on the screen like a carousel, based the specified interval.") .foregroundColor(.gray) .font(.callout) } .pickerStyle(DefaultPickerStyle()) - + VStack(alignment: .leading) { Picker("GPS Format", selection: $gpsFormat ) { ForEach(GpsFormats.allCases) { lu in @@ -110,7 +111,7 @@ struct DisplayConfig: View { .font(.callout) } .pickerStyle(DefaultPickerStyle()) - + VStack(alignment: .leading) { Picker("Display Units", selection: $units ) { ForEach(Units.allCases) { un in @@ -164,7 +165,7 @@ struct DisplayConfig: View { // Need to request a LoRaConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.displayConfig == nil { - print("empty display config") + Logger.mesh.info("empty display config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestDisplayConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 3bfe9a41..f1a1df7b 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -7,6 +7,7 @@ import SwiftUI import CoreData +import OSLog struct LoRaConfig: View { @@ -69,7 +70,7 @@ struct LoRaConfig: View { .font(.callout) } .pickerStyle(DefaultPickerStyle()) - + Toggle(isOn: $usePreset) { Label("Use Preset", systemImage: "list.bullet.rectangle") } @@ -91,7 +92,7 @@ struct LoRaConfig: View { } } Section(header: Text("Advanced")) { - + Toggle(isOn: $ignoreMqtt) { Label("Ignore MQTT", systemImage: "server.rack") } @@ -140,7 +141,7 @@ struct LoRaConfig: View { .font(.callout) } .pickerStyle(DefaultPickerStyle()) - + VStack(alignment: .leading) { HStack { Text("Frequency Slot") @@ -163,12 +164,12 @@ struct LoRaConfig: View { .foregroundColor(.gray) .font(.callout) } - + Toggle(isOn: $rxBoostedGain) { Label("RX Boosted Gain", systemImage: "waveform.badge.plus") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + HStack { Label("Frequency Override", systemImage: "waveform.path.ecg") Spacer() @@ -177,7 +178,7 @@ struct LoRaConfig: View { .scrollDismissesKeyboard(.immediately) .focused($focusedField, equals: .frequencyOverride) } - + HStack { Image(systemName: "antenna.radiowaves.left.and.right") .foregroundColor(.accentColor) @@ -230,7 +231,7 @@ struct LoRaConfig: View { setLoRaValues() // Need to request a LoRaConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.loRaConfig == nil { - print("empty lora config") + Logger.mesh.info("empty lora config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestLoRaConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 9bb450ca..a6ad64c1 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -28,15 +28,15 @@ struct AmbientLightingConfig: View { VStack { Form { ConfigHeader(title: "Ambient Lighting", config: \.ambientLightingConfig, node: node, onAppear: setAmbientLightingConfigValue) - + Section(header: Text("options")) { - + Toggle(isOn: $ledState) { Label("LED State", systemImage: ledState ? "lightbulb.led.fill" : "lightbulb.led") Text("The state of the LED (on/off)") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + HStack { Image(systemName: "eyedropper") .foregroundColor(.accentColor) diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index dd5df775..51424da0 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -5,6 +5,7 @@ // Copyright (c) Garth Vander Houwen 6/22/22. // import SwiftUI +import OSLog struct CannedMessagesConfig: View { @Environment(\.managedObjectContext) var context @@ -39,21 +40,21 @@ struct CannedMessagesConfig: View { VStack { Form { ConfigHeader(title: "Canned messages", config: \.cannedMessageConfig, node: node, onAppear: setCannedMessagesValues) - + Section(header: Text("options")) { - + Toggle(isOn: $enabled) { Label("enabled", systemImage: "list.bullet.rectangle.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $sendBell) { Label("Send Bell", systemImage: "bell") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Picker("Configuration Presets", selection: $configPreset ) { ForEach(ConfigPresets.allCases) { cp in Text(cp.description) @@ -233,7 +234,7 @@ struct CannedMessagesConfig: View { setCannedMessagesValues() // Need to request a CannedMessagesModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.cannedMessageConfig == nil { - print("empty canned messages module config") + Logger.mesh.info("empty canned messages module config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index e78a9967..fbb2c5bd 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -5,6 +5,7 @@ // Copyright(c) Garth Vander Houwen 8/16/23. // import SwiftUI +import OSLog enum DetectionSensorRole: String, CaseIterable, Equatable, Decodable { case sensor @@ -44,9 +45,9 @@ struct DetectionSensorConfig: View { VStack { Form { ConfigHeader(title: "Detection Sensor", config: \.detectionSensorConfig, node: node, onAppear: setDetectionSensorValues) - + Section(header: Text("options")) { - + Toggle(isOn: $enabled) { Label("enabled", systemImage: "dot.radiowaves.right") Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.") @@ -90,7 +91,7 @@ struct DetectionSensorConfig: View { .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: name, perform: { _ in - + let totalBytes = name.utf8.count // Only mess with the value if it is too big if totalBytes > 20 { @@ -102,7 +103,7 @@ struct DetectionSensorConfig: View { Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"") .font(.callout) .foregroundStyle(.gray) - + Picker("GPIO Pin to monitor", selection: $monitorPin) { ForEach(0..<49) { if $0 == 0 { @@ -113,13 +114,13 @@ struct DetectionSensorConfig: View { } } .pickerStyle(DefaultPickerStyle()) - + Toggle(isOn: $detectionTriggeredHigh) { Label("Detection trigger High", systemImage: "dial.high") Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $usePullup) { Label("Uses pullup resistor", systemImage: "arrow.up.to.line") Text(" Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin") @@ -189,7 +190,7 @@ struct DetectionSensorConfig: View { setDetectionSensorValues() // Need to request a Detection Sensor Module Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil { - print("empty detection sensor module config") + Logger.mesh.info("empty detection sensor module config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 02af0792..b8b94bf0 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -5,6 +5,7 @@ // Copyright (c) Garth Vander Houwen 6/22/22. // import SwiftUI +import OSLog struct ExternalNotificationConfig: View { @@ -38,28 +39,28 @@ struct ExternalNotificationConfig: View { ConfigHeader(title: "External notification", config: \.externalNotificationConfig, node: node, onAppear: setExternalNotificationValues) Section(header: Text("options")) { - + Toggle(isOn: $enabled) { Label("enabled", systemImage: "megaphone") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $alertBell) { Label("Alert when receiving a bell", systemImage: "bell") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $alertMessage) { Label("Alert when receiving a message", systemImage: "message") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $usePWM) { Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill") Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $useI2SAsBuzzer) { Label("Use I2S As Buzzer", systemImage: "light.beacon.max.fill") Text("Enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer. T-Watch S3 and T-Deck for example have this capability.") @@ -76,7 +77,7 @@ struct ExternalNotificationConfig: View { Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Picker("Output pin GPIO", selection: $output) { ForEach(0..<49) { if $0 == 0 { @@ -88,7 +89,7 @@ struct ExternalNotificationConfig: View { } .pickerStyle(DefaultPickerStyle()) .listRowSeparator(.visible) - + Picker("GPIO Output Duration", selection: $outputMilliseconds ) { ForEach(OutputIntervals.allCases) { oi in Text(oi.description) @@ -100,9 +101,9 @@ struct ExternalNotificationConfig: View { .foregroundColor(.gray) .font(.callout) .listRowSeparator(.visible) - + Picker("Nag timeout", selection: $nagTimeout ) { - ForEach(OutputIntervals.allCases) { oi in + ForEach(NagIntervals.allCases) { oi in Text(oi.description) } } @@ -112,7 +113,7 @@ struct ExternalNotificationConfig: View { .foregroundColor(.gray) .font(.callout) } - + Section(header: Text("Optional GPIO") .font(.caption) .foregroundColor(.gray) @@ -199,7 +200,7 @@ struct ExternalNotificationConfig: View { setExternalNotificationValues() // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.externalNotificationConfig == nil { - print("empty external notification module config") + Logger.mesh.info("empty external notification module config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 58beac1c..29c31e32 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -6,6 +6,7 @@ // import SwiftUI import CoreLocation +import OSLog struct MQTTConfig: View { @@ -32,8 +33,7 @@ struct MQTTConfig: View { @State var mapPublishIntervalSecs = 3600 @State var preciseLocation: Bool = false @State var mapPositionPrecision: Double = 13.0 - - + let locale = Locale.current var body: some View { @@ -41,7 +41,7 @@ struct MQTTConfig: View { Form { if node != nil && node?.loRaConfig != nil { let rc = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0)) - if rc?.dutyCycle ?? 0 > 0 && rc?.dutyCycle ?? 0 < 100 { + if rc?.dutyCycle ?? 0 > 0 && rc?.dutyCycle ?? 0 < 100 { Text("Your region has a \(rc?.dutyCycle ?? 0)% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffic will quickly overwhelm your LoRa mesh.") .font(.callout) .foregroundColor(.red) @@ -51,19 +51,19 @@ struct MQTTConfig: View { ConfigHeader(title: "MQTT", config: \.mqttConfig, node: node, onAppear: setMqttValues) Section(header: Text("options")) { - + Toggle(isOn: $enabled) { Label("enabled", systemImage: "dot.radiowaves.up.forward") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $proxyToClientEnabled) { - + Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right") Text("Utilizes the network connection on your phone to connect to MQTT.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + if enabled && proxyToClientEnabled && node!.mqttConfig!.proxyToClientEnabled == true { Toggle(isOn: $mqttConnected) { Label(mqttConnected ? "mqtt.disconnect".localized : "mqtt.connect".localized, systemImage: "server.rack") @@ -72,25 +72,25 @@ struct MQTTConfig: View { .fixedSize(horizontal: false, vertical: true) .foregroundColor(.red) } - + } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - + Toggle(isOn: $encryptionEnabled) { Label("Encryption Enabled", systemImage: "lock.icloud") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + Toggle(isOn: $jsonEnabled) { Label("JSON Enabled", systemImage: "ellipsis.curlybraces") Text("JSON mode is a limited, unencrypted MQTT output for locally integrating with home assistant") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - + Section(header: Text("Map Report")) { - + Toggle(isOn: $mapReportingEnabled) { Label("enabled", systemImage: "map") } @@ -104,7 +104,7 @@ struct MQTTConfig: View { } } .pickerStyle(DefaultPickerStyle()) - + VStack(alignment: .leading) { Toggle(isOn: $preciseLocation) { Label("Precise Location", systemImage: "scope") @@ -119,7 +119,7 @@ struct MQTTConfig: View { } } } - + if !preciseLocation { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") @@ -157,7 +157,7 @@ struct MQTTConfig: View { Text("The root topic to use for MQTT.") .foregroundColor(.gray) .font(.callout) - + if nearbyTopics.count > 0 { Picker("Nearby Topics", selection: $selectedTopic ) { ForEach(nearbyTopics, id: \.self) { nt in @@ -171,7 +171,7 @@ struct MQTTConfig: View { .font(.callout) } } - + Section(header: Text("Server")) { HStack { Label("Address", systemImage: "server.rack") @@ -190,7 +190,7 @@ struct MQTTConfig: View { .keyboardType(.default) } .autocorrectionDisabled() - + HStack { Label("mqtt.username", systemImage: "person.text.rectangle") TextField("mqtt.username", text: $username) @@ -198,9 +198,9 @@ struct MQTTConfig: View { .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: username, perform: { _ in - + let totalBytes = username.utf8.count - + // Only mess with the value if it is too big if totalBytes > 62 { username = String(username.dropLast()) @@ -218,7 +218,7 @@ struct MQTTConfig: View { .autocapitalization(.none) .disableAutocorrection(true) .onChange(of: password, perform: { _ in - + let totalBytes = password.utf8.count // Only mess with the value if it is too big if totalBytes > 62 { @@ -362,7 +362,7 @@ struct MQTTConfig: View { setMqttValues() // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil { - print("empty mqtt module config") + Logger.mesh.info("empty mqtt module config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestMqttModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) @@ -371,20 +371,20 @@ struct MQTTConfig: View { } } func setMqttValues() { - + if #available(iOS 17.0, macOS 14.0, *) { - + nearbyTopics = [] let geocoder = CLGeocoder() if LocationsHandler.shared.locationsArray.count > 0 { let region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))?.topic defaultTopic = "msh/" + (region ?? "UNSET") - geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) -> Void in - if error != nil { - print("Failed to reverse geocode location") + geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in + if let error { + Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription)") return } - + if let placemarks = placemarks, let placemark = placemarks.first { let cc = locale.region?.identifier ?? "UNK" /// Country Topic unless you are US @@ -412,10 +412,8 @@ struct MQTTConfig: View { if !neightborhoodTopic.isEmpty { nearbyTopics.append(neightborhoodTopic) } - } - else - { - print("No Location") + } else { + Logger.services.debug("No Location") } }) } diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index 30599af2..d3582e15 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -60,7 +60,7 @@ struct PaxCounterConfig: View { if self.bleManager.context == nil { self.bleManager.context = context } - + setPaxValues() // Need to request a PAX Counter module config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.paxCounterConfig == nil { @@ -76,11 +76,11 @@ struct PaxCounterConfig: View { } } .onChange(of: paxcounterUpdateInterval) { - if let val = node?.paxCounterConfig?.paxcounterUpdateInterval { + if let val = node?.paxCounterConfig?.updateInterval { hasChanges = $0 != val } } - + SaveConfigButton(node: node, hasChanges: $hasChanges) { guard let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context), let fromUser = connectedNode.user, @@ -91,7 +91,7 @@ struct PaxCounterConfig: View { var config = ModuleConfig.PaxcounterConfig() config.enabled = enabled config.paxcounterUpdateInterval = UInt32(paxcounterUpdateInterval) - + let adminMessageId = bleManager.savePaxcounterModuleConfig( config: config, fromUser: fromUser, @@ -109,6 +109,6 @@ struct PaxCounterConfig: View { private func setPaxValues() { enabled = node?.paxCounterConfig?.enabled ?? enabled - paxcounterUpdateInterval = Int(node?.paxCounterConfig?.paxcounterUpdateInterval ?? 900) + paxcounterUpdateInterval = Int(node?.paxCounterConfig?.updateInterval ?? 900) } } diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index b56ef643..912a8be7 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -5,6 +5,7 @@ // Copyright (c) Garth Vander Houwen 6/13/22. // import SwiftUI +import OSLog struct RangeTestConfig: View { @@ -24,7 +25,7 @@ struct RangeTestConfig: View { VStack { Form { ConfigHeader(title: "Range", config: \.rangeTestConfig, node: node, onAppear: setRangeTestValues) - + Section(header: Text("options")) { Toggle(isOn: $enabled) { Label("enabled", systemImage: "figure.walk") @@ -41,14 +42,14 @@ struct RangeTestConfig: View { Text("This device will send out range test messages on the selected interval.") .foregroundColor(.gray) .font(.callout) - + Toggle(isOn: $save) { Label("save", systemImage: "square.and.arrow.down.fill") Text("Saves a CSV with the range test message details, currently only available on ESP32 devices with a web server.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .disabled(!(node != nil && node?.metadata?.hasWifi ?? false)) - + } } .disabled(self.bleManager.connectedPeripheral == nil || node?.rangeTestConfig == nil) @@ -81,7 +82,7 @@ struct RangeTestConfig: View { setRangeTestValues() // Need to request a RangeTestModule Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.rangeTestConfig == nil { - print("empty range test module config") + Logger.mesh.debug("empty range test module config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index dfd951df..6df3e504 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -22,7 +22,7 @@ struct RtttlConfig: View { VStack { Form { ConfigHeader(title: "ringtone", config: \.rtttlConfig, node: node, onAppear: setRtttLConfigValue) - + Section(header: Text("options")) { HStack { Label("ringtone", systemImage: "music.quarternote.3") diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index f4e88f45..e1500ec8 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -5,6 +5,7 @@ // Copyright (c) Garth Vander Houwen 6/22/22. // import SwiftUI +import OSLog struct SerialConfig: View { @@ -25,12 +26,12 @@ struct SerialConfig: View { @State var timeout = 0 @State var overrideConsoleSerialPort = false @State var mode = 0 - + var body: some View { VStack { Form { ConfigHeader(title: "Serial", config: \.serialConfig, node: node, onAppear: setSerialValues) - + Section(header: Text("options")) { Toggle(isOn: $enabled) { @@ -137,7 +138,7 @@ struct SerialConfig: View { setSerialValues() // Need to request a SerialModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.serialConfig == nil { - print("empty serial module config") + Logger.mesh.debug("empty serial module config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index e9b1c985..cf3a89e0 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -6,6 +6,7 @@ // import SwiftUI +import OSLog struct StoreForwardConfig: View { @@ -32,9 +33,9 @@ struct StoreForwardConfig: View { VStack { Form { ConfigHeader(title: "storeforward", config: \.storeForwardConfig, node: node, onAppear: setStoreAndForwardValues) - + Section(header: Text("options")) { - + Toggle(isOn: $enabled) { Label("enabled", systemImage: "envelope.arrow.triangle.branch") Text("Enables the store and forward module. Store and forward must be enabled on both client and router devices.") @@ -66,7 +67,7 @@ struct StoreForwardConfig: View { } } } - + if isRouter { Section(header: Text("Router Options")) { Toggle(isOn: $heartbeat) { @@ -116,10 +117,10 @@ struct StoreForwardConfig: View { do { try context.save() } catch { - print("Failed to save isRouter") + Logger.mesh.error("Failed to save isRouter: \(error.localizedDescription)") } } - + var sfc = ModuleConfig.StoreForwardConfig() sfc.enabled = self.enabled sfc.heartbeat = self.heartbeat @@ -144,10 +145,10 @@ struct StoreForwardConfig: View { if self.bleManager.context == nil { self.bleManager.context = context } - + // Need to request a Detection Sensor Module Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.storeForwardConfig == nil { - print("empty store and forward module config") + Logger.mesh.debug("empty store and forward module config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 7483118b..6271cd24 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -5,6 +5,7 @@ // Copyright (c) Garth Vander Houwen 6/13/22. // import SwiftUI +import OSLog struct TelemetryConfig: View { @@ -24,13 +25,12 @@ struct TelemetryConfig: View { @State var powerMeasurementEnabled = false @State var powerUpdateInterval = 0 @State var powerScreenEnabled = false - var body: some View { VStack { Form { ConfigHeader(title: "Telemetry", config: \.telemetryConfig, node: node, onAppear: setTelemetryValues) - + Section(header: Text("update.interval")) { Picker("Device Metrics", selection: $deviceUpdateInterval ) { ForEach(UpdateIntervals.allCases) { ui in @@ -135,7 +135,7 @@ struct TelemetryConfig: View { setTelemetryValues() // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.telemetryConfig == nil { - print("empty telemetry module config") + Logger.mesh.info("empty telemetry module config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) @@ -176,7 +176,7 @@ struct TelemetryConfig: View { if node != nil && node?.telemetryConfig != nil { if newPowerUpdateInterval != node!.telemetryConfig!.powerUpdateInterval { hasChanges = true } } - } + } .onChange(of: powerScreenEnabled) { newPowerScreenEnabled in if node != nil && node?.telemetryConfig != nil { if newPowerScreenEnabled != node!.telemetryConfig!.powerScreenEnabled { hasChanges = true } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index ca134b7b..7571107d 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -6,6 +6,7 @@ // import SwiftUI +import OSLog struct NetworkConfig: View { @@ -28,16 +29,16 @@ struct NetworkConfig: View { VStack { Form { ConfigHeader(title: "Network", config: \.networkConfig, node: node, onAppear: setNetworkValues) - - if (node != nil && node?.metadata?.hasWifi ?? false) { + + if node != nil && node?.metadata?.hasWifi ?? false { Section(header: Text("WiFi Options")) { - + Toggle(isOn: $wifiEnabled) { Label("enabled", systemImage: "wifi") Text("Enabling WiFi will disable the bluetooth connection to the app.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + HStack { Label("ssid", systemImage: "network") TextField("ssid", text: $wifiSsid) @@ -74,7 +75,7 @@ struct NetworkConfig: View { .keyboardType(.default) } } - if (node != nil && node?.metadata?.hasEthernet ?? false) { + if node != nil && node?.metadata?.hasEthernet ?? false { Section(header: Text("Ethernet Options")) { Toggle(isOn: $ethEnabled) { Label("enabled", systemImage: "network") @@ -119,7 +120,7 @@ struct NetworkConfig: View { setNetworkValues() // Need to request a NetworkConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.networkConfig == nil { - print("empty network config") + Logger.mesh.info("empty network config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestNetworkConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 575de199..1f51b801 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -6,6 +6,7 @@ // import SwiftUI +import OSLog struct PositionFlags: OptionSet { let rawValue: Int @@ -76,8 +77,8 @@ struct PositionConfig: View { @State var minimumVersion = "2.3.3" @State private var supportedVersion = true @State private var showingSetFixedAlert = false - //@State private var showingRemoveFixedAlert = false - + // @State private var showingRemoveFixedAlert = false + var body: some View { VStack { Form { @@ -98,12 +99,12 @@ struct PositionConfig: View { .foregroundColor(.gray) .font(.callout) } - + Toggle(isOn: $smartPositionEnabled) { Label("Smart Position", systemImage: "brain") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + if smartPositionEnabled { VStack(alignment: .leading) { Picker("Minimum Interval", selection: $broadcastSmartMinimumIntervalSecs) { @@ -147,8 +148,7 @@ struct PositionConfig: View { .padding(.top, 5) .padding(.bottom, 5) if gpsMode == 1 { - - + Text("Positions will be provided by your device GPS, if you select disabled or not present you can set a fixed position.") .foregroundColor(.gray) .font(.callout) @@ -170,7 +170,7 @@ struct PositionConfig: View { if !(node?.positionConfig?.fixedPosition ?? false) { Text("Your current location will be set as the fixed position and broadcast over the mesh on the position interval.") } else { - + } } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -215,7 +215,7 @@ struct PositionConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } Section(header: Text("Advanced Position Flags")) { - + if includeAltitude { Toggle(isOn: $includeAltitudeMsl) { Label("Altitude is Mean Sea Level", systemImage: "arrow.up.to.line.compact") @@ -239,7 +239,7 @@ struct PositionConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } - + if gpsMode == 1 { Section(header: Text("Advanced Device GPS")) { Picker("GPS Receive GPIO", selection: $rxGpio) { @@ -285,35 +285,34 @@ struct PositionConfig: View { if node?.positionConfig?.fixedPosition ?? false { Button("Remove", role: .destructive) { if !bleManager.removeFixedPosition(fromUser: node!.user!, channel: 0) { - print("Set Position Failed") + Logger.mesh.error("Remove Fixed Position Failed") } - print("Remove a fixed position here") let mutablePositions = node?.positions?.mutableCopy() as? NSMutableOrderedSet mutablePositions?.removeAllObjects() node?.positions = mutablePositions node?.positionConfig?.fixedPosition = false do { try context.save() - print("💾 Updated Position Config with Fixed Position = false") + Logger.data.info("💾 Updated Position Config with Fixed Position = false") } catch { context.rollback() let nsError = error as NSError - print("💥 Error Saving Position Config Entity \(nsError)") + Logger.data.error("Error Saving Position Config Entity \(nsError)") } } } else { Button("Set") { if !bleManager.setFixedPosition(fromUser: node!.user!, channel: 0) { - print("Set Position Failed") + Logger.mesh.error("Set Position Failed") } node?.positionConfig?.fixedPosition = true do { try context.save() - print("💾 Updated Position Config with Fixed Position = true") + Logger.data.info("💾 Updated Position Config with Fixed Position = true") } catch { context.rollback() let nsError = error as NSError - print("💥 Error Saving Position Config Entity \(nsError)") + Logger.data.error("Error Saving Position Config Entity \(nsError)") } } } @@ -376,7 +375,7 @@ struct PositionConfig: View { supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame // Need to request a PositionConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.positionConfig == nil { - print("empty position config") + Logger.mesh.info("empty position config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestPositionConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 11b8a71e..155d9a5f 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -17,9 +17,9 @@ struct PowerConfig: View { @State private var waitBluetoothSecs = 60 @State private var lsSecs = 300 @State private var minWakeSecs = 10 - + @State private var currentDevice: DeviceHardware? - + @State private var hasChanges: Bool = false @FocusState private var isFocused: Bool @@ -28,7 +28,7 @@ struct PowerConfig: View { ConfigHeader(title: "config.power.title", config: \.powerConfig, node: node, onAppear: setPowerValues) Section { - if (currentDevice?.architecture == .esp32 || currentDevice?.architecture == .esp32S3) || (currentDevice?.architecture == .nrf52840 && (node?.deviceConfig?.role ?? 0 == 5 || node?.deviceConfig?.role ?? 0 == 6)) { + if (currentDevice?.architecture == .esp32 || currentDevice?.architecture == .esp32S3) || (currentDevice?.architecture == .nrf52840 && (node?.deviceConfig?.role ?? 0 == 5 || node?.deviceConfig?.role ?? 0 == 6)) { Toggle(isOn: $isPowerSaving) { Label("config.power.saving", systemImage: "bolt") Text("config.power.saving.description") @@ -120,13 +120,13 @@ struct PowerConfig: View { if self.bleManager.context == nil { self.bleManager.context = context } - + Api().loadDeviceHardwareData { (hw) in - + for device in hw { let currentHardware = node?.user?.hwModel ?? "UNSET" let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") - if deviceString == currentHardware { + if deviceString == currentHardware { currentDevice = device } } diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index f27b1967..a7e467b9 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -7,6 +7,7 @@ import SwiftUI import StoreKit +import OSLog struct Firmware: View { @Environment(\.managedObjectContext) var context @@ -17,14 +18,14 @@ struct Firmware: View { @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? @State private var latestAlpha: FirmwareRelease? - + var body: some View { - + let supportedVersion = self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame ScrollView { VStack(alignment: .leading) { let deviceString = currentDevice?.hwModelSlug.replacingOccurrences(of: "_", with: "") - + HStack { VStack { Image(systemName: currentDevice?.activelySupported ?? false ? "checkmark.seal.fill" : "x.circle") @@ -45,7 +46,7 @@ struct Firmware: View { .frame(width: 300, height: 300) .cornerRadius(5) } - + if supportedVersion { Text("Your Firmware is up to date") .fixedSize(horizontal: false, vertical: true) @@ -72,7 +73,7 @@ struct Firmware: View { .fixedSize(horizontal: false, vertical: true) .font(.title2) .padding(.bottom) - + Text("Get the latest stable firmware") .fixedSize(horizontal: false, vertical: true) .font(.callout) @@ -81,7 +82,7 @@ struct Firmware: View { Link("Release Notes", destination: URL(string: "\(latestStable?.pageURL ?? "https://meshtastic.org")")!) .font(.caption) .padding(.bottom) - + Text("Get the latest alpha firmware") .fixedSize(horizontal: false, vertical: true) .font(.callout) @@ -90,10 +91,10 @@ struct Firmware: View { Link("Release Notes", destination: URL(string: "\(latestAlpha?.pageURL ?? "https://meshtastic.org")")!) .font(.caption) .padding(.bottom) - + if currentDevice?.architecture == Meshtastic.Architecture.nrf52840 { VStack(alignment: .leading) { - + Text("Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor.") .fixedSize(horizontal: false, vertical: true) .foregroundStyle(.gray) @@ -109,13 +110,13 @@ struct Firmware: View { Button { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) if connectedNode != nil { - + if bleManager.sendEnterDfuMode(fromUser: connectedNode!.user!, toUser: node!.user!) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { bleManager.disconnectPeripheral(reconnect: false) } } else { - print("Enter DFU Failed") + Logger.mesh.error("Enter DFU Failed") } } } label: { @@ -160,7 +161,7 @@ struct Firmware: View { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) if connectedNode != nil { if !bleManager.sendRebootOta(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex) { - print("Reboot Failed") + Logger.mesh.error("Reboot Failed") } } } label: { @@ -178,18 +179,18 @@ struct Firmware: View { .font(.title3) Text(node?.user?.hwModel ?? "UNSET") .font(.title3) - Text ( currentDevice?.architecture.rawValue ?? "UNKNOWN") + Text( currentDevice?.architecture.rawValue ?? "UNKNOWN") .font(.title3) } } .padding() .padding(.bottom, 5) - .onAppear() { + .onAppear { Api().loadDeviceHardwareData { (hw) in for device in hw { let currentHardware = node?.user?.hwModel ?? "UNSET" let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") - if deviceString == currentHardware { + if deviceString == currentHardware { currentDevice = device } } diff --git a/Meshtastic/Views/Settings/FirmwareApi.swift b/Meshtastic/Views/Settings/FirmwareApi.swift index c894006e..3b64c1e9 100644 --- a/Meshtastic/Views/Settings/FirmwareApi.swift +++ b/Meshtastic/Views/Settings/FirmwareApi.swift @@ -6,6 +6,7 @@ // import Foundation +import OSLog /// Device Hardware API struct DeviceHardware: Codable { @@ -43,45 +44,45 @@ struct FirmwareRelease: Codable { } } -class Api : ObservableObject{ +class Api: ObservableObject { - func loadDeviceHardwareData(completion:@escaping ([DeviceHardware]) -> ()) { + func loadDeviceHardwareData(completion: @escaping ([DeviceHardware]) -> Void) { /// List from https://api.meshtastic.org/resource/deviceHardware guard let url = Bundle.main.url(forResource: "DeviceHardware.json", withExtension: nil) else { - print("Couldn't find DeviceHardware.json in main bundle.") + Logger.services.critical("Couldn't find DeviceHardware.json in main bundle.") return } - - URLSession.shared.dataTask(with: url) { data, response, error in + + URLSession.shared.dataTask(with: url) { data, _, _ in if let data = data { do { let deviceHardware = try JSONDecoder().decode([DeviceHardware].self, from: data) DispatchQueue.main.async { completion(deviceHardware) } - } catch let jsonError as NSError { - print("JSON decode failure: \(jsonError.localizedDescription)") + } catch { + Logger.services.error("JSON decode failure: \(error.localizedDescription)") } return } }.resume() } - - func loadFirmwareReleaseData(completion:@escaping (FirmwareReleases) -> ()) { + + func loadFirmwareReleaseData(completion: @escaping (FirmwareReleases) -> Void) { guard let url = URL(string: "https://api.meshtastic.org/github/firmware/list") else { - print("Invalid url...") + Logger.services.error("Invalid url...") return } - URLSession.shared.dataTask(with: url) { data, response, error in + URLSession.shared.dataTask(with: url) { data, _, _ in if let data = data { do { let firmwareReleases = try JSONDecoder().decode(FirmwareReleases.self, from: data) DispatchQueue.main.async { completion(firmwareReleases) } - } catch let jsonError as NSError { - print("JSON decode failure: \(jsonError.localizedDescription)") + } catch { + Logger.services.error("JSON decode failure: \(error.localizedDescription)") } return } diff --git a/Meshtastic/Views/Settings/GPSStatus.swift b/Meshtastic/Views/Settings/GPSStatus.swift index b9e18679..b1119694 100644 --- a/Meshtastic/Views/Settings/GPSStatus.swift +++ b/Meshtastic/Views/Settings/GPSStatus.swift @@ -10,20 +10,20 @@ import CoreLocation @available(iOS 17.0, macOS 14.0, *) struct GPSStatus: View { - + var largeFont: Font = .footnote var smallFont: Font = .caption2 - + @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared var body: some View { - + if let newLocation = locationsHandler.locationsArray.last { let horizontalAccuracy = Measurement(value: newLocation.horizontalAccuracy, unit: UnitLength.meters) let verticalAccuracy = Measurement(value: newLocation.verticalAccuracy, unit: UnitLength.meters) let altitiude = Measurement(value: newLocation.altitude, unit: UnitLength.meters) let speed = Measurement(value: newLocation.speed, unit: UnitSpeed.kilometersPerHour) let speedAccuracy = Measurement(value: newLocation.speedAccuracy, unit: UnitSpeed.metersPerSecond) - let courseAccuracy = Measurement(value: newLocation.courseAccuracy, unit: UnitAngle.degrees) + let courseAccuracy = Measurement(value: newLocation.courseAccuracy, unit: UnitAngle.degrees) Label("Coordinate \(String(format: "%.5f", newLocation.coordinate.latitude)), \(String(format: "%.5f", newLocation.coordinate.longitude))", systemImage: "mappin") .font(largeFont) @@ -33,7 +33,7 @@ struct GPSStatus: View { .font(largeFont) Label("Sats Estimate \(LocationsHandler.satsInView)", systemImage: "sparkles") .font(largeFont) - + } HStack { if newLocation.verticalAccuracy > 0 { diff --git a/Meshtastic/Views/Settings/MeshLog.swift b/Meshtastic/Views/Settings/MeshLog.swift index 4f0072ca..20fdf9d3 100644 --- a/Meshtastic/Views/Settings/MeshLog.swift +++ b/Meshtastic/Views/Settings/MeshLog.swift @@ -1,6 +1,7 @@ import SwiftUI import Foundation import UniformTypeIdentifiers +import OSLog struct MeshLog: View { let logFile = MeshLogger.logFile @@ -46,10 +47,11 @@ struct MeshLog: View { contentType: UTType.plainText, defaultFilename: "mesh-activity-log", onCompletion: { result in - if case .success = result { - print("Mesh activity log download: success.") - } else { - print("Mesh activity log download \(result).") + switch result { + case .success: + Logger.services.info("Mesh activity log download: success") + case .failure(let error): + Logger.services.error("Mesh activity log download: \(error.localizedDescription)") } } ) @@ -64,7 +66,7 @@ struct MeshLog: View { try text.write(to: logFile!, atomically: false, encoding: .utf8) logs.removeAll() } catch { - print(error) + Logger.services.error("\(error.localizedDescription)") } } label: { Label("Clear", systemImage: "trash.fill") diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index 766b72d5..c19dbd9f 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -10,10 +10,11 @@ import CoreData import MapKit import CoreLocation import CoreMotion +import OSLog @available(iOS 17.0, macOS 14.0, *) struct RouteRecorder: View { - + @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) @@ -24,7 +25,7 @@ struct RouteRecorder: View { @State var recording: RouteEntity? @State var color: Color = .blue @State var activity: Int = 1 - + var body: some View { VStack { ZStack { @@ -34,7 +35,7 @@ struct RouteRecorder: View { let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in return position.coordinate }) - + let gradient = LinearGradient( colors: [color], startPoint: .leading, endPoint: .trailing @@ -79,7 +80,7 @@ struct RouteRecorder: View { NavigationStack { VStack { if locationsHandler.isRecording { - HStack (alignment: .center) { + HStack(alignment: .center) { Image(systemName: "record.circle.fill") .symbolRenderingMode(.multicolor) .font(.title) @@ -93,8 +94,8 @@ struct RouteRecorder: View { } .padding() } else if locationsHandler.isRecordingPaused { - HStack (alignment: .center) { - + HStack(alignment: .center) { + Image(systemName: "playpause") .symbolRenderingMode(.multicolor) .font(.title3) @@ -104,7 +105,7 @@ struct RouteRecorder: View { } .padding(.top) } - + if locationsHandler.isRecording || locationsHandler.isRecordingPaused { Divider() HStack { @@ -184,11 +185,11 @@ struct RouteRecorder: View { self.recording = newRoute do { try context.save() - print("💾 Saved a new route") + Logger.data.info("💾 Saved a new route") } catch { context.rollback() let nsError = error as NSError - print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") + Logger.data.error("Error Saving RouteEntity from the Route Recorder \(nsError)") } } label: { Label("start", systemImage: "play") @@ -197,7 +198,7 @@ struct RouteRecorder: View { .buttonBorderShape(.capsule) .controlSize(.large) .padding(.bottom) - + } else if locationsHandler.isRecording { /// We are recording show pause button Button { @@ -223,16 +224,16 @@ struct RouteRecorder: View { .controlSize(.large) .padding(.bottom) } - + if locationsHandler.isRecording || locationsHandler.isRecordingPaused { /// We are recording or paused, show finish button Button { - + if let rec = recording { rec.enabled = true rec.distance = locationsHandler.distanceTraveled rec.elevationGain = locationsHandler.elevationGain - context.refresh(rec, mergeChanges:true) + context.refresh(rec, mergeChanges: true) } locationsHandler.isRecording = false locationsHandler.isRecordingPaused = false @@ -242,11 +243,11 @@ struct RouteRecorder: View { locationsHandler.recordingStarted = nil do { try context.save() - print("💾 Saved a route finish") + Logger.data.info("💾 Saved a route finish") } catch { context.rollback() let nsError = error as NSError - print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") + Logger.data.error("Error Saving RouteEntity from the Route Recorder \(nsError)") } isShowingDetails = false } label: { @@ -270,7 +271,7 @@ struct RouteRecorder: View { #endif Spacer() } - + } } } @@ -297,12 +298,12 @@ struct RouteRecorder: View { locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) do { try context.save() - print("💾 Saved a new route location") - //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") + Logger.data.info("💾 Saved a new route location") + // logger.info("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") } catch { context.rollback() let nsError = error as NSError - print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)") + Logger.data.error("Error Saving LocationEntity from the Route Recorder \(nsError)") } } } diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index 6f47c9ea..cc70de3b 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -8,10 +8,11 @@ import SwiftUI import CoreData import MapKit +import OSLog @available(iOS 17.0, macOS 14.0, *) struct Routes: View { - + @State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -20,18 +21,18 @@ struct Routes: View { @State private var isShowingBadFileAlert = false @State var isExporting = false @State var exportString = "" - + @State var hasChanges = false @State var name = "" @State var notes = "" @State var enabled = true @State var color = Color(red: 51, green: 199, blue: 88) - + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "enabled", ascending: false), NSSortDescriptor(key: "name", ascending: true), NSSortDescriptor(key: "date", ascending: false)], animation: .default) - + var routes: FetchedResults var body: some View { - + VStack { if selectedRoute == nil { Button("Import Route") { @@ -41,7 +42,7 @@ struct Routes: View { .buttonBorderShape(.capsule) .controlSize(.large) .padding() - + .alert(isPresented: $isShowingBadFileAlert) { Alert(title: Text("Not a valid route file"), message: Text("Your route file must have both Latitude and Longitude columns and headers."), dismissButton: .default(Text("OK"))) } @@ -55,7 +56,7 @@ struct Routes: View { guard selectedFile.startAccessingSecurityScopedResource() else { return } - + do { guard let fileContent = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return } let routeName = selectedFile.lastPathComponent.dropLast(4) @@ -64,7 +65,7 @@ struct Routes: View { var latIndex = -1 var longIndex = -1 for index in headers!.indices { - print("\(index): \( headers![index])") + Logger.services.debug("\(index): \( headers![index])") if headers![index].trimmingCharacters(in: .whitespaces) == "Latitude" { latIndex = index } else if headers![index].trimmingCharacters(in: .whitespaces) == "Longitude" { @@ -88,41 +89,41 @@ struct Routes: View { loc.latitudeI = Int32((Double(latitude) ?? 0) * 1e7) loc.longitudeI = Int32((Double(longitude) ?? 0) * 1e7) newLocations.append(loc) - print("Longitude: \(longitude) Latitude: \(latitude)") } } newRoute.locations? = NSOrderedSet(array: newLocations) do { try context.save() } catch let error as NSError { - print("Error: \(error.localizedDescription)") + Logger.services.error("\(error.localizedDescription)") isShowingBadFileAlert = true } } else { isShowingBadFileAlert = true } - + } catch { - print("error: \(error)") // to do deal with errors + // TODO: deal with errors + Logger.services.error("\(error.localizedDescription)") } - + } catch { - print("CSV Import Error") + Logger.services.error("CSV Import Error: \(error.localizedDescription)") } } List(routes, id: \.self, selection: $selectedRoute) { route in let routeColor = Color(UIColor(hex: route.color >= 0 ? UInt32(route.color) : 0)) Label { - VStack (alignment: .leading) { + VStack(alignment: .leading) { Text("\(route.name ?? "No Name Route")") .padding(.top) .foregroundStyle(.primary) - + Text("\(route.date?.formatted() ?? "Unknown Time")") .padding(.bottom) .font(.callout) .foregroundColor(.gray) - + if route.notes?.count ?? 0 > 0 { Text("\(route.notes ?? "")") .padding(.bottom) @@ -151,13 +152,13 @@ struct Routes: View { do { try context.save() } catch let error as NSError { - print("Error: \(error.localizedDescription)") + Logger.data.error("\(error.localizedDescription)") } } label: { Label("delete", systemImage: "trash") } } - + } .listStyle(.plain) } else { @@ -177,20 +178,20 @@ struct Routes: View { .onChange(of: name, perform: { _ in let totalBytes = name.utf8.count // Only mess with the value if it is too big - + if totalBytes > 100 { name = String(name.dropLast()) } }) - + Toggle(isOn: $enabled) { Label("enabled", systemImage: "point.topleft.filled.down.to.point.bottomright.curvepath") Text("Show on the mesh map.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - + ColorPicker("Color", selection: $color, supportsOpacity: false) - + TextField( "Notes", text: $notes, @@ -207,14 +208,14 @@ struct Routes: View { hasChanges = false } HStack { - + Button("cancel", role: .cancel) { selectedRoute = nil } .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) - + Button("save") { selectedRoute?.name = name selectedRoute?.notes = notes @@ -223,11 +224,11 @@ struct Routes: View { do { try context.save() selectedRoute = nil - print("💾 Saved a route") + Logger.data.info("💾 Saved a route") } catch { context.rollback() let nsError = error as NSError - print("💥 Error Saving RouteEntity from the Route Editor \(nsError)") + Logger.data.error("Error Saving RouteEntity from the Route Editor \(nsError)") } } .buttonStyle(.bordered) @@ -235,19 +236,19 @@ struct Routes: View { .controlSize(.large) .disabled(!hasChanges) } - .onChange(of: name) { newName in + .onChange(of: name) { _ in hasChanges = true } - .onChange(of: notes) { newNotes in + .onChange(of: notes) { _ in hasChanges = true } - .onChange(of: enabled) { newEnabled in + .onChange(of: enabled) { _ in hasChanges = true } - .onChange(of: color) { newColor in + .onChange(of: color) { _ in hasChanges = true } - Map() { + Map { Annotation("Start", coordinate: lineCoords.first ?? LocationHelper.DefaultLocation) { ZStack { Circle() @@ -295,11 +296,12 @@ struct Routes: View { contentType: .commaSeparatedText, defaultFilename: String("\(selectedRoute?.name ?? "Route") Log"), onCompletion: { result in - if case .success = result { - print("Route log download succeeded.") + switch result { + case .success: self.isExporting = false - } else { - print("Route log download failed: \(result).") + Logger.services.info("Route log download succeeded.") + case .failure(let error): + Logger.services.error("Route log download failed: \(error.localizedDescription).") } } ) diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index 7de80622..c00a3c59 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -63,7 +63,6 @@ struct SaveChannelQRCode: View { .padding() } - #if targetEnvironment(macCatalyst) Button { dismiss() diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index ec2c747f..f2a38030 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -6,6 +6,7 @@ // import SwiftUI +import OSLog #if canImport(TipKit) import TipKit #endif @@ -94,11 +95,10 @@ struct Settings: View { } .tag(SettingsSidebar.routeRecorder) } - + let node = nodes.first(where: { $0.num == preferredNodeNum }) let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0 ? true : false - - + if !(node?.deviceConfig?.isManaged ?? false) { if bleManager.connectedPeripheral != nil { Section("Configure") { @@ -107,7 +107,7 @@ struct Settings: View { if selectedNode == 0 { Text("Connect to a Node").tag(0) } - + ForEach(nodes) { node in if node.num == bleManager.connectedPeripheral?.num ?? 0 { Label { @@ -143,7 +143,7 @@ struct Settings: View { if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil && node?.metadata == nil { let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) if adminMessageId > 0 { - print("Sent node metadata request from node details") + Logger.mesh.info("Sent node metadata request from node details") } } } @@ -161,8 +161,8 @@ struct Settings: View { Section("radio.configuration") { if node != nil && node?.loRaConfig != nil { let rc = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0)) - if rc?.dutyCycle ?? 0 > 0 && rc?.dutyCycle ?? 0 < 100 { - + if rc?.dutyCycle ?? 0 > 0 && rc?.dutyCycle ?? 0 < 100 { + Label { Text("Hourly Duty Cycle") } icon: { diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index b2e2f1c3..627bc5e5 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -52,7 +52,7 @@ struct ShareChannels: View { var qrCodeImage = QrCodeImage() var body: some View { - + if #available(iOS 17.0, macOS 14.0, *) { VStack { TipView(ShareChannelsTip(), arrowEdge: .bottom) @@ -202,7 +202,7 @@ struct ShareChannels: View { .controlSize(.large) .padding(.top) .padding(.bottom) - + ShareLink("Share QR Code & Link", item: Image(uiImage: qrImage), subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"), @@ -269,20 +269,18 @@ struct ShareChannels: View { channelSet.loraConfig = loRaConfig if node?.myInfo?.channels != nil && node?.myInfo?.channels?.count ?? 0 > 0 { for ch in node?.myInfo?.channels?.array as? [ChannelEntity] ?? [] { - if ch.role > 0 { - - if ch.index == 0 && includeChannel0 || ch.index == 1 && includeChannel1 || ch.index == 2 && includeChannel2 || ch.index == 3 && includeChannel3 || - ch.index == 4 && includeChannel4 || ch.index == 5 && includeChannel5 || ch.index == 6 && includeChannel6 || ch.index == 7 && includeChannel7 { - - var channelSettings = ChannelSettings() - channelSettings.name = ch.name! - channelSettings.psk = ch.psk! - channelSettings.id = UInt32(ch.id) - channelSet.settings.append(channelSettings) - } + if ch.role > 0, ch.index == 0 && includeChannel0 || ch.index == 1 && includeChannel1 || ch.index == 2 && includeChannel2 || ch.index == 3 && includeChannel3 || + ch.index == 4 && includeChannel4 || ch.index == 5 && includeChannel5 || ch.index == 6 && includeChannel6 || ch.index == 7 && includeChannel7 { + var channelSettings = ChannelSettings() + channelSettings.name = ch.name! + channelSettings.psk = ch.psk! + channelSettings.id = UInt32(ch.id) + channelSet.settings.append(channelSettings) } } - let settingsString = try! channelSet.serializedData().base64EncodedString() + guard let settingsString = try? channelSet.serializedData().base64EncodedString() else { + return + } channelsUrl = ("https://meshtastic.org/e/#" + settingsString.base64ToBase64url() + (replaceChannels ? "" : "?add=true")) } } diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index a98c79ff..e58d4c83 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -42,11 +42,11 @@ struct UserConfig: View { VStack { Form { Section(header: Text("User Details")) { - + VStack(alignment: .leading) { HStack { Label(isLicensed ? "Call Sign" : "Long Name", systemImage: "person.crop.rectangle.fill") - + TextField("Long Name", text: $longName) .onChange(of: longName, perform: { _ in let totalBytes = longName.utf8.count @@ -65,7 +65,7 @@ struct UserConfig: View { Text("\(String(isLicensed ? "Call Sign" : "Long Name")) can be up to \(isLicensed ? "8" : "36") bytes long.") .foregroundColor(.gray) .font(.callout) - + } VStack(alignment: .leading) { HStack { @@ -167,7 +167,6 @@ struct UserConfig: View { ham.callSign = longName ham.txPower = Int32(txPower) ham.frequency = overrideFrequency - print(ham) let adminMessageId = bleManager.saveLicensedUser(ham: ham, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { hasChanges = false diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 5987d7b2..511ecdaa 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -133,6 +133,7 @@ "hybrid"="Hybrid"; "hybrid.flyover"="Hybrid Flyover"; "include"="Include"; +"incomplete"="Incomplete"; "inputevent.none"="Keins"; "inputevent.up"="Hoch"; "inputevent.down"="Runter"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 6b57e0f1..4c507879 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -137,6 +137,7 @@ "hybrid"="Hybrid"; "hybrid.flyover"="Hybrid Flyover"; "include"="Include"; +"incomplete"="Incomplete"; "inputevent.none"="None"; "inputevent.up"="Up"; "inputevent.down"="Down"; diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index c745da8f..785a2012 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -114,6 +114,7 @@ "hybrid"="Hybride"; "hybrid.flyover"="Flyover hybride"; "include"="Inclure"; +"incomplete"="Incomplete"; "inputevent.none"="Aucun"; "inputevent.up"="Haut"; "inputevent.down"="Bas"; diff --git a/he.lproj/Localizable.strings b/he.lproj/Localizable.strings index 8af2d2fd..154c2391 100644 --- a/he.lproj/Localizable.strings +++ b/he.lproj/Localizable.strings @@ -137,6 +137,7 @@ "hybrid"="היברידי"; "hybrid.flyover"="היברידי מלמעלה"; "include"="כלול"; +"incomplete"="Incomplete"; "inputevent.none"="ללא"; "inputevent.up"="למעלה"; "inputevent.down"="למטה"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index d78f00db..eb8372b0 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -135,6 +135,7 @@ "hybrid"="Hybrydowy"; "hybrid.flyover"="Hybrydowy Przelot"; "include"="Dołącz"; +"incomplete"="Incomplete"; "inputevent.none"="Brak"; "inputevent.up"="W Górę"; "inputevent.down"="W Dół"; diff --git a/protobufs b/protobufs index dd7d64cc..a45a6154 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit dd7d64cc038a6365c119ec7508762cc45f405948 +Subproject commit a45a6154d0721027bf63f85cfc5abd9f6fab2422 diff --git a/pt-PT.lproj/Localizable.strings b/pt-PT.lproj/Localizable.strings new file mode 100644 index 00000000..4c507879 --- /dev/null +++ b/pt-PT.lproj/Localizable.strings @@ -0,0 +1,377 @@ +/* + Localizable.strings + Meshtastic + + Copyright(c) Garth Vander Houwen on 12/12/22. + +*/ +"about"="About"; +"about.meshtastic"="About Meshtastic"; +"activity"="Activity"; +"admin"="Admin"; +"admin.log"="Admin Message Log"; +"ago"="ago"; +"airtime"="Airtime"; +"always.on"="Always On"; +"ambient.lighting"="Ambient Lighting"; +"ambient.lighting.config"="Ambient Lighting Config"; +"appsettings"="App Settings"; +"appsettings.provide.location"="Share location"; +"appsettings.smartposition"="Smart Position"; +"are.you.sure"="Are you sure?"; +"ascii.capable"="ASCII Capable"; +"available.radios"="Available Radios"; +"automatic.detection"="Automatic Detection"; +"battery.level"="Battery Level"; +"ble.name"="BLE Name"; +"ble.connection.timeout %d %@"="Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth."; +"ble.errorcode.6 %@"="%@ The app will automatically reconnect to the preferred radio if it comes back in range."; +"ble.errorcode.14 %@"="%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio."; +"ble.errorcode.pin %@"="%@ Please try connecting again and check the PIN carefully."; +"bluetooth"="Bluetooth"; +"bluetooth.off"="Bluetooth is off"; +"bluetooth.config"="Bluetooth Config"; +"bluetooth.mode.randompin"="Random PIN"; +"bluetooth.mode.fixedpin"="Fixed PIN"; +"bluetooth.mode.nopin"="No PIN (Just Works)"; +"bluetooth.pairingmode"="Pairing Mode"; +"bluetooth.pin.validation"="BLE Pin must be 6 digits long."; +"bytes"="Bytes"; +"cancel"="Cancel"; +"canned.messages"="Canned Messages"; +"canned.messages.config"="Canned Messages Config"; +"canned.messages.preset.manual"="Manual Configuration"; +"canned.messages.preset.rakrotary"="RAK Rotary Encoder Module"; +"canned.messages.preset.cardkb"="M5 Stack Card KB / RAK Keypad"; +"channel"="Channel"; +"channel.role.disabled"="Disabled"; +"channel.role.primary"="Primary"; +"channel.role.secondary"="Secondary"; +"channel.utilization"="Channel Utilization"; +"channels"="Channels"; +"clear.app.data"="Clear App Data"; +"clear.log"="Clear"; +"close"="Close"; +"config.power.settings"="Power"; +"config.power.title"="Power Config"; +"config.power.section.battery"="Battery"; +"config.power.section.sleep"="Sleep"; +"config.power.adc.override"="ADC Override"; +"config.power.adc.multiplier"="Multiplier"; +"config.power.ls.secs"="Light Sleep Interval"; +"config.power.min.wake.secs"="Minimum Wake Interval"; +"config.power.saving"="Power Saving"; +"config.power.saving.description"="Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button."; +"config.power.shutdown.on.power.loss"="Shutdown on Power Loss"; +"config.power.shutdown.after.secs"="After"; +"config.power.wait.bluetooth.secs"="Bluetooth Off After"; +"config.ringtone"="RTTTL Ringtone"; +"config.ringtone.title"="Ringtone Config"; +"config.ringtone.label"="Ringtone Transfer Language"; +"config.ringtone.description"="Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications."; +"config.module.paxcounter.settings"="PAX Counter"; +"config.module.paxcounter.title"="PAX Counter Config"; +"config.module.paxcounter.enabled.description"="When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be disabled for PAX counter to work."; +"config.module.paxcounter.updateinterval"="Update Interval"; +"config.module.paxcounter.updateinterval.description"="How often we can send a message to the mesh when people are detected."; +"config.save.confirm"="After config values save the node will reboot."; +"communicating"="Communicating with device. ."; +"connected.radio"="Connected Radio"; +"connected"="Bluetooth Connected"; +"connecting"="Connecting . ."; +"contacts"="Contacts"; +"contacts %@"="Contacts (%@)"; +"copy"="Copy"; +"current"="Current"; +"default"="Default"; +"delete"="Delete"; +"detection.sensor"="Detection Sensor"; +"detection.sensor.config"="Detection Sensor Config"; +"detection.sensor.log"="Detection Sensor Log"; +"device"="Device"; +"device.config"="Device Config"; +"device.configuration"="Device Configuration"; +"device.metrics.delete"="Delete all device metrics?"; +"device.metrics.log"="Device Metrics Log"; +"device.role.client"="App connected or stand alone messaging device."; +"device.role.clientmute"="Device that does not forward packets from other devices."; +"device.role.clienthidden"="Device that only broadcasts as needed for stealth or power savings."; +"device.role.tracker"="Broadcasts GPS position packets as priority."; +"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery."; +"device.role.sensor"="Broadcasts telemetry packets as priority."; +"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; +"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts."; +"device.role.repeater"="Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list."; +"device.role.router"="Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list."; +"device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile devices."; +"direct.messages"="Direct Messages"; +"dismiss.keyboard"="Dismiss"; +"display"="Display"; +"display.config"="Display Config"; +"distance"="Distance"; +"disconnect"="Disconnect"; +"echo"="Echo"; +"email.address"="Email Address"; +"enabled"="Enabled"; +"encrypted"="Encrypted"; +"export"="Export"; +"external.notification"="External Notification"; +"external.notification.config"="External Notification Config"; +"finish"="Finish"; +"firmware.version"="Firmware Version"; +"firmware.version.unsupported"="Unsupported Firmware Version Detected, unable to connect to device."; +"gas"="Gas"; +"gas.resistance"="Gas Resistance"; +"generate.qr.code"="Generate QR Code"; +"gpsformat.dec"="Decimal Degrees Format"; +"gpsformat.dms"="Degrees Minutes Seconds"; +"gpsformat.utm"="Universal Transverse Mercator"; +"gpsformat.mgrs"="Military Grid Reference System"; +"gpsformat.olc"="Open Location Code (aka Plus Codes)"; +"gpsformat.osgr"="Ordnance Survey Grid Reference"; +"gpsmode.disabled"="Disabled"; +"gpsmode.enabled"="Enabled"; +"gpsmode.notPresent"="Not Present"; +"heard"="Heard"; +"heard.last"="Last Heard"; +"hybrid"="Hybrid"; +"hybrid.flyover"="Hybrid Flyover"; +"include"="Include"; +"incomplete"="Incomplete"; +"inputevent.none"="None"; +"inputevent.up"="Up"; +"inputevent.down"="Down"; +"inputevent.left"="Left"; +"inputevent.right"="Right"; +"inputevent.select"="Select"; +"inputevent.back"="Back"; +"inputevent.cancel"="Cancel"; +"interval.one.second"="One Second"; +"interval.two.seconds"="Two Seconds"; +"interval.three.seconds"="Three Seconds"; +"interval.four.seconds"="Four Seconds"; +"interval.five.seconds"="Five Seconds"; +"interval.ten.seconds"="Ten Seconds"; +"interval.fifteen.seconds"="Fifteen Seconds"; +"interval.twenty.seconds"="Twenty Seconds"; +"interval.twentyfive.seconds"="Twenty Five Seconds"; +"interval.thirty.seconds"="Thirty Seconds"; +"interval.fortyfive.seconds"="Forty Five Seconds"; +"interval.one.minute"="One Minute"; +"interval.two.minutes"="Two Minutes"; +"interval.five.minutes"="Five Minutes"; +"interval.ten.minutes"="Ten Minutes"; +"interval.fifteen.minutes"="Fifteen Minutes"; +"interval.thirty.minutes"="Thirty Minutes"; +"interval.one.hour"="One Hour"; +"interval.two.hours"="Two Hours"; +"interval.three.hours"="Three Hours"; +"interval.four.hours"="Four Hours"; +"interval.five.hours"="Five Hours"; +"interval.six.hours"="Six Hours"; +"interval.twelve.hours"="Twelve Hours"; +"interval.eighteen.hours"="Eighteen Hours"; +"interval.twentyfour.hours"="Twenty Four Hours"; +"interval.thirtysix.hours"="Thirty Six Hours"; +"interval.fortyeight.hours"="Forty Eight Hours"; +"interval.seventytwo.hours"="Seventy Two Hours"; +"keyboard.type"="Keyboard Type"; +"logging"="Logging"; +"lora"="LoRa"; +"lora.config"="LoRa Config"; +"map"="Mesh Map"; +"map.type"="Default Type"; +"map.centering"="Centering Mode"; +"map.tiles.delete"="Delete All Map Tiles"; +"map.recentering"="Automatic Re-centering"; +"map.use.legacy"="Use Legacy Mesh Map"; +"map.usertrackingmode"="User tracking mode"; +"map.usertrackingmode.follow"="Follow"; +"map.usertrackingmode.followwithheading"="Follow with heading"; +"map.usertrackingmode.none"="None"; +"mesh.live.activity"="Mesh Live Activity"; +"mesh.log"="Mesh Log"; +"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; +"mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; +"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; +"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@"; +"mesh.log.cannedmessages.messages.received %@"="Canned Messages Messages Received For: %@"; +"mesh.log.channel.sent %@ %d"="Sent a Channel for: %@ Channel Index %d"; +"mesh.log.channel.received %d %@"="Channel %d received from: %@"; +"mesh.log.device.config %@"="Device config received: %@"; +"mesh.log.display.config %@"="Display config received: %@"; +"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@"; +"mesh.log.device.metadata.received %@"="Device Metadata received from: %@"; +"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@"; +"mesh.log.externalnotification.config %@"="External Notification module config received: %@"; +"mesh.log.lora.config %@"="LoRa config received: %@"; +"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@"; +"mesh.log.mqtt.config %@"="MQTT module config received: %@"; +"mesh.log.myinfo %@"="MyInfo received: %@"; +"mesh.log.network.config %@"="Network config received: %@"; +"mesh.log.nodeinfo.received %@"="Node info received for: %@"; +"mesh.log.paxcounter %@"="PAX Counter message received from: %@"; +"mesh.log.paxcounter.config %@"="PAX Counter config received: %@"; +"mesh.log.position.config %@"="Positon config received: %@"; +"mesh.log.position.received %@"="Position Packet received from node: %@"; +"mesh.log.power.config %@"="Power config received: %@"; +"mesh.log.rangetest.config %@"="Range Test module config received: %@"; +"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; +"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; +"mesh.log.serial.config %@"="Serial module config received: %@"; +"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; +"mesh.log.storeforward.config %@"="Store & Forward module config received: %@"; +"mesh.log.telemetry.config %@"="Telemetry module config received: %@"; +"mesh.log.telemetry.received %@"="Telemetry received for: %@"; +"mesh.log.textmessage.received"="Message received from the text message app."; +"mesh.log.textmessage.send.failed %@"="Message Send Failed, not properly connected to %@"; +"mesh.log.textmessage.sent %@ %@ %@"="Sent message %@ from %@ to %@"; +"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly."; +"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@"; +"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; +"mesh.log.wantconfig %@"="Issuing Want Config to %@"; +"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; +"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; +"message"="Message"; +"message.details"="Message Details"; +"messages"="Messages"; +"mode"="Mode"; +"module.configuration"="Module Configuration"; +"mqtt"="MQTT"; +"mqtt.connect"="Connect to MQTT"; +"mqtt.config"="MQTT Config"; +"mqtt.clientproxy"="MQTT Client Proxy"; +"mqtt.disconnect"="Disconnect from MQTT"; +"mqtt.username"="Username"; +"name"="Name"; +"network"="Network"; +"network.config"="Network Config"; +"nodes"="Nodes"; +"nodes %@"="Nodes (%@)"; +"nodelist.filter.distance %@"="up to %@ away"; +"save.config %@"="Save Config for %@"; +"no.nodes"="No Meshtastic Nodes Found"; +"not.connected"="No device connected"; +"numbers.punctuation"="Numbers and Punctuation"; +"off"="Off"; +"offline"="Offline"; +"on.boot"="On Boot Only"; +"options"="Options"; +"password"="Password"; +"pause"="Pause"; +"paxcounter.ble"="BLE"; +"paxcounter.delete"="Delete all pax data?"; +"paxcounter.wifi"="WiFi"; +"paxcounter.content.unavailable"="No PAX Counter Logs"; +"paxcounter.log"="PAX Counter Log"; +"paxcounter.total"="Total PAX"; +"phone.gps"="Phone GPS"; +"phone.gps.interval.description"="How frequently your phone will send your location to the device, location updates to the mesh are managed by the device."; +"position"="Position"; +"position.config"="Position Config"; +"position.precision %@"="Within %@"; +"preferred.radio"="Preferred Radio"; +"radio.configuration"="Radio Configuration"; +"range.test"="Range Test"; +"range.test.blocked"="Block Range Test"; +"range.test.config"="Range Test Config"; +"reply"="Reply"; +"reboot"="Reboot"; +"reboot.node"="Reboot node?"; +"received.ack"="Received Ack"; +"received.ack.real"="Recipient Ack"; +"relativetimeofday.morning"="Morning"; +"relativetimeofday.midday"="Midday"; +"relativetimeofday.afternoon"="Afternoon"; +"relativetimeofday.evening"="Evening"; +"relativetimeofday.nighttime"="Nighttime"; +"resume"="Resume"; +"ringtone"="Ringtone"; +"ringtone.config"="Ringtone Config"; +"route.recorder"="Route Recorder"; +"routes"="Routes"; +"routes.activitytype.walking"="Walking"; +"routes.activitytype.hiking"="Hiking"; +"routes.activitytype.biking"="Biking"; +"routes.activitytype.driving"="Driving"; +"routes.activitytype.overlanding"="Overlanding"; +"routes.activitytype.skiing"="Skiing"; +"routes.activitytype.filename.walking"="walk"; +"routes.activitytype.filename.hiking"="hike"; +"routes.activitytype.filename.biking"="bike tour"; +"routes.activitytype.filename.driving"="drive"; +"routes.activitytype.filename.overlanding"="overland drive"; +"routes.activitytype.filename.skiing"="ski tour"; +"routing.acknowledged"="Acknowledged"; +"routing.noroute"="No Route"; +"routing.gotnak"="Received a negative acknowledgment"; +"routing.timeout"="Timeout"; +"routing.nointerface"="No Interface"; +"routing.maxretransmit"="Max Retransmission Reached"; +"routing.nochannel"="No Channel"; +"routing.toolarge"="The packet is too large"; +"routing.noresponse"="No Response"; +"routing.dutycyclelimit"="Regional Duty Cycle Limit Reached"; +"routing.badRequest"="Bad Request"; +"routing.notauthorized"="Not Authorized"; +"satellite"="Satellite"; +"satellite.flyover"="Satellite Flyover"; +"save"="Save"; +"save.config %@"="Save Config for %@"; +"serial"="Serial"; +"serial.config"="Serial Config"; +"serial.mode.default"="Default"; +"serial.mode.simple"="Simple"; +"serial.mode.proto"="Protobufs"; +"serial.mode.txtmsg"="Text Message"; +"serial.mode.nmea"="NMEA Positions"; +"settings"="Settings"; +"share.channels"="Share QR Code"; +"share.position"="Share Position"; +"subscribed"="Subscribed to mesh"; +"select.contact"="Select a Contact"; +"select.node"="Select a Node"; +"select.menu.item"="Select an item from the menu"; +"set.region"="Set LoRa Region"; +"standard"="Standard"; +"standard.muted"="Standard Muted"; +"start"="Start"; +"storeforward"="Store & Forward"; +"storeforward.config"="Store & Forward Config"; +"storeforward.heartbeat"="Send Heartbeat"; +"ssid"="SSID"; +"tapback"="Tapback Response"; +"tapback.heart"="Heart"; +"tapback.thumbsup"="Thumbs Up"; +"tapback.thumbsdown"="Thumbs Down"; +"tapback.haha"="HaHa"; +"tapback.exclamation"="Exclamation Mark"; +"tapback.question"="Question Mark"; +"tapback.poop"="Poop"; +"tapback.wave"="Wave"; +"telemetry"="Telemetry (Sensors)"; +"telemetry.config"="Telemetry Config"; +"timeout"="Timeout"; +"timestamp"="Timestamp"; +"tip.bluetooth.connect.title"="Connected Radio"; +"tip.bluetooth.connect.message"="Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.channel.admin.title"="Admin Channel"; +"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices."; +"tip.channels.create.title"="Manage Channels"; +"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)"; +"tip.channels.share.title"="Sharing Meshtastic Channels"; +"tip.channels.share.message"="A Meshtastic QR code contains the LoRa config and channel values needed for radios to communicate. You can share a complete channel configuration using the Replace Channels option, if you choose Add Channels your shared channels will be added to the channels on the receiving radio."; +"tip.messages.title"="Messages"; +"tip.messages.message"="You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details."; +"twitter"="Twitter"; +"unknown"="Unknown"; +"unknown.age"="Unknown Age"; +"unset"="Unset"; +"update.firmware"="Update Your Firmware"; +"update.interval"="Update Interval"; +"uptime"="Uptime"; +"user"="User"; +"user.details"="User Details"; +"voltage"="Voltage"; +"waiting"="Waiting. . ."; +"appsettings.newNodeNotifications"="New Node Notifications"; diff --git a/pt.lproj/Localizable.strings b/pt.lproj/Localizable.strings new file mode 100644 index 00000000..e12f17e4 --- /dev/null +++ b/pt.lproj/Localizable.strings @@ -0,0 +1,376 @@ +/* + Localizable.strings + Meshtastic + + Copyright(c) Garth Vander Houwen on 12/12/22. Translated from English to Portuguese by Philip Rosa-Leeke 2024 + +*/ +"about"="Sobre"; +"about.meshtastic"="Sobre Meshtastic"; +"activity"="Actividade"; +"admin"="Admin"; +"admin.log"="Log das Mensagens do Admin"; +"ago"="há"; +"airtime"="Tempo ao Ár"; +"always.on"="Sempre Ligado"; +"ambient.lighting"="Iluminação Ambiental"; +"ambient.lighting.config"="Configuração Iluminação Ambiental"; +"appsettings"="Definições do App"; +"appsettings.provide.location"="Partilha localização"; +"appsettings.smartposition"="Posição Inteligente"; +"are.you.sure"="Tem a certeza?"; +"ascii.capable"="Capacidade ASCII"; +"available.radios"="Rádios Disponíveis"; +"automatic.detection"="Deteção Automático"; +"battery.level"="Nível de Bataria"; +"ble.name"="Nome BLE"; +"ble.connection.timeout %d %@"="Falha de conexão após %d tentativas de conectar a %@. Você pode precisar esquecer seu dispositivo em Configurações > Bluetooth."; +"ble.errorcode.6 %@"="%@ O App vai reconetar automaticamente ao rádio preferido se ele voltar ao alcance."; +"ble.errorcode.14 %@"="%@ Esse erro geralmente não pode ser corrigido sem esquecer o dispositivo em Configurações > Bluetooth e reconetar ao rádio."; +"ble.errorcode.pin %@"="%@ Por favor, tente conectar novamente e verifique cuidadosamente o PIN."; +"bluetooth"="Bluetooth"; +"bluetooth.off"="Bluetooth está desligado"; +"bluetooth.config"="Configuração Bluetooth"; +"bluetooth.mode.randompin"="PIN Aleatório"; +"bluetooth.mode.fixedpin"="PIN fixo"; +"bluetooth.mode.nopin"="Sem PIN (Simplesmente Funciona)"; +"bluetooth.pairingmode"="Modo Pairing"; +"bluetooth.pin.validation"="O Pin do BLE deve ter 6 dígitos."; +"bytes"="Bytes"; +"cancel"="Cancelar"; +"canned.messages"="Mensagens Enlatados"; +"canned.messages.config"="Configuração dos Mensagens Enlatados"; +"canned.messages.preset.manual"="Configuração Manual"; +"canned.messages.preset.rakrotary"="Module Codificador do RAK Rotary"; +"canned.messages.preset.cardkb"="M5 Stack Card KB / Teclado RAK"; +"channel"="Canal"; +"channel.role.disabled"="Desativado"; +"channel.role.primary"="Primário"; +"channel.role.secondary"="Secundária"; +"channel.utilization"="Utilização do Canal"; +"channels"="Canais"; +"clear.app.data"="Apagar os dados do App"; +"clear.log"="Apagar"; +"close"="Fechar"; +"config.power.settings"="Energia"; +"config.power.title"="Configuração de Energia"; +"config.power.section.battery"="Bataria"; +"config.power.section.sleep"="Dormir"; +"config.power.adc.override"="Substituir ADC"; +"config.power.adc.multiplier"="Multiplicador"; +"config.power.ls.secs"="Intervalo de Dormir Leve"; +"config.power.min.wake.secs"="Intervalo Mínimo de Despertar"; +"config.power.saving"="Poupar a Energia"; +"config.power.saving.description"="Vai por dormir o máximo possível, para o papel do rastreador e o papel do sensor isso incluirá também o rádio lora. Não use essa configuração se deseja usar seu dispositivo com os aplicativos do telefone ou está usando um dispositivo sem um botão do usuário."; +"config.power.shutdown.on.power.loss"="Desligar em caso de Perda de Energia"; +"config.power.shutdown.after.secs"="Após"; +"config.power.wait.bluetooth.secs"="Desligar o Bluetooth Após"; +"config.ringtone"="="Toque RTTTL"; +"config.ringtone.title"="="Configuração de Toque"; +"config.ringtone.label"="="Idioma de Transferência de Toque"; +"config.ringtone.description"="="Idioma de Transferência de Toque (RTTTL) Sequência de Toque usada por campainhas suportadas em notificações externas."; +"config.module.paxcounter.settings"="="Contador de PAX"; +"config.module.paxcounter.title"="="Configuração do Contador de PAX"; +"config.module.paxcounter.enabled.description"="="Quando ativado, o módulo de Contador de PAX conta o número de pessoas que passam usando Wi-Fi e Bluetooth. Tanto o Wi-Fi quanto o Bluetooth devem estar desativados para que o contador de PAX funcione."; +"config.module.paxcounter.updateinterval"="="Intervalo de Atualização"; +"config.module.paxcounter.updateinterval.description"="="Com que frequência podemos enviar uma mensagem para a malha quando as pessoas são detectadas."; +"config.save.confirm"="="Após salvar os valores de configuração, o nó reiniciará"; +"communicating"="="Comunicando com dispositivo. ."; +"connected.radio"="="Rádio Conectado"; +"connected"="Bluetooth Connectado"; +"connecting"="="Conectando . ."; +"contacts"="Contactos"; +"contacts %@"="Contactos (%@)"; +"copy"="Copiar"; +"current"="Atual"; +"default"="Padrão"; +"delete"="Apagar"; +"detection.sensor"="Sensor de Detecção"; +"detection.sensor.config"="="Configuração do Sensor de Detecção"; +"detection.sensor.log"="Log Sensor de Detecção"; +"device"="="Dispositivo"; +"device.config"="="Configuração do Dispositivo"; +"device.configuration"="Configuração do Dispositivo"; +"device.metrics.delete"="="Apagar todas as métricas do dispositivo?"; +"device.metrics.log"="Log g de Métricas do Dispositivo"; +"device.role.client"="="Dispositivo conectado ao App ou independente para mensagens."; +"device.role.clientmute"="="Dispositivo que não encaminha pacotes de outros dispositivos."; +"device.role.clienthidden"="="Dispositivo que apenas transmite conforme necessário em modo furtivo ou economia de energia."; +"device.role.tracker"="Transmite pacotes de posição GPS como prioridade."; +"device.role.lostandfound"="="Transmite a localização como mensagem para o canal padrão regularmente para auxiliar na recuperação do dispositivo."; +"device.role.sensor"="="="Transmite pacotes de telemetria como prioridade."; +"device.role.tak"="Otimizado para comunicação do sistema ATAK, reduz transmissões rotineiras."; +"device.role.taktracker"="="="Permite transmissões automáticas de TAK PLI e reduz transmissões rotineiras."; +"device.role.repeater"="="Nó de infraestrutura para ampliar a cobertura da rede transmitindo mensagens com sobrecarga mínima. Não visível na lista de Nós."; +"device.role.router"="="Nó de infraestrutura para ampliar a cobertura da rede transmitindo mensagens. Visível na lista de Nós."; +"device.role.routerclient"="="Combinação de ROTEADOR e CLIENTE. Não para dispositivos móveis."; +"direct.messages"="Mensagens Directas"; +"dismiss.keyboard"="Dispensar"; +"display"="Icrã"; +"display.config"="Configuração do Icrãn"; +"distance"="Distância"; +"disconnect"="Desconectar"; +"echo"="Eco"; +"email.address"="Endereço de Email"; +"enabled"="Activado"; +"encrypted"="Encriptado"; +"export"="Exportar"; +"external.notification"="Notificação Externa"; +"external.notification.config"="Configuração de Notificação Externa"; +"finish"="Terminar"; +"firmware.version"="Versão do Firmware"; +"firmware.version.unsupported"=" "Versão de Firmware não suportada detetada, impossível conectar ao dispositivo."; +"gas"="Gas"; +"gas.resistance"=" "Resistência ao Gas"; +"generate.qr.code"=" "Gerar Código QR"; +"gpsformat.dec"=" "Formato de Graus Decimais"; +"gpsformat.dms"=" "Graus Minutos Segundos"; +"gpsformat.utm"="Universal Transverse Mercator"; +"gpsformat.mgrs"=" "Sistema de Referência de Grelha Militar"; +"gpsformat.olc"=" "Código de Localização Aberto (também conhecido como Plus Codes)"; +"gpsformat.osgr"=" "Referência de Grelha da Ordnance Survey"; +"gpsmode.disabled"=" "Desativado"; +"gpsmode.enabled"=" "Ativado"; +"gpsmode.notPresent"="Não Presente"; +"heard"="Ouvido"; +"heard.last"="Último Ouvido"; +"hybrid"="Híbrido"; +"hybrid.flyover"="Híbrido o de Sobrevoo"; +"include"="Incluir"; +"inputevent.none"="Nenhum"; +"inputevent.up"="Para Cima"; +"inputevent.down"="Para Baixo"; +"inputevent.left"="Esquerda"; +"inputevent.right"="Direita"; +"inputevent.select"="Selecionar"; +"inputevent.back"="Voltar"; +"inputevent.cancel"="Cancelar"; +"interval.one.second"="Um Segundo"; +"interval.two.seconds"="Dois Segundos"; +"interval.three.seconds"="Três Segundos"; +"interval.four.seconds"="Quatro Segundos"; +"interval.five.seconds"="Cinco Segundos"; +"interval.ten.seconds"="Dez Segundos"; +"interval.fifteen.seconds"="Quinze Segundos"; +"interval.twenty.seconds"="Vinte Segundos"; +"interval.twentyfive.seconds"="Vinte e Cinco Segundos"; +"interval.thirty.seconds"="Trinta Segundos"; +"interval.fortyfive.seconds"="Quarenta e Cinco Segundos"; +"interval.one.minute"="Um Minuto"; +"interval.two.minutes"="Dois Minutos"; +"interval.five.minutes"="Cinco Minutos"; +"interval.ten.minutes"="Dez Minutos"; +"interval.fifteen.minutes"="Quinze Minutos"; +"interval.thirty.minutes"="Trinta Minutos"; +"interval.one.hour"="Uma Hora"; +"interval.two.hours"="Duas Horas"; +"interval.three.hours"="Três Horas"; +"interval.four.hours"="Quatro Horas"; +"interval.five.hours"="Cinco Horas"; +"interval.six.hours"="Seis Horas"; +"interval.twelve.hours"="Doze Horas"; +"interval.eighteen.hours"="Dezoito Horas"; +"interval.twentyfour.hours"="Vinte e Quatro Horas"; +"interval.thirtysix.hours"="Trinta e Seis Horas"; +"interval.fortyeight.hours"="Quarenta e Oito Horas"; +"interval.seventytwo.hours"="Setenta e Duas Horas"; +"keyboard.type"="Tipo de Teclado"; +"logging"="Registo"; +"lora"="LoRa"; +"lora.config"="Configuração LoRa"; +"map"="Mapa do Mesh"; +"map.type"="Tipo Padrão"; +"map.centering"=" "Modo de Centralização"; +"map.tiles.delete"="Apagar Todas as Imagens da Mapa"; +"map.recentering"=" "Re-centralização Automática"; +"map.use.legacy"=" "Utilizar Mapa do Mesh Antigo"; +"map.usertrackingmode"=" "Modo de Rastreamento do Utilizador"; +"map.usertrackingmode.follow"=" "Seguir"; +"map.usertrackingmode.followwithheading"=" "Seguir com Direção"; +"map.usertrackingmode.none"="Nenhum"; +"mesh.live.activity"=" "Atividade Ao Vivo do Mesh"; +"mesh.log"="Log do Mesh"; +"mesh.log.ambientlighting.config %@"=" "Configuração do módulo de Iluminação Ambiente recebida: %@"; +"mesh.log.bluetooth.config %@"=" "Configuração Bluetooth recebida: %@"; +"mesh.log.cannedmessage.config %@"=" "Configuração do módulo de Mensagens Padrão recebida: %@"; +"mesh.log.cannedmessages.messages.get %@"=" "Mensagens Padrão solicitadas para o módulo de mensagens para o nó: %@"; +"mesh.log.cannedmessages.messages.received %@"=" "Mensagens Padrão recebidas para: %@"; +"mesh.log.channel.sent %@ %d"=" "Um Canal Enviado para: %@ Índice do Canal %d"; +"mesh.log.channel.received %d %@"=" "Canal %d recebido de: %@"; +"mesh.log.device.config %@"=" "Configuração do dispositivo recebida: %@"; +"mesh.log.display.config %@"=" "Configuração do icrãn recebida: %@"; +"mesh.log.devicemetadata %@"=" "Solicitando os Metadados do Dispositivo para %@"; +"mesh.log.device.metadata.received %@"=" "Os Metadados do dispositivo recebidos de: %@"; +"mesh.log.detectionsensor.config %@"=" "Configuração do módulo de sensor de detecção recebida: %@"; +"mesh.log.externalnotification.config %@"=" "Configuração do módulo de notificação externa recebida: %@"; +"mesh.log.lora.config %@"=" "Configuração LoRa recebida: %@"; +"mesh.log.lora.config.sent %@"="Configuração do LoRa Enviado para: %@"; +"mesh.log.mqtt.config %@"=" "Configuração do módulo MQTT recebida: %@"; +"mesh.log.myinfo %@"="MyInfo recebido: %@"; +"mesh.log.network.config %@"=" "Configuração de rede recebida: %@"; +"mesh.log.nodeinfo.received %@"=" "Informações do nó recebidas para: %@"; +"mesh.log.paxcounter %@"=" "Mensagem do Contador PAX recebida de: %@"; +"mesh.log.paxcounter.config %@"=" "Configuração do Contador PAX recebida: %@"; +"mesh.log.position.config %@"=" "Configuração de posição recebida: %@"; +"mesh.log.position.received %@"=" "Pacote de posição recebido do nó: %@"; +"mesh.log.power.config %@"=" "Configuração de energia recebida: %@"; +"mesh.log.rangetest.config %@"=" "Configuração do módulo de teste de alcance recebida: %@"; +"mesh.log.ringtone.config %@"=" "Configuração de toque RTTTL recebida: %@"; +"mesh.log.routing.message %@ %@"=" "Roteamento recebido para RequestID: %@ Estado de Ack: %@"; +"mesh.log.serial.config %@"=" "Configuração do módulo serial recebida: %@"; +"mesh.log.sharelocation %@"=" "Enviado um Pacote de Posição do GPS do dispositivo Apple para o nó: %@"; +"mesh.log.storeforward.config %@"=" "Configuração do módulo Store & Forward recebida: %@"; +"mesh.log.telemetry.config %@"=" "Configuração do módulo de telemetria recebida: %@"; +"mesh.log.telemetry.received %@"=" "Telemetria recebida para: %@"; +"mesh.log.textmessage.received"=" "Mensagem recebida do App de mensagem de texto."; +"mesh.log.textmessage.send.failed %@"=" "Falha no envio da mensagem, não conectado corretamente a %@"; +"mesh.log.textmessage.sent %@ %@ %@"=" "Mensagem enviada %@ de %@ para %@"; +"mesh.log.traceroute.received.direct %@"=" "Solicitação de Rastreamento enviada para o nó: %@" foi recebida diretamente."; +"mesh.log.traceroute.received.route %@"=" "Solicitação de Rastreamento retornada: %@"; +"mesh.log.traceroute.sent %@"=" "Enviei uma solicitação de Rastreamento para o nó: %@"; +"mesh.log.wantconfig %@"=" "Emitindo Configuração Desejada para %@"; +"mesh.log.waypoint.sent %@"=" "Enviado um Pacote de Ponto de Referência de: %@"; +"mesh.log.waypoint.received %@"=" "Pacote de Ponto de Referência recebido do nó: %@"; +"message"="Mensagem"; +"message.details"="Dados de Mensagem"; +"messages"="Mensagens"; +"mode"="Modo"; +"module.configuration"="Configuração do Módulo"; +"mqtt"="MQTT"; +"mqtt.connect"="Conectar ao MQTT"; +"mqtt.config"="Configuração MQTT"; +"mqtt.clientproxy"=" "Proxy do Cliente MQTT"; +"mqtt.disconnect"=" "Desconectar do MQTT"; +"mqtt.username"=" "Nome de Utilizador"; +"name"="Nome"; +"network"="Rede"; +"network.config"="Configuração de Rede"; +"nodes"="Nós"; +"nodes %@"="Nós (%@)"; +"nodelist.filter.distance %@"="até %@ de distância"; +"save.config %@"="Salvar Configuração para %@"; +"no.nodes"="Nenhum Nó Meshtastic Encontrado"; +"not.connected"=" "Nenhum dispositivo conectado"; +"numbers.punctuation"=" "Números e Pontuação"; +"off"="Desligado"; +"offline"="Offline"; +"on.boot"=" "No arranque"; +"options"="Opções"; +"password"="Senha"; +"pause"="Pausa"; +"paxcounter.ble"="BLE"; +"paxcounter.delete"=" "Apagar todos os dados de pax?"; +"paxcounter.wifi"="WiFi"; +"paxcounter.content.unavailable"=" "Nenhum Log do Contador PAX Disponível"; +"paxcounter.log"=" "Log do Contador PAX"; +"paxcounter.total"="Total de PAX"; +"phone.gps"="GPS do Telefone"; +"phone.gps.interval.description"=" "Com que frequência seu telefone enviará sua localização para o dispositivo, as atualizações de localização no mesh são geridas pelo dispositivo."; +"position"="Posição"; +"position.config"="Configuração de Posição"; +"position.precision %@"="Dentro de %@"; +"preferred.radio"="Rádio Preferido"; +"radio.configuration"="Configuração de Rádio"; +"range.test"="Teste de Alcance"; +"range.test.blocked"=" "Bloquear Teste de Alcance"; +"range.test.config"="Configuração do teste de Alcance"; +"reply"="Responder"; +"reboot"="Reiniciar"; +"reboot.node"="Reiniciar nó?"; +"received.ack"="Ack Recebido"; +"received.ack.real"="Ack do Destinário"; +"relativetimeofday.morning"="Manhã"; +"relativetimeofday.midday"="Meio-dia"; +"relativetimeofday.afternoon"="Tarde"; +"relativetimeofday.evening"="Noite"; +"relativetimeofday.nighttime"="Noite"; +"resume"="Continuar"; +"ringtone"="Toque"; +"ringtone.config"="Configuração de Toque"; +"route.recorder"="Gravador de Rotas"; +"routes"="Rotas"; +"routes.activitytype.walking"="Caminhada"; +"routes.activitytype.hiking"="Caminhada na Montanha"; +"routes.activitytype.biking"=" "Passeio de Bicicleta"; +"routes.activitytype.driving"="Conduzir"; +"routes.activitytype.overlanding"="Overlanding"; +"routes.activitytype.skiing"="Esqui"; +"routes.activitytype.filename.walking"="Caminhar"; +"routes.activitytype.filename.hiking"="Caminhar na Montanha"; +"routes.activitytype.filename.biking"="Passeio de Bicicleta"; +"routes.activitytype.filename.driving"="Conduzir"; +"routes.activitytype.filename.overlanding"="Caminhar overland"; +"routes.activitytype.filename.skiing"="Passeio de esqui"; +"routing.acknowledged"=" "Reconhecido"; +"routing.noroute"="Sem Rota"; +"routing.gotnak"=" "Recebido um reconhecimento negativo"; +"routing.timeout"=" "Tempo Esgotado"; +"routing.nointerface"="Sem Interface"; +"routing.maxretransmit"=" "Máximo de Retransmissão Alcançado"; +"routing.nochannel"="Sem Canal"; +"routing.toolarge"="O pacote é grande de mais"; +"routing.noresponse"="Sem Resposta"; +"routing.dutycyclelimit"="O limite do Regional Duty Cycle foi abrangido"; +"routing.badRequest"="="Pedido Ruim"; +"routing.notauthorized"="Não Autorizado"; +"satellite"="Satéllite"; +"satellite.flyover"="Passagem de Satélite"; +"save"="Salvar"; +"save.config %@"="Salvar a Configuração para %@"; +"serial"="Serial"; +"serial.config"="Configuração Serial"; +"serial.mode.default"="Padrão"; +"serial.mode.simple"="Simples"; +"serial.mode.proto"="Protobufs"; +"serial.mode.txtmsg"="Mensagem de Texto"; +"serial.mode.nmea"="Posições NMEA"; +"settings"="Definições"; +"share.channels"="Partilhar o Código do QR"; +"share.position"="Partilhar o Posição"; +"subscribed"="Inscrito no mesh"; +"select.contact"="Seleciona a Contacto"; +"select.node"="Seleciona a Nó"; +"select.menu.item"="Seleciona um opção do menu"; +"set.region"="Seleciona o Região da LoRa"; +"standard"="Padrão"; +"standard.muted"="Padrão Silenciado"; +"start"="Iniciar"; +"storeforward"="="Armazenar e Encaminhar"; +"storeforward.config"="Configuração de Armazenar e Encaminhar"; +"storeforward.heartbeat"="="Enviar Batimento Cardíaco"; +"ssid"="SSID"; +"tapback"="Resposta Tapback"; +"tapback.heart"="Coração"; +"tapback.thumbsup"="="Polegar para Cima"; +"tapback.thumbsdown"="Polegar para Baixo"; +"tapback.haha"="HaHa"; +"tapback.exclamation"="Ponto de Exclamação"; +"tapback.question"="Ponto de Interrogação"; +"tapback.poop"="Cocó"; +"tapback.wave"="Adeus"; +"telemetry"="Telemetria (Sensores)"; +"telemetry.config"="Configuração Telemetria"; +"timeout"="Tempo Limite"; +"timestamp"="Carimbo de Data/Hora"; +"tip.bluetooth.connect.title"="Rádio Conectado"; +"tip.bluetooth.connect.message"="="Mostra informações para o rádio LoRa conectado via bluetooth. Você pode deslizar para a esquerda para desconectar o rádio e pressionar por um longo período para ver estatísticas ou iniciar a atividade ao vivo."; +"tip.channel.admin.title"="="Canal de Administração"; +"tip.channel.admin.message"="="Canal de administração detectado: Selecione um nó do menu suspenso para gerir dispositivos conectados ou remotos."; +"tip.channels.create.title"="="Gerir Canais"; +"tip.channels.create.message"="="A maioria dos dados na sua malha é enviada pelo canal principal. Você pode configurar canais secundários para criar grupos de mensagens adicionais protegidos por sua própria chave. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)"; +"tip.channels.share.title"="="Compartilhando Canais Meshtastis"; +"tip.channels.share.message"="="Um código QR Meshtastic contém a configuração LoRa e os valores do canal necessários para os rádios se comunicarem. Você pode compartilhar uma configuração completa do canal usando a opção Substituir Canais; se você escolher Adicionar Canais, seus canais compartilhados serão adicionados aos canais no rádio receptor."; +"tip.messages.title"="Mensagens"; +"tip.messages.message"="="Você pode enviar e receber mensagens de canal (conversas em grupo) e mensagens diretas. De qualquer mensagem, você pode pressionar por um longo período para ver ações disponíveis como copiar, responder, tapback e excluir, bem como detalhes de entrega."; +"twitter"="Twitter"; +"unknown"="Desconhecido"; +"unknown.age"="Idade Desconhecido"; +"unset"="="Não Definido"; +"update.firmware"="Atualiza o Seu Firmware"; +"update.interval"="Intervalo de Atualização"; +"uptime"="Tempo No Ár"; +"user"="Utilizador"; +"user.details"="Dados do Utilizador"; +"voltage"="Tensão"; +"waiting"="À Espara. . ."; +"appsettings.newNodeNotifications"="Notificações de Nó Novo"; diff --git a/se.lproj/Localizable.strings b/se.lproj/Localizable.strings index b181a2a3..e45ca2fe 100644 --- a/se.lproj/Localizable.strings +++ b/se.lproj/Localizable.strings @@ -137,6 +137,7 @@ "hybrid"="Hybrid"; "hybrid.flyover"="Hybrid Flygöversikt"; "include"="Inkludera"; +"incomplete"="Incomplete"; "inputevent.none"="Ingen"; "inputevent.up"="Upp"; "inputevent.down"="Ner"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index d0371670..5606f44b 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -133,6 +133,7 @@ "hybrid"="混合"; "hybrid.flyover"="混合视图"; "include"="包含"; +"incomplete"="Incomplete"; "inputevent.none"="无"; "inputevent.up"="上"; "inputevent.down"="下"; diff --git a/zh-Hant-TW.lproj/Localizable.strings b/zh-Hant-TW.lproj/Localizable.strings index 5ec48114..1665f916 100644 --- a/zh-Hant-TW.lproj/Localizable.strings +++ b/zh-Hant-TW.lproj/Localizable.strings @@ -3,7 +3,8 @@ Meshtastic Created by BM6HIP on 2024/3/2 - + Updated by Oliver on 2024/5/9 + */ "about"="關於"; "about.meshtastic"="關於 Meshtastic"; @@ -13,13 +14,13 @@ "ago"="ago"; "airtime"="廣播時間"; "always.on"="常亮"; -"ambient.lighting"="Ambient Lighting"; -"ambient.lighting.config"="Ambient Lighting Config"; +"ambient.lighting"="環境照明"; +"ambient.lighting.config"="環境照明設定"; "appsettings"="設定"; "appsettings.provide.location"="提供定位到 Mesh 網路"; -"appsettings.smartposition"="Smart Position"; +"appsettings.smartposition"="智能定位"; "are.you.sure"="是否確定?"; -"ascii.capable"="ASCII Capable"; +"ascii.capable"="支援 ASCII"; "available.radios"="可以連接的設備"; "automatic.detection"="自動識別"; "battery.level"="電池電量"; @@ -36,9 +37,9 @@ "bluetooth.mode.nopin"="不使用 PIN 碼(直接配對)"; "bluetooth.pairingmode"="配對模式"; "bluetooth.pin.validation"="藍芽 PIN 碼必須是 6 位數字。"; -"bytes"="字節"; +"bytes"="位元組"; "cancel"="取消"; -"canned.messages"="通知"; +"canned.messages"="罐頭訊息"; "canned.messages.config"="通知設定"; "canned.messages.preset.manual"="手動設定"; "canned.messages.preset.rakrotary"="RAK 旋轉編碼器"; @@ -56,24 +57,24 @@ "config.power.title"="電源設定"; "config.power.section.battery"="電池"; "config.power.section.sleep"="休眠"; -"config.power.adc.override"="ADC Override"; -"config.power.adc.multiplier"="Multiplier"; -"config.power.ls.secs"="Light Sleep Interval"; +"config.power.adc.override"="ADC校正"; +"config.power.adc.multiplier"="修正倍數"; +"config.power.ls.secs"="輕度休眠間隔"; "config.power.min.wake.secs"="最小的喚醒間隔時間"; "config.power.saving"="省電模式"; -"config.power.saving.description"="Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button."; +"config.power.saving.description"="為了追蹤器和感測器的角色,這將包括將 LoRa 無線電設備盡可能地進入睡眠模式。如果您想要使用手機應用程式操作您的設備,或者使用沒有用戶按鈕的設備,請不要使用此設定。"; "config.power.shutdown.on.power.loss"="失去電源後關機"; "config.power.shutdown.after.secs"="之後"; "config.power.wait.bluetooth.secs"="等待藍芽"; -"config.ringtone"="RTTTL Ringtone"; +"config.ringtone"="RTTTL 鈴聲"; "config.ringtone.title"="鈴聲"; -"config.ringtone.label"="Ringtone Transfer Language"; -"config.ringtone.description"="Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications."; -"config.module.paxcounter.settings"="PAX Counter"; -"config.module.paxcounter.title"="PAX Counter Config"; -"config.module.paxcounter.enabled.description"="When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be disabled for PAX counter to work."; -"config.module.paxcounter.updateinterval"="Update Interval"; -"config.module.paxcounter.updateinterval.description"="How often we can send a message to the mesh when people are detected."; +"config.ringtone.label"="鈴聲傳輸語言(RTTTL)"; +"config.ringtone.description"="RTTTL 鈴聲字串(Ringtone Transfer Language)被用於外部通知中支援的蜂鳴器。"; +"config.module.paxcounter.settings"="人流計數器"; +"config.module.paxcounter.title"="人流計數器設定"; +"config.module.paxcounter.enabled.description"="啟用後,人流計數器模組將透過 WiFi 和藍牙計算經過的人數。必須停用 WiFi 和藍牙才能讓 PAX 計數器正常工作。"; +"config.module.paxcounter.updateinterval"="更新時間間隔"; +"config.module.paxcounter.updateinterval.description"="當檢測到人員時,我們可以多久發送一次訊息到網狀網路。"; "config.save.confirm"="電台將會在設定儲存後重啟。"; "connected.radio"="已連接的電台"; "communicating"="與電台進行通訊中..."; @@ -93,15 +94,15 @@ "device.metrics.log"="電台指標紀錄檔"; "device.role.client"="標準模式 - App 可以連接到電台進行收發操作,並且會自動轉發 Mesh 網路中其他中繼點的消息。"; "device.role.clientmute"="靜音模式 - 與標準模式類似,App 可以連接到電台進行收發操作,但不會轉發 Mesh 網路中其他中繼點的消息。"; -"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption."; -"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\""; -"device.role.router"="纯路由模式 - 自動轉發 Mesh 網路中其他中繼點的消息,中繼模式下螢幕會熄滅,Wi-Fi 和藍芽將會進入睡眠模式,App 將無法連接到電台進行收發操作。"; +"device.role.clienthidden"="隱藏模式 - 用於那些\"只在被問到時才回答\"的節點,關閉所有常規廣播,但允許臨時通訊。依然會進行轉播,但只在本地轉播模式下進行(僅限已知的網狀網路)。可以用於私密操作或顯著減少空中時間/功耗。"; +"device.role.lostandfound"="遺失物模式 - 用於自動頻繁地向網狀網路發送一條包含設備當前位置的短信:\"I'm lost! Position: lat / long\""; +"device.role.router"="纯路由模式 - 自動轉發 Mesh 網路中其他中繼點的消息,中繼模式下螢幕會熄滅,Wi-Fi 和藍芽將會進入睡眠模式,App 將無法連接到電台進行收發操作。"; "device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼點的消息,App 也可以連接到電台進行收發操作。"; "device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。"; "device.role.tracker"="追蹤模式 - 用於作為 GPS 追蹤器。從該設備發送的定位數據包優先級較高,每兩分鐘廣播一次。智能位置廣播預設為關閉。"; -"device.role.sensor"="Broadcasts telemetry packets as priority."; -"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; -"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts."; +"device.role.sensor"="傳感器模式 - 優先廣播傳感器數據包"; +"device.role.tak"="TAK模式 - 優化了 ATAK 系統通訊,減少常規廣播。"; +"device.role.taktracker"="TAK TRACKER追蹤器 - 啟用自動 TAK PLI 廣播並減少常規廣播。"; "direct.messages"="聊天"; "dismiss.keyboard"="隱藏鍵盤"; "display"="螢幕(電台螢幕)"; @@ -112,7 +113,7 @@ "email.address"="電子信箱"; "enabled"="啟用"; "encrypted"="加密"; -"export"="Export"; +"export"="匯出"; "external.notification"="外部通知"; "external.notification.config"="外部通知設定"; "finish"="完成"; @@ -124,14 +125,15 @@ "gpsformat.dec"="十進制"; "gpsformat.dms"="度分秒"; "gpsformat.utm"="通用橫軸墨卡托投影"; -"gpsformat.mgrs"="軍事網格系統"; -"gpsformat.olc"="開放的位置代碼(又稱加碼)"; -"gpsformat.osgr"="英國國土測量局網格"; +"gpsformat.mgrs"="軍事網格參考系統"; +"gpsformat.olc"="開放位置代碼"; +"gpsformat.osgr"="英國國土測量局網格參考系統"; "heard"="收到"; "heard.last"="最後收到"; "hybrid"="混合"; "hybrid.flyover"="混合視圖"; "include"="包含"; +"incomplete"="Incomplete"; "inputevent.none"="無"; "inputevent.up"="上"; "inputevent.down"="下"; @@ -174,15 +176,15 @@ "lora"="LoRa"; "lora.config"="LoRa 設定"; "map"="Mesh 地圖"; -"map.centering"="居中"; +"map.centering"="置中"; "map.tiles.delete"="刪除已緩存的地圖區塊"; "map.recentering"="自動重新居中"; -"map.use.legacy"="Use Legacy Mesh Map"; +"map.use.legacy"="使用傳統Mesh地圖"; "map.type"="地圖類型"; "map.usertrackingmode"="使用者跟隨模式"; "map.usertrackingmode.none"="無"; "map.usertrackingmode.follow"="跟隨"; -"map.usertrackingmode.followwithheading"="Follow with heading"; +"map.usertrackingmode.followwithheading"="跟隨與方向"; "mesh.live.activity"="Mesh 即時活動"; "mesh.log"="Mesh 紀錄檔"; "mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; @@ -194,53 +196,53 @@ "mesh.log.channel.received %d %@"="Channel %d received from: %@"; "mesh.log.device.config %@"="收到裝置設定: %@"; "mesh.log.display.config %@"="收到顯示模組設定: %@"; -"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@"; -"mesh.log.device.metadata.received %@"="Device Metadata admin message received from: %@"; -"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@"; -"mesh.log.externalnotification.config %@"="External Notification module config received: %@"; +"mesh.log.devicemetadata %@"="請求設備元數據:%@"; +"mesh.log.device.metadata.received %@"="從 %@ 收到設備元數據管理消息"; +"mesh.log.detectionsensor.config %@"="收到偵測感應器模組配置:%@"; +"mesh.log.externalnotification.config %@"="收到外部通知模組配置:%@"; "mesh.log.lora.config %@"="收到LoRa設定: %@"; -"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@"; -"mesh.log.mqtt.config %@"="MQTT module config received: %@"; -"mesh.log.myinfo %@"="MyInfo received: %@"; +"mesh.log.lora.config.sent %@"="發送LoRa配置給:%@"; +"mesh.log.mqtt.config %@"="收到MQTT模組配置:%@"; +"mesh.log.myinfo %@"="收到我的資訊:%@"; "mesh.log.network.config %@"="收到網路設定: %@"; "mesh.log.nodeinfo.received %@"="收到中繼點訊息: %@"; -"mesh.log.paxcounter %@"="PAX Counter message received for: %@"; -"mesh.log.position.config %@"="Positon config received: %@"; +"mesh.log.paxcounter %@"="為 %@ 收到PAX計數器消息"; +"mesh.log.position.config %@"="收到位置配置:%@"; "mesh.log.position.received %@"="從中繼點接收到定位封包: %@"; "mesh.log.rangetest.config %@"="收到拉距測試模組設定: %@"; -"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; -"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; -"mesh.log.serial.config %@"="Serial module config received: %@"; +"mesh.log.ringtone.config %@"="收到RTTTL鈴聲配置:%@"; +"mesh.log.routing.message %@ %@"="為請求ID: %@ 收到路由 Ack狀態: %@"; +"mesh.log.serial.config %@"="收到串列模組配置:%@"; "mesh.log.sharelocation %@"="傳送iOS裝置的GPS定位封包到中繼點上: %@"; -"mesh.log.storeforward.config %@"="Store & Forward module config received: %@"; +"mesh.log.storeforward.config %@"="收到儲存與轉發模組配置:%@"; "mesh.log.telemetry.config %@"="收到遠測模組設定: %@"; "mesh.log.telemetry.received %@"="收到遠測資料: %@"; -"mesh.log.textmessage.received"="Message received from the text message app."; +"mesh.log.textmessage.received"="從文字消息應用程序收到消息。"; "mesh.log.textmessage.send.failed %@"="訊息傳送失敗, 沒有正確連接到 %@"; "mesh.log.textmessage.sent %@ %@ %@"="傳送訊息 %@ 從 %@ 到 %@"; -"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly."; -"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@"; -"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; -"mesh.log.wantconfig %@"="Issuing Want Config to %@"; -"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; -"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; +"mesh.log.traceroute.received.direct %@"="直接收到發送至節點的追蹤路由請求:%@"; +"mesh.log.traceroute.received.route %@"="返回的追蹤路由請求:%@"; +"mesh.log.traceroute.sent %@"="發送追蹤路由請求至節點:%@"; +"mesh.log.wantconfig %@"="對 %@ 發出配置請求"; +"mesh.log.waypoint.sent %@"="從 %@ 發送航點封包"; +"mesh.log.waypoint.received %@"="從節點收到航點封包:%@"; "message"="訊息"; "message.details"="詳細訊息"; "messages"="訊息"; "mode"="模式"; "module.configuration"="模塊設定"; "mqtt"="MQTT"; -"mqtt.connect"="Connect to MQTT"; +"mqtt.connect"="連線到 MQTT"; "mqtt.config"="MQTT 設定"; "mqtt.clientproxy"="MQTT 客户端代理"; -"mqtt.disconnect"="Disconnect from MQTT"; +"mqtt.disconnect"="是否與 MQTT 連接"; "mqtt.username"="用戶名稱"; "name"="名稱"; "network"="網路"; "network.config"="網路設定"; "nodes"="中繼點"; "nodes %@"="中繼點 (%@)"; -"nodelist.filter.distance %@"="up to %@ away"; +"nodelist.filter.distance %@"="距離達 %@ 以內"; "no.nodes"="未找到 Meshtastic 中繼點"; "not.connected"="未連接到電台"; "numbers.punctuation"="數字和標點符號"; @@ -278,7 +280,7 @@ "routing.nochannel"="没有頻道"; "routing.toolarge"="數據包過大"; "routing.noresponse"="無回應"; -"routing.dutycyclelimit"="已達到物錢區域循環週期發射上限"; +"routing.dutycyclelimit"="已達到頻道占用循環週期發射上限"; "routing.badRequest"="錯誤請求"; "routing.notauthorized"="未授權"; "satellite"="衛星"; @@ -315,15 +317,15 @@ "tapback.exclamation"="驚嘆號"; "tapback.question"="問號"; "tapback.poop"="便便"; -"tapback.wave"="Wave"; +"tapback.wave"="招手"; "telemetry"="遠測(傳感器)"; "telemetry.config"="遠側設定"; "timeout"="超時"; "timestamp"="時間戳記"; "tip.bluetooth.connect.title"="連接到 LoRa 電台"; "tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 電台的信息。您可以向左滑動斷開電台,長按查看統計訊息或開始即時活動。"; -"tip.channel.admin.title"="Admin Channel"; -"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices."; +"tip.channel.admin.title"="管理頻道"; +"tip.channel.admin.message"="偵測到管理頻道:從下拉選單中選擇一個節點來管理連接或遠端設備。"; "tip.channels.create.title"="管理頻道"; "tip.channels.create.message"="現在 Mesh 上的資料會通過主通道發送。您可以設定輔助通道來建立由自己的金鑰保護的其他訊息組 [頻道設定提示](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="共享 Meshtastic 頻道"; @@ -340,4 +342,4 @@ "user.details"="使用者資料"; "voltage"="電壓"; "waiting"="等待中..."; -"appsettings.newNodeNotifications"="New Node Notifications"; +"appsettings.newNodeNotifications"="新節點通知"; diff --git a/zh-TW.lproj/Localizable.strings b/zh-TW.lproj/Localizable.strings index f25fc223..3c2aada3 100644 --- a/zh-TW.lproj/Localizable.strings +++ b/zh-TW.lproj/Localizable.strings @@ -3,29 +3,31 @@ Meshtastic Created by BM6HIP on 2024/3/2 - + Updated by Oliver on 2024/5/9 + */ "about"="關於"; "about.meshtastic"="關於 Meshtastic"; +"activity"="Activity"; "admin"="管理員"; "admin.log"="管理員消息紀錄檔"; "ago"="ago"; "airtime"="廣播時間"; "always.on"="常亮"; -"ambient.lighting"="Ambient Lighting"; -"ambient.lighting.config"="Ambient Lighting Config"; +"ambient.lighting"="環境照明"; +"ambient.lighting.config"="環境照明設定"; "appsettings"="設定"; "appsettings.provide.location"="提供定位到 Mesh 網路"; -"appsettings.smartposition"="Smart Position"; +"appsettings.smartposition"="智能定位"; "are.you.sure"="是否確定?"; -"ascii.capable"="ASCII Capable"; +"ascii.capable"="支援 ASCII"; "available.radios"="可以連接的設備"; "automatic.detection"="自動識別"; "battery.level"="電池電量"; "ble.name"="藍芽名稱"; -"ble.connection.timeout %d %@"="嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該設備。"; -"ble.errorcode.6 %@"="%@ 如果在首選裝置的旁邊,App 將會自動重連。"; -"ble.errorcode.14 %@"="%@ 這個錯誤通常無法自動修復,你需要在系統設定的藍芽選項中忽略該裝置並重新配對。"; +"ble.connection.timeout %d %@"="嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該電台。"; +"ble.errorcode.6 %@"="%@ 如果在首選電台的旁邊,App 將會自動重連。"; +"ble.errorcode.14 %@"="%@ 這個錯誤通常無法自動修復,你需要在系統設定的藍芽選項中忽略該電台並重新配對。"; "ble.errorcode.pin %@"="%@ 請再次嘗試連接並仔細檢查 PIN 碼。"; "bluetooth"="藍芽"; "bluetooth.off"="藍芽已關閉"; @@ -35,9 +37,9 @@ "bluetooth.mode.nopin"="不使用 PIN 碼(直接配對)"; "bluetooth.pairingmode"="配對模式"; "bluetooth.pin.validation"="藍芽 PIN 碼必須是 6 位數字。"; -"bytes"="字節"; +"bytes"="位元組"; "cancel"="取消"; -"canned.messages"="通知"; +"canned.messages"="罐頭訊息"; "canned.messages.config"="通知設定"; "canned.messages.preset.manual"="手動設定"; "canned.messages.preset.rakrotary"="RAK 旋轉編碼器"; @@ -55,27 +57,27 @@ "config.power.title"="電源設定"; "config.power.section.battery"="電池"; "config.power.section.sleep"="休眠"; -"config.power.adc.override"="ADC Override"; -"config.power.adc.multiplier"="Multiplier"; -"config.power.ls.secs"="Light Sleep Interval"; +"config.power.adc.override"="ADC校正"; +"config.power.adc.multiplier"="修正倍數"; +"config.power.ls.secs"="輕度休眠間隔"; "config.power.min.wake.secs"="最小的喚醒間隔時間"; "config.power.saving"="省電模式"; -"config.power.saving.description"="將會盡可能的進入休眠,追蹤器模式和感測器模式將會包含在內"; +"config.power.saving.description"="為了追蹤器和感測器的角色,這將包括將 LoRa 無線電設備盡可能地進入睡眠模式。如果您想要使用手機應用程式操作您的設備,或者使用沒有用戶按鈕的設備,請不要使用此設定。"; "config.power.shutdown.on.power.loss"="失去電源後關機"; "config.power.shutdown.after.secs"="之後"; "config.power.wait.bluetooth.secs"="等待藍芽"; -"config.ringtone"="RTTTL Ringtone"; +"config.ringtone"="RTTTL 鈴聲"; "config.ringtone.title"="鈴聲"; -"config.ringtone.label"="Ringtone Transfer Language"; -"config.ringtone.description"="支援外部通知的蜂鳴器所使用的 RTTTL(Ringtone Transfer Language)鈴聲字串"; -"config.module.paxcounter.settings"="PAX Counter"; -"config.module.paxcounter.title"="PAX Counter Config"; -"config.module.paxcounter.enabled.description"="啟用 PAX 計數器模組後,將使用 WiFi 和藍牙計算經過的人數。PAX 計數器需要同時啟用 WiFi 和藍牙才能正常運作"; -"config.module.paxcounter.updateinterval"="更新間隔"; -"config.module.paxcounter.updateinterval.description"="How often we can send a message to the mesh when people are detected."; -"config.save.confirm"="裝置將會在設定儲存後重啟。"; -"connected.radio"="已連接的裝置"; -"communicating"="與裝置進行通訊中..."; +"config.ringtone.label"="鈴聲傳輸語言(RTTTL)"; +"config.ringtone.description"="RTTTL 鈴聲字串(Ringtone Transfer Language)被用於外部通知中支援的蜂鳴器。"; +"config.module.paxcounter.settings"="人流計數器"; +"config.module.paxcounter.title"="人流計數器設定"; +"config.module.paxcounter.enabled.description"="啟用後,人流計數器模組將透過 WiFi 和藍牙計算經過的人數。必須停用 WiFi 和藍牙才能讓 PAX 計數器正常工作。"; +"config.module.paxcounter.updateinterval"="更新時間間隔"; +"config.module.paxcounter.updateinterval.description"="當檢測到人員時,我們可以多久發送一次訊息到網狀網路。"; +"config.save.confirm"="電台將會在設定儲存後重啟。"; +"connected.radio"="已連接的電台"; +"communicating"="與電台進行通訊中..."; "connected"="已連接"; "connecting"="連接中..."; "contacts"="聯絡人"; @@ -86,21 +88,24 @@ "delete"="刪除"; "detection.sensor"="檢測感測器"; "device"="設備"; -"device.config"="裝置設定"; +"device.config"="電台設定"; "device.configuration"="設備設定"; -"device.metrics.delete"="刪除所有設備指標??"; -"device.metrics.log"="設備指標紀錄檔"; -"device.role.client"="標準模式 - App 可以連接到裝置進行收發操作,並且會自動轉發 Mesh 網路中其他中繼節點的消息。"; -"device.role.clientmute"="靜音模式 - 與標準模式類似,App 可以連接到裝置進行收發操作,但不會轉發 Mesh 網路中其他中繼節點的消息。"; -"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption."; -"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\""; -"device.role.router"="纯路由模式 - 自動轉發 Mesh 網路中其他中繼節點的消息,中繼模式下螢幕會熄滅,Wi-Fi 和藍芽將會進入睡眠模式,App 將無法連接到裝置進行收發操作。"; -"device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼節點的消息,App 也可以連接到裝置進行收發操作。"; -"device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼節點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。"; +"device.metrics.delete"="刪除所有電台指標??"; +"device.metrics.log"="電台指標紀錄檔"; +"device.role.client"="標準模式 - App 可以連接到電台進行收發操作,並且會自動轉發 Mesh 網路中其他中繼點的消息。"; +"device.role.clientmute"="靜音模式 - 與標準模式類似,App 可以連接到電台進行收發操作,但不會轉發 Mesh 網路中其他中繼點的消息。"; +"device.role.clienthidden"="隱藏模式 - 用於那些\"只在被問到時才回答\"的節點,關閉所有常規廣播,但允許臨時通訊。依然會進行轉播,但只在本地轉播模式下進行(僅限已知的網狀網路)。可以用於私密操作或顯著減少空中時間/功耗。"; +"device.role.lostandfound"="遺失物模式 - 用於自動頻繁地向網狀網路發送一條包含設備當前位置的短信:\"I'm lost! Position: lat / long\""; +"device.role.router"="纯路由模式 - 自動轉發 Mesh 網路中其他中繼點的消息,中繼模式下螢幕會熄滅,Wi-Fi 和藍芽將會進入睡眠模式,App 將無法連接到電台進行收發操作。"; +"device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼點的消息,App 也可以連接到電台進行收發操作。"; +"device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。"; "device.role.tracker"="追蹤模式 - 用於作為 GPS 追蹤器。從該設備發送的定位數據包優先級較高,每兩分鐘廣播一次。智能位置廣播預設為關閉。"; +"device.role.sensor"="傳感器模式 - 優先廣播傳感器數據包"; +"device.role.tak"="TAK模式 - 優化了 ATAK 系統通訊,減少常規廣播。"; +"device.role.taktracker"="TAK TRACKER追蹤器 - 啟用自動 TAK PLI 廣播並減少常規廣播。"; "direct.messages"="聊天"; "dismiss.keyboard"="隱藏鍵盤"; -"display"="螢幕(設備螢幕)"; +"display"="螢幕(電台螢幕)"; "display.config"="螢幕設定"; "distance"="距離"; "disconnect"="斷開連接"; @@ -108,20 +113,21 @@ "email.address"="電子信箱"; "enabled"="啟用"; "encrypted"="加密"; +"export"="匯出"; "external.notification"="外部通知"; "external.notification.config"="外部通知設定"; "finish"="完成"; "firmware.version"="韌體版本"; -"firmware.version.unsupported"="檢測到不支援的韌體版本,無法連接到裝置。"; +"firmware.version.unsupported"="檢測到不支援的韌體版本,無法連接到電台。"; "gas"="Gas"; "gas.resistance"="Gas Resistance"; "generate.qr.code"="生成QRcode"; "gpsformat.dec"="十進制"; "gpsformat.dms"="度分秒"; "gpsformat.utm"="通用橫軸墨卡托投影"; -"gpsformat.mgrs"="軍事網格系統"; -"gpsformat.olc"="開放的位置代碼(又稱加碼)"; -"gpsformat.osgr"="英國國土測量局網格"; +"gpsformat.mgrs"="軍事網格參考系統"; +"gpsformat.olc"="開放位置代碼"; +"gpsformat.osgr"="英國國土測量局網格參考系統"; "heard"="收到"; "heard.last"="最後收到"; "hybrid"="混合"; @@ -165,19 +171,19 @@ "interval.tyeight.hours"="四十八小时小時"; "interval.eventytwo.hours"="七十二小時"; "keyboard.type"="鍵盤類型"; -"logging"="載入中"; +"logging"="加載中"; "lora"="LoRa"; "lora.config"="LoRa 設定"; "map"="Mesh 地圖"; "map.centering"="置中"; "map.tiles.delete"="刪除已緩存的地圖區塊"; -"map.recentering"="自動重新置中"; -"map.use.legacy"="Use Legacy Mesh Map"; +"map.recentering"="自動重新居中"; +"map.use.legacy"="使用傳統Mesh地圖"; "map.type"="地圖類型"; "map.usertrackingmode"="使用者跟隨模式"; "map.usertrackingmode.none"="無"; "map.usertrackingmode.follow"="跟隨"; -"map.usertrackingmode.followwithheading"="Follow with heading"; +"map.usertrackingmode.followwithheading"="跟隨與方向"; "mesh.live.activity"="Mesh 即時活動"; "mesh.log"="Mesh 紀錄檔"; "mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; @@ -189,54 +195,55 @@ "mesh.log.channel.received %d %@"="Channel %d received from: %@"; "mesh.log.device.config %@"="收到裝置設定: %@"; "mesh.log.display.config %@"="收到顯示模組設定: %@"; -"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@"; -"mesh.log.device.metadata.received %@"="Device Metadata admin message received from: %@"; -"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@"; -"mesh.log.externalnotification.config %@"="External Notification module config received: %@"; +"mesh.log.devicemetadata %@"="請求設備元數據:%@"; +"mesh.log.device.metadata.received %@"="從 %@ 收到設備元數據管理消息"; +"mesh.log.detectionsensor.config %@"="收到偵測感應器模組配置:%@"; +"mesh.log.externalnotification.config %@"="收到外部通知模組配置:%@"; "mesh.log.lora.config %@"="收到LoRa設定: %@"; -"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@"; -"mesh.log.mqtt.config %@"="MQTT module config received: %@"; -"mesh.log.myinfo %@"="MyInfo received: %@"; +"mesh.log.lora.config.sent %@"="發送LoRa配置給:%@"; +"mesh.log.mqtt.config %@"="收到MQTT模組配置:%@"; +"mesh.log.myinfo %@"="收到我的資訊:%@"; "mesh.log.network.config %@"="收到網路設定: %@"; -"mesh.log.nodeinfo.received %@"="收到中繼節點訊息: %@"; -"mesh.log.paxcounter %@"="PAX Counter message received for: %@"; -"mesh.log.position.config %@"="Positon config received: %@"; -"mesh.log.position.received %@"="從中繼節點接收到定位封包: %@"; +"mesh.log.nodeinfo.received %@"="收到中繼點訊息: %@"; +"mesh.log.paxcounter %@"="為 %@ 收到PAX計數器消息"; +"mesh.log.position.config %@"="收到位置配置:%@"; +"mesh.log.position.received %@"="從中繼點接收到定位封包: %@"; "mesh.log.rangetest.config %@"="收到拉距測試模組設定: %@"; -"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; -"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; -"mesh.log.serial.config %@"="Serial module config received: %@"; -"mesh.log.sharelocation %@"="傳送iOS裝置的GPS定位封包到中繼節點上: %@"; -"mesh.log.storeforward.config %@"="Store & Forward module config received: %@"; -"mesh.log.telemetry.config %@"="收到遙測模組設定: %@"; -"mesh.log.telemetry.received %@"="收到遙測資料: %@"; -"mesh.log.textmessage.received"="Message received from the text message app."; +"mesh.log.ringtone.config %@"="收到RTTTL鈴聲配置:%@"; +"mesh.log.routing.message %@ %@"="為請求ID: %@ 收到路由 Ack狀態: %@"; +"mesh.log.serial.config %@"="收到串列模組配置:%@"; +"mesh.log.sharelocation %@"="傳送iOS裝置的GPS定位封包到中繼點上: %@"; +"mesh.log.storeforward.config %@"="收到儲存與轉發模組配置:%@"; +"mesh.log.telemetry.config %@"="收到遠測模組設定: %@"; +"mesh.log.telemetry.received %@"="收到遠測資料: %@"; +"mesh.log.textmessage.received"="從文字消息應用程序收到消息。"; "mesh.log.textmessage.send.failed %@"="訊息傳送失敗, 沒有正確連接到 %@"; "mesh.log.textmessage.sent %@ %@ %@"="傳送訊息 %@ 從 %@ 到 %@"; -"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly."; -"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@"; -"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; -"mesh.log.wantconfig %@"="Issuing Want Config to %@"; -"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; -"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; +"mesh.log.traceroute.received.direct %@"="直接收到發送至節點的追蹤路由請求:%@"; +"mesh.log.traceroute.received.route %@"="返回的追蹤路由請求:%@"; +"mesh.log.traceroute.sent %@"="發送追蹤路由請求至節點:%@"; +"mesh.log.wantconfig %@"="對 %@ 發出配置請求"; +"mesh.log.waypoint.sent %@"="從 %@ 發送航點封包"; +"mesh.log.waypoint.received %@"="從節點收到航點封包:%@"; "message"="訊息"; "message.details"="詳細訊息"; "messages"="訊息"; "mode"="模式"; "module.configuration"="模塊設定"; "mqtt"="MQTT"; -"mqtt.connect"="Connect to MQTT"; +"mqtt.connect"="連線到 MQTT"; "mqtt.config"="MQTT 設定"; "mqtt.clientproxy"="MQTT 客户端代理"; -"mqtt.disconnect"="Disconnect from MQTT"; +"mqtt.disconnect"="是否與 MQTT 連接"; "mqtt.username"="用戶名稱"; "name"="名稱"; "network"="網路"; "network.config"="網路設定"; -"nodes"="中繼節點"; -"nodes %@"="中繼節點 (%@)"; -"no.nodes"="未找到 Meshtastic 中繼節點"; -"not.connected"="未連接到設備"; +"nodes"="中繼點"; +"nodes %@"="中繼點 (%@)"; +"nodelist.filter.distance %@"="距離達 %@ 以內"; +"no.nodes"="未找到 Meshtastic 中繼點"; +"not.connected"="未連接到電台"; "numbers.punctuation"="數字和標點符號"; "off"="關閉"; "offline"="離線"; @@ -245,17 +252,17 @@ "password"="密碼"; "pause"="暫停"; "phone.gps"="手機 GPS"; -"phone.gps.interval.description"="設備通過手機獲得定位的時間間隔,但是向 Mesh 網路中更新定位的時間間隔由裝置控制。"; +"phone.gps.interval.description"="電台通過手機獲得定位的時間間隔,但是向 Mesh 網路中更新定位的時間間隔由電台控制。"; "position"="定位"; "position.config"="定位設定"; -"preferred.radio"="首選設備"; -"radio.configuration"="設備設定"; +"preferred.radio"="首選電台"; +"radio.configuration"="電台設定"; "range.test"="拉距測試"; "range.test.blocked"="區塊範圍測試"; "range.test.config"="拉距測試設定"; "reply"="回復"; "reboot"="重新啟動"; -"reboot.node"="重啟中繼節點"; +"reboot.node"="重啟中繼點"; "received.ack"="收到確認"; "received.ack.real"="收件人確認"; "resume"="恢復"; @@ -272,7 +279,7 @@ "routing.nochannel"="没有頻道"; "routing.toolarge"="數據包過大"; "routing.noresponse"="無回應"; -"routing.dutycyclelimit"="已達到目前區域循環週期發射上限"; +"routing.dutycyclelimit"="已達到頻道占用循環週期發射上限"; "routing.badRequest"="錯誤請求"; "routing.notauthorized"="未授權"; "satellite"="衛星"; @@ -291,7 +298,7 @@ "share.position"="分享位置"; "subscribed"="連接到 Mesh 網路"; "select.contact"="選擇聯絡人"; -"select.node"="選擇中繼節點"; +"select.node"="選擇中繼點"; "select.menu.item"="從菜單選擇項目"; "set.region"="設定 LoRa 區域"; "standard"="標準"; @@ -303,22 +310,25 @@ "storeforward.heartbeat"="發送心跳包"; "tapback"="響應"; "tapback.heart"="心"; -"tapback.thumbsup"="讚"; -"tapback.thumbsdown"="倒讚"; +"tapback.thumbsup"="豎大拇指"; +"tapback.thumbsdown"="倒大拇指"; "tapback.haha"="哈哈"; "tapback.exclamation"="驚嘆號"; "tapback.question"="問號"; "tapback.poop"="便便"; -"telemetry"="遙測(傳感器)"; -"telemetry.config"="遙測設定"; +"tapback.wave"="招手"; +"telemetry"="遠測(傳感器)"; +"telemetry.config"="遠側設定"; "timeout"="超時"; "timestamp"="時間戳記"; -"tip.bluetooth.connect.title"="連接到 LoRa 設備"; -"tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 裝置的信息。您可以向左滑動斷開裝置,長按查看統計訊息或開始即時活動。"; +"tip.bluetooth.connect.title"="連接到 LoRa 電台"; +"tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 電台的信息。您可以向左滑動斷開電台,長按查看統計訊息或開始即時活動。"; +"tip.channel.admin.title"="管理頻道"; +"tip.channel.admin.message"="偵測到管理頻道:從下拉選單中選擇一個節點來管理連接或遠端設備。"; "tip.channels.create.title"="管理頻道"; "tip.channels.create.message"="現在 Mesh 上的資料會通過主通道發送。您可以設定輔助通道來建立由自己的金鑰保護的其他訊息組 [頻道設定提示](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="共享 Meshtastic 頻道"; -"tip.channels.share.message"="在 Meshtastic 網路中最多有 8 個頻道。第一個頻道是主頻道,大多數活動都發生在這裡,也是必需的。如果您不共享主頻道,您的第一個共享頻道就會成為其他網路的主頻道。它會在其主頻道和您的輔助頻道上對話。名稱為 admin 的頻道可遠端控制中繼節點。其他頻道用於私人群组,每個群組都有自己的密鑰。"; +"tip.channels.share.message"="在 Meshtastic 網路中最多有 8 個頻道。第一個頻道是主頻道,大多數活動都發生在這裡,也是必需的。如果您不共享主頻道,您的第一個共享頻道就會成為其他網路的主頻道。它會在其主頻道和您的輔助頻道上對話。名稱為 admin 的頻道可遠端控制中繼點。其他頻道用於私人群组,每個群組都有自己的密鑰。"; "tip.messages.title"="消息"; "tip.messages.message"="您可以發送和接收1對1聊天和群聊。在任何訊息中,您都可以長按查看可用的操作,如複製、回復、拍一拍、刪除以及詳情。"; "twitter"="Twitter"; @@ -331,3 +341,4 @@ "user.details"="使用者資料"; "voltage"="電壓"; "waiting"="等待中..."; +"appsettings.newNodeNotifications"="新節點通知";