From ad43fc0666be097ff9ca36fd6c69e907203463aa Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 30 Nov 2024 19:09:17 -0800 Subject: [PATCH 1/6] UNK node notifications and immediate notification scheduling --- .../Helpers/LocalNotificationManager.swift | 89 ++++++++++--------- Meshtastic/Helpers/MeshPackets.swift | 88 ++++++++++-------- 2 files changed, 98 insertions(+), 79 deletions(-) diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index 47f64bce..064021e1 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -4,36 +4,36 @@ import OSLog class LocalNotificationManager { - var notifications = [Notification]() + var notifications = [Notification]() let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title: "πŸ‘ \(Tapbacks.thumbsUp.description)", options: []) let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title: "πŸ‘Ž \(Tapbacks.thumbsDown.description)", options: []) let replyInputAction = UNTextInputNotificationAction(identifier: "messageNotification.replyInputAction", title: "reply".localized, options: []) - // Step 1 Request Permissions for notifications - private func requestAuthorization() { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in + // Step 1 Request Permissions for notifications + private func requestAuthorization() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in - if granted == true && error == nil { + if granted == true && error == nil { self.scheduleNotifications() - } - } - } + } + } + } func schedule() { - UNUserNotificationCenter.current().getNotificationSettings { settings in - switch settings.authorizationStatus { - case .notDetermined: + UNUserNotificationCenter.current().getNotificationSettings { settings in + switch settings.authorizationStatus { + case .notDetermined: self.requestAuthorization() - case .authorized, .provisional: - self.scheduleNotifications() - default: - break // Do nothing - } - } - } + case .authorized, .provisional: + self.scheduleNotifications() + default: + break // Do nothing + } + } + } - // This function iterates over the Notification objects in the notifications array and schedules them for delivery in the future - private func scheduleNotifications() { + // This function iterates over the Notification objects in the notifications array and schedules them for delivery in the future + private func scheduleNotifications() { let messageNotificationCategory = UNNotificationCategory( identifier: "messageNotificationCategory", actions: [thumbsUpAction, thumbsDownAction, replyInputAction], @@ -43,13 +43,15 @@ class LocalNotificationManager { UNUserNotificationCenter.current().setNotificationCategories([messageNotificationCategory]) - for notification in notifications { - let content = UNMutableNotificationContent() - content.subtitle = notification.subtitle - content.title = notification.title - content.body = notification.content - content.sound = .default - content.interruptionLevel = .timeSensitive + for notification in notifications { + let content = UNMutableNotificationContent() + if let subtitle = notification.subtitle { + content.subtitle = subtitle + } + content.title = notification.title + content.body = notification.content + content.sound = .default + content.interruptionLevel = .timeSensitive if notification.target != nil { content.userInfo["target"] = notification.target @@ -68,34 +70,33 @@ class LocalNotificationManager { content.userInfo["userNum"] = notification.userNum } - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) - let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger) + let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: nil) - UNUserNotificationCenter.current().add(request) { error in + UNUserNotificationCenter.current().add(request) { error in if let error { Logger.services.error("Error Scheduling Notification: \(error.localizedDescription)") } - } - } - } + } + } + } - // Check and debug what local notifications have been scheduled - func listScheduledNotifications() { - UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in + // Check and debug what local notifications have been scheduled + func listScheduledNotifications() { + UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in - for notification in notifications { + for notification in notifications { Logger.services.debug("\(notification, privacy: .public)") - } - } - } + } + } + } } struct Notification { - var id: String - var title: String - var subtitle: String - var content: String + var id: String + var title: String + var subtitle: String? + var content: String var target: String? var path: String? var messageId: Int64? diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index a5cfa7ba..4247f326 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -948,44 +948,61 @@ func textMessageAppPacket( manager.schedule() Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") } - } else if newMessage.fromUser != nil && newMessage.toUser == nil { + } else if newMessage.toUser == nil { let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) - do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if !fetchedMyInfo.isEmpty { - appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if !fetchedMyInfo.isEmpty { + appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages - for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { - if channel.index == newMessage.channel { - context.refresh(channel, mergeChanges: true) - } - if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { - // Create an iOS Notification for the received private channel message and schedule it immediately - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", - content: messageText!, - target: "messages", - path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", - messageId: newMessage.messageId, - channel: newMessage.channel, - userNum: Int64(newMessage.fromUser?.userId ?? "0") - ) - ] - manager.schedule() - Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") - } - } - } - } catch { - // Handle error - } - } + for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { + if channel.index == newMessage.channel { + context.refresh(channel, mergeChanges: true) + } + if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { + // Create an iOS Notification for the received private channel message and schedule it immediately + let manager = LocalNotificationManager() + + if newMessage.fromUser != nil { + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", + content: messageText!, + target: "messages", + path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(newMessage.fromUser?.userId ?? "0") + ) + ] + } else { + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "unknown".localized, + content: messageText!, + target: "messages", + path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: 0 + ) + ] + } + + manager.schedule() + Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") + } + } + } + } catch { + // Handle error + } + } } } catch { context.rollback() @@ -998,6 +1015,7 @@ func textMessageAppPacket( } } + func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from)) From f3b6d9f5047d849c1eddd8d4afaf6819a1574aa5 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 28 Dec 2024 17:36:15 -0800 Subject: [PATCH 2/6] Make notifications instant --- Meshtastic/Helpers/LocalNotificationManager.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index 47f64bce..3eb450b9 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -68,8 +68,7 @@ class LocalNotificationManager { content.userInfo["userNum"] = notification.userNum } - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) - let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger) + let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) { error in if let error { From 79ef5f09f0e254b7e13a781e53a00a0c171a3192 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:16:41 -0800 Subject: [PATCH 3/6] remove all changes made to mesh packets --- .../Helpers/LocalNotificationManager.swift | 1167 +++++++++++++++-- 1 file changed, 1075 insertions(+), 92 deletions(-) diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index 3cccaa9d..d7a80c17 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -1,110 +1,1093 @@ +// +// MeshPackets.swift +// Meshtastic Apple +// +// Created by Garth Vander Houwen on 5/27/22. +// + import Foundation +import CoreData +import MeshtasticProtobufs import SwiftUI +import RegexBuilder import OSLog +#if canImport(ActivityKit) +import ActivityKit +#endif -class LocalNotificationManager { - - var notifications = [Notification]() - let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title: "πŸ‘ \(Tapbacks.thumbsUp.description)", options: []) - let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title: "πŸ‘Ž \(Tapbacks.thumbsDown.description)", options: []) - let replyInputAction = UNTextInputNotificationAction(identifier: "messageNotification.replyInputAction", title: "reply".localized, options: []) - - // Step 1 Request Permissions for notifications - private func requestAuthorization() { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in - - if granted == true && error == nil { - self.scheduleNotifications() - } +func generateMessageMarkdown (message: String) -> String { + if !message.isEmoji() { + let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber] + guard let detector = try? NSDataDetector(types: types.rawValue) else { + return message } - } - - func schedule() { - UNUserNotificationCenter.current().getNotificationSettings { settings in - switch settings.authorizationStatus { - case .notDetermined: - self.requestAuthorization() - case .authorized, .provisional: - self.scheduleNotifications() - default: - break // Do nothing - } - } - } - - // This function iterates over the Notification objects in the notifications array and schedules them for delivery in the future - private func scheduleNotifications() { - let messageNotificationCategory = UNNotificationCategory( - identifier: "messageNotificationCategory", - actions: [thumbsUpAction, thumbsDownAction, replyInputAction], - intentIdentifiers: [], - options: .customDismissAction - ) - - UNUserNotificationCenter.current().setNotificationCategories([messageNotificationCategory]) - - for notification in notifications { - let content = UNMutableNotificationContent() - if let subtitle = notification.subtitle { - content.subtitle = subtitle - } - content.title = notification.title - content.body = notification.content - content.sound = .default - content.interruptionLevel = .timeSensitive - - if notification.target != nil { - content.userInfo["target"] = notification.target - } - if notification.path != nil { - content.userInfo["path"] = notification.path - } - if notification.messageId != nil { - content.categoryIdentifier = "messageNotificationCategory" - content.userInfo["messageId"] = notification.messageId - } - if notification.channel != nil { - content.userInfo["channel"] = notification.channel - } - if notification.userNum != nil { - content.userInfo["userNum"] = notification.userNum - } - if notification.critical { - content.sound = UNNotificationSound.defaultCritical - } - -let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: nil) - - - UNUserNotificationCenter.current().add(request) { error in - if let error { - Logger.services.error("Error Scheduling Notification: \(error.localizedDescription)") + let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count)) + var messageWithMarkdown = message + if matches.count > 0 { + for match in matches { + guard let range = Range(match.range, in: message) else { continue } + if match.resultType == .address { + let address = message[range] + let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics) + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))") + } else if match.resultType == .phoneNumber { + let phone = messageWithMarkdown[range] + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") + } else if match.resultType == .link { + let start = match.range.lowerBound + let stop = match.range.upperBound + let url = message[start ..< stop] + let absoluteUrl = match.url?.absoluteString ?? "" + let markdownUrl = "[\(url)](\(absoluteUrl))" + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: markdownUrl) } } } + return messageWithMarkdown } + return message +} - // Check and debug what local notifications have been scheduled - func listScheduledNotifications() { - UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in +func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { - for notification in notifications { - Logger.services.debug("\(notification, privacy: .public)") + if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { + upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { + upsertDeviceConfigPacket(config: config.device, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { + upsertDisplayConfigPacket(config: config.display, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { + upsertLoRaConfigPacket(config: config.lora, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { + upsertNetworkConfigPacket(config: config.network, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { + upsertPositionConfigPacket(config: config.position, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { + upsertPowerConfigPacket(config: config.power, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { + upsertSecurityConfigPacket(config: config.security, nodeNum: nodeNum, context: context) + } +} + +func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { + + if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(config.ambientLighting) { + upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(config.cannedMessage) { + upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(config.detectionSensor) { + upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(config.externalNotification) { + upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(config.mqtt) { + upsertMqttModuleConfigPacket(config: config.mqtt, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.paxcounter(config.paxcounter) { + upsertPaxCounterModuleConfigPacket(config: config.paxcounter, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(config.rangeTest) { + upsertRangeTestModuleConfigPacket(config: config.rangeTest, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(config.serial) { + upsertSerialModuleConfigPacket(config: config.serial, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(config.telemetry) { + upsertTelemetryModuleConfigPacket(config: config.telemetry, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.storeForward(config.storeForward) { + upsertStoreForwardModuleConfigPacket(config: config.storeForward, nodeNum: nodeNum, context: context) + } +} + +func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedObjectContext) -> MyInfoEntity? { + + let logString = String.localizedStringWithFormat("mesh.log.myinfo %@".localized, String(myInfo.myNodeNum)) + MeshLogger.log("ℹ️ \(logString)") + + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(myInfo.myNodeNum)) + + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + // Not Found Insert + if fetchedMyInfo.isEmpty { + + let myInfoEntity = MyInfoEntity(context: context) + myInfoEntity.peripheralId = peripheralId + myInfoEntity.myNodeNum = Int64(myInfo.myNodeNum) + myInfoEntity.rebootCount = Int32(myInfo.rebootCount) + myInfoEntity.deviceId = myInfo.deviceID + do { + try context.save() + Logger.data.info("πŸ’Ύ Saved a new myInfo for node: \(myInfo.myNodeNum.toHex(), privacy: .public)") + return myInfoEntity + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("πŸ’₯ Error Inserting New Core Data MyInfoEntity: \(nsError, privacy: .public)") + } + } else { + + fetchedMyInfo[0].peripheralId = peripheralId + fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum) + fetchedMyInfo[0].rebootCount = Int32(myInfo.rebootCount) + + do { + try context.save() + Logger.data.info("πŸ’Ύ Updated myInfo for node: \(myInfo.myNodeNum.toHex(), privacy: .public)") + return fetchedMyInfo[0] + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("πŸ’₯ Error Updating Core Data MyInfoEntity: \(nsError, privacy: .public)") + } + } + } catch { + Logger.data.error("πŸ’₯ Fetch MyInfo Error") + } + return nil +} + +func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectContext) { + + if channel.isInitialized && channel.hasSettings && channel.role != Channel.Role.disabled { + + let logString = String.localizedStringWithFormat("mesh.log.channel.received %d %@".localized, channel.index, String(fromNum)) + MeshLogger.log("πŸŽ›οΈ \(logString)") + + let fetchedMyInfoRequest = MyInfoEntity.fetchRequest() + fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", fromNum) + + do { + let fetchedMyInfo = try context.fetch(fetchedMyInfoRequest) + if fetchedMyInfo.count == 1 { + let newChannel = ChannelEntity(context: context) + newChannel.id = Int32(channel.index) + newChannel.index = Int32(channel.index) + newChannel.uplinkEnabled = channel.settings.uplinkEnabled + newChannel.downlinkEnabled = channel.settings.downlinkEnabled + newChannel.name = channel.settings.name + newChannel.role = Int32(channel.role.rawValue) + newChannel.psk = channel.settings.psk + if channel.settings.hasModuleSettings { + newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision) + newChannel.mute = channel.settings.moduleSettings.isClientMuted + } + guard let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as? NSMutableOrderedSet else { + return + } + if let oldChannel = mutableChannels.first(where: {($0 as AnyObject).index == newChannel.index }) as? ChannelEntity { + let index = mutableChannels.index(of: oldChannel as Any) + mutableChannels.replaceObject(at: index, with: newChannel) + } else { + mutableChannels.add(newChannel) + } + fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet + if newChannel.name?.lowercased() == "admin" { + fetchedMyInfo[0].adminIndex = newChannel.index + } + context.refresh(newChannel, mergeChanges: true) + do { + try context.save() + } catch { + Logger.data.error("πŸ’₯ Failed to save channel: \(error.localizedDescription, privacy: .public)") + } + Logger.data.info("πŸ’Ύ Updated MyInfo channel \(channel.index) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum)") + } else if channel.role.rawValue > 0 { + Logger.data.error("πŸ’₯Trying to save a channel to a MyInfo that does not exist: \(fromNum.toHex(), privacy: .public)") + } + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("πŸ’₯ Error Saving MyInfo Channel from ADMIN_APP \(nsError, privacy: .public)") + } + } +} + +func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + if metadata.isInitialized { + let logString = String.localizedStringWithFormat("mesh.log.device.metadata.received %@".localized, fromNum.toHex()) + MeshLogger.log("🏷️ \(logString)") + + let fetchedNodeRequest = NodeInfoEntity.fetchRequest() + fetchedNodeRequest.predicate = NSPredicate(format: "num == %lld", fromNum) + + do { + let fetchedNode = try context.fetch(fetchedNodeRequest) + let newMetadata = DeviceMetadataEntity(context: context) + newMetadata.time = Date() + newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion) + newMetadata.canShutdown = metadata.canShutdown + newMetadata.hasWifi = metadata.hasWifi_p + newMetadata.hasBluetooth = metadata.hasBluetooth_p + newMetadata.hasEthernet = metadata.hasEthernet_p + newMetadata.role = Int32(metadata.role.rawValue) + newMetadata.positionFlags = Int32(metadata.positionFlags) + // Swift does strings weird, this does work to get the version without the github hash + let lastDotIndex = metadata.firmwareVersion.lastIndex(of: ".") + var version = metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: metadata.firmwareVersion))] + version = version.dropLast() + newMetadata.firmwareVersion = String(version) + if fetchedNode.count > 0 { + fetchedNode[0].metadata = newMetadata + } else { + + if fromNum > 0 { + let newNode = createNodeInfo(num: Int64(fromNum), context: context) + newNode.metadata = newMetadata + } + } + if sessionPasskey?.count != 0 { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + } catch { + Logger.data.error("πŸ’₯ Failed to save device metadata: \(error.localizedDescription, privacy: .public)") + } + Logger.data.info("πŸ’Ύ Updated Device Metadata from Admin App Packet For: \(fromNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving MyInfo Channel from ADMIN_APP \(nsError, privacy: .public)") + } + } +} + +func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? { + + let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, String(nodeInfo.num)) + MeshLogger.log("πŸ“Ÿ \(logString)") + + guard nodeInfo.num > 0 else { return nil } + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeInfo.num)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Not Found Insert + if fetchedNode.isEmpty && nodeInfo.num > 0 { + + let newNode = NodeInfoEntity(context: context) + newNode.id = Int64(nodeInfo.num) + newNode.num = Int64(nodeInfo.num) + newNode.channel = Int32(nodeInfo.channel) + newNode.favorite = nodeInfo.isFavorite + newNode.ignored = nodeInfo.isIgnored + newNode.hopsAway = Int32(nodeInfo.hopsAway) + + if nodeInfo.hasDeviceMetrics { + let telemetry = TelemetryEntity(context: context) + telemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) + telemetry.voltage = nodeInfo.deviceMetrics.voltage + telemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization + telemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx + var newTelemetries = [TelemetryEntity]() + newTelemetries.append(telemetry) + newNode.telemetries? = NSOrderedSet(array: newTelemetries) + } + + newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + 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) + newUser.longName = nodeInfo.user.longName + newUser.shortName = nodeInfo.user.shortName + newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() + newUser.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) + newUser.hwDisplayName = dh?.displayName + } + } + newUser.isLicensed = nodeInfo.user.isLicensed + newUser.role = Int32(nodeInfo.user.role.rawValue) + if !nodeInfo.user.publicKey.isEmpty { + newUser.pkiEncrypted = true + newUser.publicKey = nodeInfo.user.publicKey + } + newNode.user = newUser + } else if nodeInfo.num > Constants.minimumNodeNum { + let newUser = createUser(num: Int64(nodeInfo.num), context: context) + newNode.user = newUser + } + + if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { + let position = PositionEntity(context: context) + position.latest = true + position.seqNo = Int32(nodeInfo.position.seqNumber) + position.latitudeI = nodeInfo.position.latitudeI + position.longitudeI = nodeInfo.position.longitudeI + position.altitude = nodeInfo.position.altitude + position.satsInView = Int32(nodeInfo.position.satsInView) + position.speed = Int32(nodeInfo.position.groundSpeed) + position.heading = Int32(nodeInfo.position.groundTrack) + position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) + var newPostions = [PositionEntity]() + newPostions.append(position) + newNode.positions? = NSOrderedSet(array: newPostions) + } + + // Look for a MyInfo + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) + + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if fetchedMyInfo.count > 0 { + newNode.myInfo = fetchedMyInfo[0] + } + do { + try context.save() + Logger.data.info("πŸ’Ύ Saved a new Node Info For: \(String(nodeInfo.num))") + return newNode + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError)") + } + } catch { + Logger.data.error("Fetch MyInfo Error") + } + } else if nodeInfo.num > 0 { + + fetchedNode[0].id = Int64(nodeInfo.num) + fetchedNode[0].num = Int64(nodeInfo.num) + fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + fetchedNode[0].snr = nodeInfo.snr + fetchedNode[0].channel = Int32(nodeInfo.channel) + fetchedNode[0].favorite = nodeInfo.isFavorite + fetchedNode[0].ignored = nodeInfo.isIgnored + fetchedNode[0].hopsAway = Int32(nodeInfo.hopsAway) + + if nodeInfo.hasUser { + if fetchedNode[0].user == nil { + fetchedNode[0].user = UserEntity(context: context) + } + // Set the public key for a user if it is empty, don't update + if fetchedNode[0].user?.publicKey == nil && !nodeInfo.user.publicKey.isEmpty { + fetchedNode[0].user?.pkiEncrypted = true + fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey + } + fetchedNode[0].user!.userId = nodeInfo.user.id + fetchedNode[0].user!.num = Int64(nodeInfo.num) + fetchedNode[0].user!.numString = String(nodeInfo.num) + fetchedNode[0].user!.longName = nodeInfo.user.longName + fetchedNode[0].user!.shortName = nodeInfo.user.shortName + 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() + fetchedNode[0].user!.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId }) + fetchedNode[0].user!.hwDisplayName = dh?.displayName + } + } + } else { + if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum { + + let newUser = createUser(num: Int64(nodeInfo.num), context: context) + fetchedNode[0].user = newUser + } + } + + if nodeInfo.hasDeviceMetrics { + + let newTelemetry = TelemetryEntity(context: context) + newTelemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) + newTelemetry.voltage = nodeInfo.deviceMetrics.voltage + newTelemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization + newTelemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx + guard let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as? NSMutableOrderedSet else { + return nil + } + fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet + } + + if nodeInfo.hasPosition { + + if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { + + let position = PositionEntity(context: context) + position.latitudeI = nodeInfo.position.latitudeI + position.longitudeI = nodeInfo.position.longitudeI + position.altitude = nodeInfo.position.altitude + position.satsInView = Int32(nodeInfo.position.satsInView) + position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) + guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else { + return nil + } + fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet + } + + } + + // Look for a MyInfo + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) + + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if fetchedMyInfo.count > 0 { + fetchedNode[0].myInfo = fetchedMyInfo[0] + } + do { + try context.save() + Logger.data.info("πŸ’Ύ [NodeInfo] saved for \(nodeInfo.num.toHex(), privacy: .public)") + return fetchedNode[0] + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("πŸ’₯ Error Saving Core Data NodeInfoEntity: \(nsError, privacy: .public)") + } + } catch { + Logger.data.error("πŸ’₯ Fetch MyInfo Error") + } + } + } catch { + Logger.data.error("πŸ’₯ Fetch NodeInfoEntity Error") + } + return nil +} + +func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { + + if let adminMessage = try? AdminMessage(serializedBytes: packet.decoded.payload) { + + if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) { + + if let cmmc = try? CannedMessageModuleConfig(serializedBytes: packet.decoded.payload) { + + if !cmmc.messages.isEmpty { + + let logString = String.localizedStringWithFormat("mesh.log.cannedmessages.messages.received %@".localized, packet.from.toHex()) + MeshLogger.log("πŸ₯« \(logString)") + + let fetchNodeRequest = NodeInfoEntity.fetchRequest() + fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + let fetchedNode = try context.fetch(fetchNodeRequest) + if fetchedNode.count == 1 { + let messages = String(cmmc.textFormatString()) + .replacingOccurrences(of: "11: ", with: "") + .replacingOccurrences(of: "\"", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + fetchedNode[0].cannedMessageConfig?.messages = messages + do { + try context.save() + Logger.data.info("πŸ’Ύ Updated Canned Messages Messages For: \(fetchedNode[0].num.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("πŸ’₯ Error Saving NodeInfoEntity from POSITION_APP \(nsError, privacy: .public)") + } + } + } catch { + Logger.data.error("πŸ’₯ Error Deserializing ADMIN_APP packet.") + } + } + } + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { + channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) { + deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) { + let config = adminMessage.getConfigResponse + if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { + upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { + upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { + upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { + upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { + upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { + upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { + upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { + upsertSecurityConfigPacket(config: config.security, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) { + let moduleConfig = adminMessage.getModuleConfigResponse + if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(moduleConfig.ambientLighting) { + upsertAmbientLightingModuleConfigPacket(config: moduleConfig.ambientLighting, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) { + upsertCannedMessagesModuleConfigPacket(config: moduleConfig.cannedMessage, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(moduleConfig.detectionSensor) { + upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(moduleConfig.externalNotification) { + upsertExternalNotificationModuleConfigPacket(config: moduleConfig.externalNotification, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(moduleConfig.mqtt) { + upsertMqttModuleConfigPacket(config: moduleConfig.mqtt, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(moduleConfig.rangeTest) { + upsertRangeTestModuleConfigPacket(config: moduleConfig.rangeTest, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(moduleConfig.serial) { + upsertSerialModuleConfigPacket(config: moduleConfig.serial, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.storeForward(moduleConfig.storeForward) { + upsertStoreForwardModuleConfigPacket(config: moduleConfig.storeForward, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(moduleConfig.telemetry) { + upsertTelemetryModuleConfigPacket(config: moduleConfig.telemetry, nodeNum: Int64(packet.from), context: context) + } + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getRingtoneResponse(adminMessage.getRingtoneResponse) { + let ringtone = adminMessage.getRingtoneResponse + upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: Int64(packet.from), context: context) + } else { + 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) + } +} + +func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) { + + let fetchedAdminMessageRequest = MessageEntity.fetchRequest() + fetchedAdminMessageRequest.predicate = NSPredicate(format: "messageId == %lld", packet.decoded.requestID) + do { + let fetchedMessage = try context.fetch(fetchedAdminMessageRequest) + if fetchedMessage.count > 0 { + fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970) + fetchedMessage[0].ackError = Int32(RoutingError.none.rawValue) + fetchedMessage[0].receivedACK = true + fetchedMessage[0].realACK = true + fetchedMessage[0].ackSNR = packet.rxSnr + if fetchedMessage[0].fromUser != nil { + fetchedMessage[0].fromUser?.objectWillChange.send() + } + do { + try context.save() + } catch { + Logger.data.error("Failed to save admin message response as an ack: \(error.localizedDescription)") + } + } + } catch { + 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 = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + + if let paxMessage = try? Paxcount(serializedBytes: 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 { + guard let mutablePax = fetchedNode[0].pax!.mutableCopy() as? NSMutableOrderedSet else { + return + } + mutablePax.add(newPax) + fetchedNode[0].pax = mutablePax + do { + try context.save() + } catch { + Logger.data.error("Failed to save pax: \(error.localizedDescription)") + } + } else { + Logger.data.info("Node Info Not Found") + } + } + } catch { + + } +} + +func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { + + if let routingMessage = try? Routing(serializedBytes: packet.decoded.payload) { + + let routingError = RoutingError(rawValue: routingMessage.errorReason.rawValue) + + let routingErrorString = routingError?.display ?? "unknown".localized + let logString = String.localizedStringWithFormat("mesh.log.routing.message %@ %@".localized, String(packet.decoded.requestID), routingErrorString) + MeshLogger.log("πŸ•ΈοΈ \(logString)") + + let fetchMessageRequest = MessageEntity.fetchRequest() + fetchMessageRequest.predicate = NSPredicate(format: "messageId == %lld", Int64(packet.decoded.requestID)) + + do { + let fetchedMessage = try context.fetch(fetchMessageRequest) + if fetchedMessage.count > 0 { + + if fetchedMessage[0].toUser != nil { + // Real ACK from DM Recipient + if packet.to != packet.from { + fetchedMessage[0].realACK = true + } + } + fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue) + if routingMessage.errorReason == Routing.Error.none { + + fetchedMessage[0].receivedACK = true + } + fetchedMessage[0].ackSNR = packet.rxSnr + if packet.rxTime > 0 { + fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) + } else { + fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970) + } + + if fetchedMessage[0].toUser != nil { + fetchedMessage[0].toUser!.objectWillChange.send() + } else { + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", connectedNodeNum) + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if fetchedMyInfo.count > 0 { + + for ch in fetchedMyInfo[0].channels!.array as? [ChannelEntity] ?? [] where ch.index == packet.channel { + ch.objectWillChange.send() + } + } + } catch { } + } + + } else { + return + } + try context.save() + Logger.data.info("πŸ’Ύ ACK Saved for Message: \(packet.decoded.requestID)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving ACK for message: \(packet.id) Error: \(nsError)") + } + } +} + +func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { + + if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) { + + let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from)) + MeshLogger.log("πŸ“ˆ \(logString)") + + if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) { + /// Other unhandled telemetry packets + return + } + + let telemetry = TelemetryEntity(context: context) + + let fetchNodeTelemetryRequest = NodeInfoEntity.fetchRequest() + fetchNodeTelemetryRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + let fetchedNode = try context.fetch(fetchNodeTelemetryRequest) + if fetchedNode.count == 1 { + /// Currently only Device Metrics and Environment Telemetry are supported in the app + if telemetryMessage.variant == Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) { + // Device Metrics + telemetry.airUtilTx = telemetryMessage.deviceMetrics.airUtilTx + telemetry.channelUtilization = telemetryMessage.deviceMetrics.channelUtilization + telemetry.batteryLevel = Int32(telemetryMessage.deviceMetrics.batteryLevel) + telemetry.voltage = telemetryMessage.deviceMetrics.voltage + telemetry.uptimeSeconds = Int32(telemetryMessage.deviceMetrics.uptimeSeconds) + telemetry.metricsType = 0 + Logger.statistics.info("πŸ“ˆ [Mesh Statistics] Channel Utilization: \(telemetryMessage.deviceMetrics.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.deviceMetrics.airUtilTx, privacy: .public) for Node: \(packet.from.toHex(), privacy: .public)") + } else if telemetryMessage.variant == Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) { + // Environment Metrics + telemetry.barometricPressure = telemetryMessage.environmentMetrics.barometricPressure + telemetry.current = telemetryMessage.environmentMetrics.current + telemetry.iaq = Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.iaq) + telemetry.gasResistance = telemetryMessage.environmentMetrics.gasResistance + telemetry.relativeHumidity = telemetryMessage.environmentMetrics.relativeHumidity + telemetry.temperature = telemetryMessage.environmentMetrics.temperature + telemetry.current = telemetryMessage.environmentMetrics.current + telemetry.voltage = telemetryMessage.environmentMetrics.voltage + telemetry.weight = telemetryMessage.environmentMetrics.weight + telemetry.windSpeed = telemetryMessage.environmentMetrics.windSpeed + telemetry.windGust = telemetryMessage.environmentMetrics.windGust + telemetry.windLull = telemetryMessage.environmentMetrics.windLull + telemetry.windDirection = Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.windDirection) + telemetry.metricsType = 1 + } else if telemetryMessage.variant == Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) { + // Local Stats for Live activity + telemetry.uptimeSeconds = Int32(telemetryMessage.localStats.uptimeSeconds) + telemetry.channelUtilization = telemetryMessage.localStats.channelUtilization + telemetry.airUtilTx = telemetryMessage.localStats.airUtilTx + telemetry.numPacketsTx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsTx) + telemetry.numPacketsRx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRx) + telemetry.numPacketsRxBad = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRxBad) + telemetry.numRxDupe = Int32(truncatingIfNeeded: telemetryMessage.localStats.numRxDupe) + telemetry.numTxRelay = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTxRelay) + telemetry.numTxRelayCanceled = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTxRelayCanceled) + telemetry.numOnlineNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numOnlineNodes) + telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes) + telemetry.metricsType = 4 + Logger.statistics.info("πŸ“ˆ [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)") + } + telemetry.snr = packet.rxSnr + telemetry.rssi = packet.rxRssi + telemetry.time = Date(timeIntervalSince1970: TimeInterval(Int64(truncatingIfNeeded: telemetryMessage.time))) + guard let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as? NSMutableOrderedSet else { + return + } + mutableTelemetries.add(telemetry) + if packet.rxTime > 0 { + fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(packet.rxTime)) + } else { + fetchedNode[0].lastHeard = Date() + } + fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet + } + try context.save() + + Logger.data.info("πŸ’Ύ [TelemetryEntity] of type \(MetricsTypes(rawValue: Int(telemetry.metricsType))?.name ?? "Unknown Metrics Type") Saved for Node: \(packet.from.toHex())") + if telemetry.metricsType == 0 { + // Connected Device Metrics + // ------------------------ + // Low Battery notification + if connectedNode == Int64(packet.from) { + if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 { + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(UUID().uuidString)"), + title: "Critically Low Battery!", + subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")", + content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.", + target: "nodes", + path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)" + ) + ] + manager.schedule() + } + } + } else if telemetry.metricsType == 4 { + // Update our live activity if there is one running, not available on mac +#if !targetEnvironment(macCatalyst) +#if canImport(ActivityKit) + + let fifteenMinutesLater = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())! + let date = Date.now...fifteenMinutesLater + let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(uptimeSeconds: UInt32(telemetry.uptimeSeconds), + channelUtilization: telemetry.channelUtilization, + airtime: telemetry.airUtilTx, + sentPackets: UInt32(telemetry.numPacketsTx), + receivedPackets: UInt32(telemetry.numPacketsRx), + badReceivedPackets: UInt32(telemetry.numPacketsRxBad), + dupeReceivedPackets: UInt32(telemetry.numRxDupe), + packetsSentRelay: UInt32(telemetry.numTxRelay), + packetsCanceledRelay: UInt32(telemetry.numTxRelayCanceled), + nodesOnline: UInt32(telemetry.numOnlineNodes), + totalNodes: UInt32(telemetry.numTotalNodes), + timerRange: date) + + let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Node Stats Data.", sound: .default) + let updatedContent = ActivityContent(state: updatedMeshStatus, staleDate: nil) + + let meshActivity = Activity.activities.first(where: { $0.attributes.nodeNum == connectedNode }) + if meshActivity != nil { + Task { + await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration) + // await meshActivity?.update(updatedContent) + Logger.services.debug("Updated live activity.") + } + } +#endif +#endif + } + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("πŸ’₯ Error Saving Telemetry for Node \(packet.from, privacy: .public) Error: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("πŸ’₯ Error Fetching NodeInfoEntity for Node \(packet.from.toHex(), privacy: .public)") + } +} + +func textMessageAppPacket( + packet: MeshPacket, + wantRangeTestPackets: Bool, + critical: Bool = false, + connectedNode: Int64, + storeForward: Bool = false, + context: NSManagedObjectContext, + appState: AppState +) { + var messageText = String(bytes: packet.decoded.payload, encoding: .utf8) + let rangeRef = Reference(Int.self) + let rangeTestRegex = Regex { + "seq " + TryCapture(as: rangeRef) { + OneOrMore(.digit) + } transform: { match in + Int(match) + } + } + 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(serializedBytes: packet.decoded.payload) { + 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)") + let messageUsers = UserEntity.fetchRequest() + messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from]) + do { + let fetchedUsers = try context.fetch(messageUsers) + let newMessage = MessageEntity(context: context) + newMessage.messageId = Int64(packet.id) + if packet.rxTime > 0 { + newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) + } else { + newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) + } + newMessage.receivedACK = false + newMessage.snr = packet.rxSnr + newMessage.rssi = packet.rxRssi + newMessage.isEmoji = packet.decoded.emoji == 1 + newMessage.channel = Int32(packet.channel) + newMessage.portNum = Int32(packet.decoded.portnum.rawValue) + if packet.decoded.portnum == PortNum.detectionSensorApp { + if !UserDefaults.enableDetectionNotifications { + newMessage.read = true + } + } + if packet.decoded.replyID > 0 { + newMessage.replyID = Int64(packet.decoded.replyID) + } + if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != Constants.maximumNodeNum { + if !storeForwardBroadcast { + newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) + } else { + /// Make a new to user if they are unknown + newMessage.toUser = createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) + } + } + if fetchedUsers.first(where: { $0.num == packet.from }) != nil { + newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) + /// Set the public key for the message + if newMessage.fromUser?.pkiEncrypted ?? false && packet.pkiEncrypted { + newMessage.pkiEncrypted = true + newMessage.publicKey = packet.publicKey + } + /// Check for key mismatch + if let nodeKey = newMessage.fromUser?.publicKey { + if newMessage.toUser != nil && packet.pkiEncrypted && !packet.publicKey.isEmpty { + if nodeKey != newMessage.publicKey { + newMessage.fromUser?.keyMatch = false + newMessage.fromUser?.newPublicKey = newMessage.publicKey + let nodeKey = String(nodeKey.base64EncodedString()).prefix(8) + let messageKey = String(newMessage.publicKey?.base64EncodedString() ?? "No Key").prefix(8) + Logger.data.error("πŸ”‘ Key mismatch original key: \(nodeKey, privacy: .public) . . . new key: \(messageKey, privacy: .public) . . .") + } + } + } else if packet.pkiEncrypted { + /// We have no key, set it if it is not empty + if !packet.publicKey.isEmpty { + newMessage.fromUser?.pkiEncrypted = true + newMessage.fromUser?.publicKey = packet.publicKey + } + } + } else { + /// Make a new from user if they are unknown + newMessage.fromUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + } + if packet.rxTime > 0 { + newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + } else { + newMessage.fromUser?.userNode?.lastHeard = Date() + } + newMessage.messagePayload = messageText + newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText!) + if packet.to != Constants.maximumNodeNum && newMessage.fromUser != nil { + newMessage.fromUser?.lastMessage = Date() + } + var messageSaved = false + do { + try context.save() + Logger.data.info("πŸ’Ύ Saved a new message for \(newMessage.messageId)") + messageSaved = true + + if messageSaved { + if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications { + return + } + if newMessage.fromUser != nil && newMessage.toUser != nil { + // Set Unread Message Indicators + if packet.to == connectedNode { + appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0 + } + if !(newMessage.fromUser?.mute ?? false) { + // Create an iOS Notification for the received DM message and schedule it immediately + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", + content: messageText!, + target: "messages", + path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(packet.from), + critical: critical + ) + ] + manager.schedule() + Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") + } + } else if newMessage.fromUser != nil && newMessage.toUser == nil { + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) + + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if !fetchedMyInfo.isEmpty { + appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages + + for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { + if channel.index == newMessage.channel { + context.refresh(channel, mergeChanges: true) + } + if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { + // Create an iOS Notification for the received private channel message and schedule it immediately + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", + content: messageText!, + target: "messages", + path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(newMessage.fromUser?.userId ?? "0"), + critical: critical) + ] + manager.schedule() + Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") + } + } + } + } catch { + // Handle error + } + } + } + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Failed to save new MessageEntity \(nsError)") + } + } catch { + Logger.data.error("Fetch Message To and From Users Error") + } + } } -struct Notification { - var id: String - var title: String - var subtitle: String? - var content: String - var target: String? - var path: String? - var messageId: Int64? - var channel: Int32? - var userNum: Int64? - var critical: Bool = false +func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from)) + MeshLogger.log("πŸ“ \(logString)") + + let fetchWaypointRequest = WaypointEntity.fetchRequest() + fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(packet.id)) + + do { + + if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) { + let fetchedWaypoint = try context.fetch(fetchWaypointRequest) + if fetchedWaypoint.isEmpty { + let waypoint = WaypointEntity(context: context) + + waypoint.id = Int64(packet.id) + waypoint.name = waypointMessage.name + waypoint.longDescription = waypointMessage.description_p + waypoint.latitudeI = waypointMessage.latitudeI + waypoint.longitudeI = waypointMessage.longitudeI + waypoint.icon = Int64(waypointMessage.icon) + waypoint.locked = Int64(waypointMessage.lockedTo) + if waypointMessage.expire >= 1 { + waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } else { + waypoint.expire = nil + } + waypoint.created = Date() + do { + try context.save() + 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 + let longitude = Double(waypoint.longitudeI) / 1e7 + manager.notifications = [ + Notification( + id: ("notification.id.\(waypoint.id)"), + title: "New Waypoint Received", + subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")", + content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")", + target: "map", + path: "meshtastic:///map?waypointid=\(waypoint.id)" + ) + ] + Logger.data.debug("meshtastic:///map?waypointid=\(waypoint.id)") + manager.schedule() + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError)") + } + } else { + fetchedWaypoint[0].id = Int64(packet.id) + fetchedWaypoint[0].name = waypointMessage.name + fetchedWaypoint[0].longDescription = waypointMessage.description_p + fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI + fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI + fetchedWaypoint[0].icon = Int64(waypointMessage.icon) + fetchedWaypoint[0].locked = Int64(waypointMessage.lockedTo) + if waypointMessage.expire >= 1 { + fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } else { + fetchedWaypoint[0].expire = nil + } + fetchedWaypoint[0].lastUpdated = Date() + do { + try context.save() + Logger.data.info("πŸ’Ύ Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError)") + } + } + } + } catch { + Logger.mesh.error("Error Deserializing WAYPOINT_APP packet.") + } } From 4c674c068a3daf708a2eaf590fb393bac9ee8f77 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:18:53 -0800 Subject: [PATCH 4/6] fix --- .../Helpers/LocalNotificationManager.swift | 1175 ++--------------- 1 file changed, 94 insertions(+), 1081 deletions(-) diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index d7a80c17..7fe17865 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -1,1093 +1,106 @@ -// -// MeshPackets.swift -// Meshtastic Apple -// -// Created by Garth Vander Houwen on 5/27/22. -// - import Foundation -import CoreData -import MeshtasticProtobufs import SwiftUI -import RegexBuilder import OSLog -#if canImport(ActivityKit) -import ActivityKit -#endif -func generateMessageMarkdown (message: String) -> String { - if !message.isEmoji() { - let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber] - guard let detector = try? NSDataDetector(types: types.rawValue) else { - return message +class LocalNotificationManager { + + var notifications = [Notification]() + let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title: "πŸ‘ \(Tapbacks.thumbsUp.description)", options: []) + let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title: "πŸ‘Ž \(Tapbacks.thumbsDown.description)", options: []) + let replyInputAction = UNTextInputNotificationAction(identifier: "messageNotification.replyInputAction", title: "reply".localized, options: []) + + // Step 1 Request Permissions for notifications + private func requestAuthorization() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in + + if granted == true && error == nil { + self.scheduleNotifications() + } } - let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count)) - var messageWithMarkdown = message - if matches.count > 0 { - for match in matches { - guard let range = Range(match.range, in: message) else { continue } - if match.resultType == .address { - let address = message[range] - let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics) - messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))") - } else if match.resultType == .phoneNumber { - let phone = messageWithMarkdown[range] - messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") - } else if match.resultType == .link { - let start = match.range.lowerBound - let stop = match.range.upperBound - let url = message[start ..< stop] - let absoluteUrl = match.url?.absoluteString ?? "" - let markdownUrl = "[\(url)](\(absoluteUrl))" - messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: markdownUrl) + } + + func schedule() { + UNUserNotificationCenter.current().getNotificationSettings { settings in + switch settings.authorizationStatus { + case .notDetermined: + self.requestAuthorization() + case .authorized, .provisional: + self.scheduleNotifications() + default: + break // Do nothing + } + } + } + + // This function iterates over the Notification objects in the notifications array and schedules them for delivery in the future + private func scheduleNotifications() { + let messageNotificationCategory = UNNotificationCategory( + identifier: "messageNotificationCategory", + actions: [thumbsUpAction, thumbsDownAction, replyInputAction], + intentIdentifiers: [], + options: .customDismissAction + ) + + UNUserNotificationCenter.current().setNotificationCategories([messageNotificationCategory]) + + for notification in notifications { + let content = UNMutableNotificationContent() + if let subtitle = notification.subtitle { + content.subtitle = subtitle + } + content.title = notification.title + content.body = notification.content + content.sound = .default + content.interruptionLevel = .timeSensitive + + if notification.target != nil { + content.userInfo["target"] = notification.target + } + if notification.path != nil { + content.userInfo["path"] = notification.path + } + if notification.messageId != nil { + content.categoryIdentifier = "messageNotificationCategory" + content.userInfo["messageId"] = notification.messageId + } + if notification.channel != nil { + content.userInfo["channel"] = notification.channel + } + if notification.userNum != nil { + content.userInfo["userNum"] = notification.userNum + } + +let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: nil) + + + UNUserNotificationCenter.current().add(request) { error in + if let error { + Logger.services.error("Error Scheduling Notification: \(error.localizedDescription)") } } } - return messageWithMarkdown } - return message + + // Check and debug what local notifications have been scheduled + func listScheduledNotifications() { + UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in + + for notification in notifications { + Logger.services.debug("\(notification, privacy: .public)") + } + } + } + } -func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { - - if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { - upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { - upsertDeviceConfigPacket(config: config.device, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { - upsertDisplayConfigPacket(config: config.display, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { - upsertLoRaConfigPacket(config: config.lora, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { - upsertNetworkConfigPacket(config: config.network, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { - upsertPositionConfigPacket(config: config.position, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { - upsertPowerConfigPacket(config: config.power, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { - upsertSecurityConfigPacket(config: config.security, nodeNum: nodeNum, context: context) - } -} - -func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { - - if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(config.ambientLighting) { - upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(config.cannedMessage) { - upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(config.detectionSensor) { - upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(config.externalNotification) { - upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(config.mqtt) { - upsertMqttModuleConfigPacket(config: config.mqtt, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.paxcounter(config.paxcounter) { - upsertPaxCounterModuleConfigPacket(config: config.paxcounter, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(config.rangeTest) { - upsertRangeTestModuleConfigPacket(config: config.rangeTest, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(config.serial) { - upsertSerialModuleConfigPacket(config: config.serial, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(config.telemetry) { - upsertTelemetryModuleConfigPacket(config: config.telemetry, nodeNum: nodeNum, context: context) - } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.storeForward(config.storeForward) { - upsertStoreForwardModuleConfigPacket(config: config.storeForward, nodeNum: nodeNum, context: context) - } -} - -func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedObjectContext) -> MyInfoEntity? { - - let logString = String.localizedStringWithFormat("mesh.log.myinfo %@".localized, String(myInfo.myNodeNum)) - MeshLogger.log("ℹ️ \(logString)") - - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(myInfo.myNodeNum)) - - do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - // Not Found Insert - if fetchedMyInfo.isEmpty { - - let myInfoEntity = MyInfoEntity(context: context) - myInfoEntity.peripheralId = peripheralId - myInfoEntity.myNodeNum = Int64(myInfo.myNodeNum) - myInfoEntity.rebootCount = Int32(myInfo.rebootCount) - myInfoEntity.deviceId = myInfo.deviceID - do { - try context.save() - Logger.data.info("πŸ’Ύ Saved a new myInfo for node: \(myInfo.myNodeNum.toHex(), privacy: .public)") - return myInfoEntity - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("πŸ’₯ Error Inserting New Core Data MyInfoEntity: \(nsError, privacy: .public)") - } - } else { - - fetchedMyInfo[0].peripheralId = peripheralId - fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum) - fetchedMyInfo[0].rebootCount = Int32(myInfo.rebootCount) - - do { - try context.save() - Logger.data.info("πŸ’Ύ Updated myInfo for node: \(myInfo.myNodeNum.toHex(), privacy: .public)") - return fetchedMyInfo[0] - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("πŸ’₯ Error Updating Core Data MyInfoEntity: \(nsError, privacy: .public)") - } - } - } catch { - Logger.data.error("πŸ’₯ Fetch MyInfo Error") - } - return nil -} - -func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectContext) { - - if channel.isInitialized && channel.hasSettings && channel.role != Channel.Role.disabled { - - let logString = String.localizedStringWithFormat("mesh.log.channel.received %d %@".localized, channel.index, String(fromNum)) - MeshLogger.log("πŸŽ›οΈ \(logString)") - - let fetchedMyInfoRequest = MyInfoEntity.fetchRequest() - fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", fromNum) - - do { - let fetchedMyInfo = try context.fetch(fetchedMyInfoRequest) - if fetchedMyInfo.count == 1 { - let newChannel = ChannelEntity(context: context) - newChannel.id = Int32(channel.index) - newChannel.index = Int32(channel.index) - newChannel.uplinkEnabled = channel.settings.uplinkEnabled - newChannel.downlinkEnabled = channel.settings.downlinkEnabled - newChannel.name = channel.settings.name - newChannel.role = Int32(channel.role.rawValue) - newChannel.psk = channel.settings.psk - if channel.settings.hasModuleSettings { - newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision) - newChannel.mute = channel.settings.moduleSettings.isClientMuted - } - guard let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as? NSMutableOrderedSet else { - return - } - if let oldChannel = mutableChannels.first(where: {($0 as AnyObject).index == newChannel.index }) as? ChannelEntity { - let index = mutableChannels.index(of: oldChannel as Any) - mutableChannels.replaceObject(at: index, with: newChannel) - } else { - mutableChannels.add(newChannel) - } - fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet - if newChannel.name?.lowercased() == "admin" { - fetchedMyInfo[0].adminIndex = newChannel.index - } - context.refresh(newChannel, mergeChanges: true) - do { - try context.save() - } catch { - Logger.data.error("πŸ’₯ Failed to save channel: \(error.localizedDescription, privacy: .public)") - } - Logger.data.info("πŸ’Ύ Updated MyInfo channel \(channel.index) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum)") - } else if channel.role.rawValue > 0 { - Logger.data.error("πŸ’₯Trying to save a channel to a MyInfo that does not exist: \(fromNum.toHex(), privacy: .public)") - } - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("πŸ’₯ Error Saving MyInfo Channel from ADMIN_APP \(nsError, privacy: .public)") - } - } -} - -func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - if metadata.isInitialized { - let logString = String.localizedStringWithFormat("mesh.log.device.metadata.received %@".localized, fromNum.toHex()) - MeshLogger.log("🏷️ \(logString)") - - let fetchedNodeRequest = NodeInfoEntity.fetchRequest() - fetchedNodeRequest.predicate = NSPredicate(format: "num == %lld", fromNum) - - do { - let fetchedNode = try context.fetch(fetchedNodeRequest) - let newMetadata = DeviceMetadataEntity(context: context) - newMetadata.time = Date() - newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion) - newMetadata.canShutdown = metadata.canShutdown - newMetadata.hasWifi = metadata.hasWifi_p - newMetadata.hasBluetooth = metadata.hasBluetooth_p - newMetadata.hasEthernet = metadata.hasEthernet_p - newMetadata.role = Int32(metadata.role.rawValue) - newMetadata.positionFlags = Int32(metadata.positionFlags) - // Swift does strings weird, this does work to get the version without the github hash - let lastDotIndex = metadata.firmwareVersion.lastIndex(of: ".") - var version = metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: metadata.firmwareVersion))] - version = version.dropLast() - newMetadata.firmwareVersion = String(version) - if fetchedNode.count > 0 { - fetchedNode[0].metadata = newMetadata - } else { - - if fromNum > 0 { - let newNode = createNodeInfo(num: Int64(fromNum), context: context) - newNode.metadata = newMetadata - } - } - if sessionPasskey?.count != 0 { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - } catch { - Logger.data.error("πŸ’₯ Failed to save device metadata: \(error.localizedDescription, privacy: .public)") - } - Logger.data.info("πŸ’Ύ Updated Device Metadata from Admin App Packet For: \(fromNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving MyInfo Channel from ADMIN_APP \(nsError, privacy: .public)") - } - } -} - -func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? { - - let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, String(nodeInfo.num)) - MeshLogger.log("πŸ“Ÿ \(logString)") - - guard nodeInfo.num > 0 else { return nil } - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeInfo.num)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Not Found Insert - if fetchedNode.isEmpty && nodeInfo.num > 0 { - - let newNode = NodeInfoEntity(context: context) - newNode.id = Int64(nodeInfo.num) - newNode.num = Int64(nodeInfo.num) - newNode.channel = Int32(nodeInfo.channel) - newNode.favorite = nodeInfo.isFavorite - newNode.ignored = nodeInfo.isIgnored - newNode.hopsAway = Int32(nodeInfo.hopsAway) - - if nodeInfo.hasDeviceMetrics { - let telemetry = TelemetryEntity(context: context) - telemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) - telemetry.voltage = nodeInfo.deviceMetrics.voltage - telemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization - telemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx - var newTelemetries = [TelemetryEntity]() - newTelemetries.append(telemetry) - newNode.telemetries? = NSOrderedSet(array: newTelemetries) - } - - newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - 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) - newUser.longName = nodeInfo.user.longName - newUser.shortName = nodeInfo.user.shortName - newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() - newUser.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) - Task { - Api().loadDeviceHardwareData { (hw) in - let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) - newUser.hwDisplayName = dh?.displayName - } - } - newUser.isLicensed = nodeInfo.user.isLicensed - newUser.role = Int32(nodeInfo.user.role.rawValue) - if !nodeInfo.user.publicKey.isEmpty { - newUser.pkiEncrypted = true - newUser.publicKey = nodeInfo.user.publicKey - } - newNode.user = newUser - } else if nodeInfo.num > Constants.minimumNodeNum { - let newUser = createUser(num: Int64(nodeInfo.num), context: context) - newNode.user = newUser - } - - if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { - let position = PositionEntity(context: context) - position.latest = true - position.seqNo = Int32(nodeInfo.position.seqNumber) - position.latitudeI = nodeInfo.position.latitudeI - position.longitudeI = nodeInfo.position.longitudeI - position.altitude = nodeInfo.position.altitude - position.satsInView = Int32(nodeInfo.position.satsInView) - position.speed = Int32(nodeInfo.position.groundSpeed) - position.heading = Int32(nodeInfo.position.groundTrack) - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) - var newPostions = [PositionEntity]() - newPostions.append(position) - newNode.positions? = NSOrderedSet(array: newPostions) - } - - // Look for a MyInfo - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) - - do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if fetchedMyInfo.count > 0 { - newNode.myInfo = fetchedMyInfo[0] - } - do { - try context.save() - Logger.data.info("πŸ’Ύ Saved a new Node Info For: \(String(nodeInfo.num))") - return newNode - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError)") - } - } catch { - Logger.data.error("Fetch MyInfo Error") - } - } else if nodeInfo.num > 0 { - - fetchedNode[0].id = Int64(nodeInfo.num) - fetchedNode[0].num = Int64(nodeInfo.num) - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - fetchedNode[0].snr = nodeInfo.snr - fetchedNode[0].channel = Int32(nodeInfo.channel) - fetchedNode[0].favorite = nodeInfo.isFavorite - fetchedNode[0].ignored = nodeInfo.isIgnored - fetchedNode[0].hopsAway = Int32(nodeInfo.hopsAway) - - if nodeInfo.hasUser { - if fetchedNode[0].user == nil { - fetchedNode[0].user = UserEntity(context: context) - } - // Set the public key for a user if it is empty, don't update - if fetchedNode[0].user?.publicKey == nil && !nodeInfo.user.publicKey.isEmpty { - fetchedNode[0].user?.pkiEncrypted = true - fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey - } - fetchedNode[0].user!.userId = nodeInfo.user.id - fetchedNode[0].user!.num = Int64(nodeInfo.num) - fetchedNode[0].user!.numString = String(nodeInfo.num) - fetchedNode[0].user!.longName = nodeInfo.user.longName - fetchedNode[0].user!.shortName = nodeInfo.user.shortName - 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() - fetchedNode[0].user!.hwModelId = Int32(nodeInfo.user.hwModel.rawValue) - Task { - Api().loadDeviceHardwareData { (hw) in - let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId }) - fetchedNode[0].user!.hwDisplayName = dh?.displayName - } - } - } else { - if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum { - - let newUser = createUser(num: Int64(nodeInfo.num), context: context) - fetchedNode[0].user = newUser - } - } - - if nodeInfo.hasDeviceMetrics { - - let newTelemetry = TelemetryEntity(context: context) - newTelemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) - newTelemetry.voltage = nodeInfo.deviceMetrics.voltage - newTelemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization - newTelemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx - guard let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as? NSMutableOrderedSet else { - return nil - } - fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet - } - - if nodeInfo.hasPosition { - - if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { - - let position = PositionEntity(context: context) - position.latitudeI = nodeInfo.position.latitudeI - position.longitudeI = nodeInfo.position.longitudeI - position.altitude = nodeInfo.position.altitude - position.satsInView = Int32(nodeInfo.position.satsInView) - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) - guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else { - return nil - } - fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet - } - - } - - // Look for a MyInfo - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) - - do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if fetchedMyInfo.count > 0 { - fetchedNode[0].myInfo = fetchedMyInfo[0] - } - do { - try context.save() - Logger.data.info("πŸ’Ύ [NodeInfo] saved for \(nodeInfo.num.toHex(), privacy: .public)") - return fetchedNode[0] - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("πŸ’₯ Error Saving Core Data NodeInfoEntity: \(nsError, privacy: .public)") - } - } catch { - Logger.data.error("πŸ’₯ Fetch MyInfo Error") - } - } - } catch { - Logger.data.error("πŸ’₯ Fetch NodeInfoEntity Error") - } - return nil -} - -func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { - - if let adminMessage = try? AdminMessage(serializedBytes: packet.decoded.payload) { - - if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) { - - if let cmmc = try? CannedMessageModuleConfig(serializedBytes: packet.decoded.payload) { - - if !cmmc.messages.isEmpty { - - let logString = String.localizedStringWithFormat("mesh.log.cannedmessages.messages.received %@".localized, packet.from.toHex()) - MeshLogger.log("πŸ₯« \(logString)") - - let fetchNodeRequest = NodeInfoEntity.fetchRequest() - fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - - do { - let fetchedNode = try context.fetch(fetchNodeRequest) - if fetchedNode.count == 1 { - let messages = String(cmmc.textFormatString()) - .replacingOccurrences(of: "11: ", with: "") - .replacingOccurrences(of: "\"", with: "") - .trimmingCharacters(in: .whitespacesAndNewlines) - fetchedNode[0].cannedMessageConfig?.messages = messages - do { - try context.save() - Logger.data.info("πŸ’Ύ Updated Canned Messages Messages For: \(fetchedNode[0].num.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("πŸ’₯ Error Saving NodeInfoEntity from POSITION_APP \(nsError, privacy: .public)") - } - } - } catch { - Logger.data.error("πŸ’₯ Error Deserializing ADMIN_APP packet.") - } - } - } - } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { - channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) - } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) { - deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) - } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) { - let config = adminMessage.getConfigResponse - if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { - upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { - upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { - upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { - upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { - upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { - upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { - upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) - } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { - upsertSecurityConfigPacket(config: config.security, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) - } - } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) { - let moduleConfig = adminMessage.getModuleConfigResponse - if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(moduleConfig.ambientLighting) { - upsertAmbientLightingModuleConfigPacket(config: moduleConfig.ambientLighting, nodeNum: Int64(packet.from), context: context) - } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) { - upsertCannedMessagesModuleConfigPacket(config: moduleConfig.cannedMessage, nodeNum: Int64(packet.from), context: context) - } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(moduleConfig.detectionSensor) { - upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from), context: context) - } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(moduleConfig.externalNotification) { - upsertExternalNotificationModuleConfigPacket(config: moduleConfig.externalNotification, nodeNum: Int64(packet.from), context: context) - } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(moduleConfig.mqtt) { - upsertMqttModuleConfigPacket(config: moduleConfig.mqtt, nodeNum: Int64(packet.from), context: context) - } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(moduleConfig.rangeTest) { - upsertRangeTestModuleConfigPacket(config: moduleConfig.rangeTest, nodeNum: Int64(packet.from), context: context) - } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(moduleConfig.serial) { - upsertSerialModuleConfigPacket(config: moduleConfig.serial, nodeNum: Int64(packet.from), context: context) - } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.storeForward(moduleConfig.storeForward) { - upsertStoreForwardModuleConfigPacket(config: moduleConfig.storeForward, nodeNum: Int64(packet.from), context: context) - } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(moduleConfig.telemetry) { - upsertTelemetryModuleConfigPacket(config: moduleConfig.telemetry, nodeNum: Int64(packet.from), context: context) - } - } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getRingtoneResponse(adminMessage.getRingtoneResponse) { - let ringtone = adminMessage.getRingtoneResponse - upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: Int64(packet.from), context: context) - } else { - 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) - } -} - -func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) { - - let fetchedAdminMessageRequest = MessageEntity.fetchRequest() - fetchedAdminMessageRequest.predicate = NSPredicate(format: "messageId == %lld", packet.decoded.requestID) - do { - let fetchedMessage = try context.fetch(fetchedAdminMessageRequest) - if fetchedMessage.count > 0 { - fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970) - fetchedMessage[0].ackError = Int32(RoutingError.none.rawValue) - fetchedMessage[0].receivedACK = true - fetchedMessage[0].realACK = true - fetchedMessage[0].ackSNR = packet.rxSnr - if fetchedMessage[0].fromUser != nil { - fetchedMessage[0].fromUser?.objectWillChange.send() - } - do { - try context.save() - } catch { - Logger.data.error("Failed to save admin message response as an ack: \(error.localizedDescription)") - } - } - } catch { - 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 = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - - if let paxMessage = try? Paxcount(serializedBytes: 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 { - guard let mutablePax = fetchedNode[0].pax!.mutableCopy() as? NSMutableOrderedSet else { - return - } - mutablePax.add(newPax) - fetchedNode[0].pax = mutablePax - do { - try context.save() - } catch { - Logger.data.error("Failed to save pax: \(error.localizedDescription)") - } - } else { - Logger.data.info("Node Info Not Found") - } - } - } catch { - - } -} - -func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { - - if let routingMessage = try? Routing(serializedBytes: packet.decoded.payload) { - - let routingError = RoutingError(rawValue: routingMessage.errorReason.rawValue) - - let routingErrorString = routingError?.display ?? "unknown".localized - let logString = String.localizedStringWithFormat("mesh.log.routing.message %@ %@".localized, String(packet.decoded.requestID), routingErrorString) - MeshLogger.log("πŸ•ΈοΈ \(logString)") - - let fetchMessageRequest = MessageEntity.fetchRequest() - fetchMessageRequest.predicate = NSPredicate(format: "messageId == %lld", Int64(packet.decoded.requestID)) - - do { - let fetchedMessage = try context.fetch(fetchMessageRequest) - if fetchedMessage.count > 0 { - - if fetchedMessage[0].toUser != nil { - // Real ACK from DM Recipient - if packet.to != packet.from { - fetchedMessage[0].realACK = true - } - } - fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue) - if routingMessage.errorReason == Routing.Error.none { - - fetchedMessage[0].receivedACK = true - } - fetchedMessage[0].ackSNR = packet.rxSnr - if packet.rxTime > 0 { - fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) - } else { - fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970) - } - - if fetchedMessage[0].toUser != nil { - fetchedMessage[0].toUser!.objectWillChange.send() - } else { - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", connectedNodeNum) - do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if fetchedMyInfo.count > 0 { - - for ch in fetchedMyInfo[0].channels!.array as? [ChannelEntity] ?? [] where ch.index == packet.channel { - ch.objectWillChange.send() - } - } - } catch { } - } - - } else { - return - } - try context.save() - Logger.data.info("πŸ’Ύ ACK Saved for Message: \(packet.decoded.requestID)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving ACK for message: \(packet.id) Error: \(nsError)") - } - } -} - -func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { - - if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) { - - let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from)) - MeshLogger.log("πŸ“ˆ \(logString)") - - if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) { - /// Other unhandled telemetry packets - return - } - - let telemetry = TelemetryEntity(context: context) - - let fetchNodeTelemetryRequest = NodeInfoEntity.fetchRequest() - fetchNodeTelemetryRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - - do { - let fetchedNode = try context.fetch(fetchNodeTelemetryRequest) - if fetchedNode.count == 1 { - /// Currently only Device Metrics and Environment Telemetry are supported in the app - if telemetryMessage.variant == Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) { - // Device Metrics - telemetry.airUtilTx = telemetryMessage.deviceMetrics.airUtilTx - telemetry.channelUtilization = telemetryMessage.deviceMetrics.channelUtilization - telemetry.batteryLevel = Int32(telemetryMessage.deviceMetrics.batteryLevel) - telemetry.voltage = telemetryMessage.deviceMetrics.voltage - telemetry.uptimeSeconds = Int32(telemetryMessage.deviceMetrics.uptimeSeconds) - telemetry.metricsType = 0 - Logger.statistics.info("πŸ“ˆ [Mesh Statistics] Channel Utilization: \(telemetryMessage.deviceMetrics.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.deviceMetrics.airUtilTx, privacy: .public) for Node: \(packet.from.toHex(), privacy: .public)") - } else if telemetryMessage.variant == Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) { - // Environment Metrics - telemetry.barometricPressure = telemetryMessage.environmentMetrics.barometricPressure - telemetry.current = telemetryMessage.environmentMetrics.current - telemetry.iaq = Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.iaq) - telemetry.gasResistance = telemetryMessage.environmentMetrics.gasResistance - telemetry.relativeHumidity = telemetryMessage.environmentMetrics.relativeHumidity - telemetry.temperature = telemetryMessage.environmentMetrics.temperature - telemetry.current = telemetryMessage.environmentMetrics.current - telemetry.voltage = telemetryMessage.environmentMetrics.voltage - telemetry.weight = telemetryMessage.environmentMetrics.weight - telemetry.windSpeed = telemetryMessage.environmentMetrics.windSpeed - telemetry.windGust = telemetryMessage.environmentMetrics.windGust - telemetry.windLull = telemetryMessage.environmentMetrics.windLull - telemetry.windDirection = Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.windDirection) - telemetry.metricsType = 1 - } else if telemetryMessage.variant == Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) { - // Local Stats for Live activity - telemetry.uptimeSeconds = Int32(telemetryMessage.localStats.uptimeSeconds) - telemetry.channelUtilization = telemetryMessage.localStats.channelUtilization - telemetry.airUtilTx = telemetryMessage.localStats.airUtilTx - telemetry.numPacketsTx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsTx) - telemetry.numPacketsRx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRx) - telemetry.numPacketsRxBad = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRxBad) - telemetry.numRxDupe = Int32(truncatingIfNeeded: telemetryMessage.localStats.numRxDupe) - telemetry.numTxRelay = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTxRelay) - telemetry.numTxRelayCanceled = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTxRelayCanceled) - telemetry.numOnlineNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numOnlineNodes) - telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes) - telemetry.metricsType = 4 - Logger.statistics.info("πŸ“ˆ [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)") - } - telemetry.snr = packet.rxSnr - telemetry.rssi = packet.rxRssi - telemetry.time = Date(timeIntervalSince1970: TimeInterval(Int64(truncatingIfNeeded: telemetryMessage.time))) - guard let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as? NSMutableOrderedSet else { - return - } - mutableTelemetries.add(telemetry) - if packet.rxTime > 0 { - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(packet.rxTime)) - } else { - fetchedNode[0].lastHeard = Date() - } - fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet - } - try context.save() - - Logger.data.info("πŸ’Ύ [TelemetryEntity] of type \(MetricsTypes(rawValue: Int(telemetry.metricsType))?.name ?? "Unknown Metrics Type") Saved for Node: \(packet.from.toHex())") - if telemetry.metricsType == 0 { - // Connected Device Metrics - // ------------------------ - // Low Battery notification - if connectedNode == Int64(packet.from) { - if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 { - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(UUID().uuidString)"), - title: "Critically Low Battery!", - subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")", - content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.", - target: "nodes", - path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)" - ) - ] - manager.schedule() - } - } - } else if telemetry.metricsType == 4 { - // Update our live activity if there is one running, not available on mac -#if !targetEnvironment(macCatalyst) -#if canImport(ActivityKit) - - let fifteenMinutesLater = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())! - let date = Date.now...fifteenMinutesLater - let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(uptimeSeconds: UInt32(telemetry.uptimeSeconds), - channelUtilization: telemetry.channelUtilization, - airtime: telemetry.airUtilTx, - sentPackets: UInt32(telemetry.numPacketsTx), - receivedPackets: UInt32(telemetry.numPacketsRx), - badReceivedPackets: UInt32(telemetry.numPacketsRxBad), - dupeReceivedPackets: UInt32(telemetry.numRxDupe), - packetsSentRelay: UInt32(telemetry.numTxRelay), - packetsCanceledRelay: UInt32(telemetry.numTxRelayCanceled), - nodesOnline: UInt32(telemetry.numOnlineNodes), - totalNodes: UInt32(telemetry.numTotalNodes), - timerRange: date) - - let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Node Stats Data.", sound: .default) - let updatedContent = ActivityContent(state: updatedMeshStatus, staleDate: nil) - - let meshActivity = Activity.activities.first(where: { $0.attributes.nodeNum == connectedNode }) - if meshActivity != nil { - Task { - await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration) - // await meshActivity?.update(updatedContent) - Logger.services.debug("Updated live activity.") - } - } -#endif -#endif - } - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("πŸ’₯ Error Saving Telemetry for Node \(packet.from, privacy: .public) Error: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("πŸ’₯ Error Fetching NodeInfoEntity for Node \(packet.from.toHex(), privacy: .public)") - } -} - -func textMessageAppPacket( - packet: MeshPacket, - wantRangeTestPackets: Bool, - critical: Bool = false, - connectedNode: Int64, - storeForward: Bool = false, - context: NSManagedObjectContext, - appState: AppState -) { - var messageText = String(bytes: packet.decoded.payload, encoding: .utf8) - let rangeRef = Reference(Int.self) - let rangeTestRegex = Regex { - "seq " - TryCapture(as: rangeRef) { - OneOrMore(.digit) - } transform: { match in - Int(match) - } - } - 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(serializedBytes: packet.decoded.payload) { - 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)") - let messageUsers = UserEntity.fetchRequest() - messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from]) - do { - let fetchedUsers = try context.fetch(messageUsers) - let newMessage = MessageEntity(context: context) - newMessage.messageId = Int64(packet.id) - if packet.rxTime > 0 { - newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) - } else { - newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) - } - newMessage.receivedACK = false - newMessage.snr = packet.rxSnr - newMessage.rssi = packet.rxRssi - newMessage.isEmoji = packet.decoded.emoji == 1 - newMessage.channel = Int32(packet.channel) - newMessage.portNum = Int32(packet.decoded.portnum.rawValue) - if packet.decoded.portnum == PortNum.detectionSensorApp { - if !UserDefaults.enableDetectionNotifications { - newMessage.read = true - } - } - if packet.decoded.replyID > 0 { - newMessage.replyID = Int64(packet.decoded.replyID) - } - if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != Constants.maximumNodeNum { - if !storeForwardBroadcast { - newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) - } else { - /// Make a new to user if they are unknown - newMessage.toUser = createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) - } - } - if fetchedUsers.first(where: { $0.num == packet.from }) != nil { - newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) - /// Set the public key for the message - if newMessage.fromUser?.pkiEncrypted ?? false && packet.pkiEncrypted { - newMessage.pkiEncrypted = true - newMessage.publicKey = packet.publicKey - } - /// Check for key mismatch - if let nodeKey = newMessage.fromUser?.publicKey { - if newMessage.toUser != nil && packet.pkiEncrypted && !packet.publicKey.isEmpty { - if nodeKey != newMessage.publicKey { - newMessage.fromUser?.keyMatch = false - newMessage.fromUser?.newPublicKey = newMessage.publicKey - let nodeKey = String(nodeKey.base64EncodedString()).prefix(8) - let messageKey = String(newMessage.publicKey?.base64EncodedString() ?? "No Key").prefix(8) - Logger.data.error("πŸ”‘ Key mismatch original key: \(nodeKey, privacy: .public) . . . new key: \(messageKey, privacy: .public) . . .") - } - } - } else if packet.pkiEncrypted { - /// We have no key, set it if it is not empty - if !packet.publicKey.isEmpty { - newMessage.fromUser?.pkiEncrypted = true - newMessage.fromUser?.publicKey = packet.publicKey - } - } - } else { - /// Make a new from user if they are unknown - newMessage.fromUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) - } - if packet.rxTime > 0 { - newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - } else { - newMessage.fromUser?.userNode?.lastHeard = Date() - } - newMessage.messagePayload = messageText - newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText!) - if packet.to != Constants.maximumNodeNum && newMessage.fromUser != nil { - newMessage.fromUser?.lastMessage = Date() - } - var messageSaved = false - do { - try context.save() - Logger.data.info("πŸ’Ύ Saved a new message for \(newMessage.messageId)") - messageSaved = true - - if messageSaved { - if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications { - return - } - if newMessage.fromUser != nil && newMessage.toUser != nil { - // Set Unread Message Indicators - if packet.to == connectedNode { - appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0 - } - if !(newMessage.fromUser?.mute ?? false) { - // Create an iOS Notification for the received DM message and schedule it immediately - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", - content: messageText!, - target: "messages", - path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)", - messageId: newMessage.messageId, - channel: newMessage.channel, - userNum: Int64(packet.from), - critical: critical - ) - ] - manager.schedule() - Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") - } - } else if newMessage.fromUser != nil && newMessage.toUser == nil { - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) - - do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if !fetchedMyInfo.isEmpty { - appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages - - for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { - if channel.index == newMessage.channel { - context.refresh(channel, mergeChanges: true) - } - if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { - // Create an iOS Notification for the received private channel message and schedule it immediately - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", - content: messageText!, - target: "messages", - path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", - messageId: newMessage.messageId, - channel: newMessage.channel, - userNum: Int64(newMessage.fromUser?.userId ?? "0"), - critical: critical) - ] - manager.schedule() - Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") - } - } - } - } catch { - // Handle error - } - } - } - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Failed to save new MessageEntity \(nsError)") - } - } catch { - Logger.data.error("Fetch Message To and From Users Error") - } - } -} - -func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from)) - MeshLogger.log("πŸ“ \(logString)") - - let fetchWaypointRequest = WaypointEntity.fetchRequest() - fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(packet.id)) - - do { - - if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) { - let fetchedWaypoint = try context.fetch(fetchWaypointRequest) - if fetchedWaypoint.isEmpty { - let waypoint = WaypointEntity(context: context) - - waypoint.id = Int64(packet.id) - waypoint.name = waypointMessage.name - waypoint.longDescription = waypointMessage.description_p - waypoint.latitudeI = waypointMessage.latitudeI - waypoint.longitudeI = waypointMessage.longitudeI - waypoint.icon = Int64(waypointMessage.icon) - waypoint.locked = Int64(waypointMessage.lockedTo) - if waypointMessage.expire >= 1 { - waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) - } else { - waypoint.expire = nil - } - waypoint.created = Date() - do { - try context.save() - 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 - let longitude = Double(waypoint.longitudeI) / 1e7 - manager.notifications = [ - Notification( - id: ("notification.id.\(waypoint.id)"), - title: "New Waypoint Received", - subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")", - content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")", - target: "map", - path: "meshtastic:///map?waypointid=\(waypoint.id)" - ) - ] - Logger.data.debug("meshtastic:///map?waypointid=\(waypoint.id)") - manager.schedule() - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError)") - } - } else { - fetchedWaypoint[0].id = Int64(packet.id) - fetchedWaypoint[0].name = waypointMessage.name - fetchedWaypoint[0].longDescription = waypointMessage.description_p - fetchedWaypoint[0].latitudeI = waypointMessage.latitudeI - fetchedWaypoint[0].longitudeI = waypointMessage.longitudeI - fetchedWaypoint[0].icon = Int64(waypointMessage.icon) - fetchedWaypoint[0].locked = Int64(waypointMessage.lockedTo) - if waypointMessage.expire >= 1 { - fetchedWaypoint[0].expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) - } else { - fetchedWaypoint[0].expire = nil - } - fetchedWaypoint[0].lastUpdated = Date() - do { - try context.save() - Logger.data.info("πŸ’Ύ Updated Node Waypoint App Packet For: \(fetchedWaypoint[0].id)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError)") - } - } - } - } catch { - Logger.mesh.error("Error Deserializing WAYPOINT_APP packet.") - } +struct Notification { + var id: String + var title: String + var subtitle: String? + var content: String + var target: String? + var path: String? + var messageId: Int64? + var channel: Int32? + var userNum: Int64? } From 194171dd158f4550388a42283072b7b3ec9ea2ee Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:19:13 -0800 Subject: [PATCH 5/6] Update MeshPackets.swift --- Meshtastic/Helpers/MeshPackets.swift | 70 ++++++++++++++-------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 6d13a2d8..d7a80c17 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -961,43 +961,44 @@ func textMessageAppPacket( manager.schedule() Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") } - } else if newMessage.toUser == nil { + } else if newMessage.fromUser != nil && newMessage.toUser == nil { let fetchMyInfoRequest = MyInfoEntity.fetchRequest() -fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) -do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if !fetchedMyInfo.isEmpty { - appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages + do { + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if !fetchedMyInfo.isEmpty { + appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages - for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { - if channel.index == newMessage.channel { - context.refresh(channel, mergeChanges: true) - } - if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { - // Create an iOS Notification for the received private channel message and schedule it immediately - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? \"unknown\".localized)", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? \"?\")", - content: messageText!, - target: "messages", - path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", - messageId: newMessage.messageId, - channel: newMessage.channel, - userNum: Int64(newMessage.fromUser?.userId ?? "0"), - critical: critical) - ] - manager.schedule() - Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? \"unknown\".localized)") - } - } - } -} catch { - // Handle error -} + for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { + if channel.index == newMessage.channel { + context.refresh(channel, mergeChanges: true) + } + if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { + // Create an iOS Notification for the received private channel message and schedule it immediately + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", + content: messageText!, + target: "messages", + path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", + messageId: newMessage.messageId, + channel: newMessage.channel, + userNum: Int64(newMessage.fromUser?.userId ?? "0"), + critical: critical) + ] + manager.schedule() + Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") + } + } + } + } catch { + // Handle error + } + } } } catch { context.rollback() @@ -1010,7 +1011,6 @@ do { } } - func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from)) From 4a0e0ca0c0e330117317c2f895e63c24143a9a40 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:20:43 -0800 Subject: [PATCH 6/6] Update LocalNotificationManager.swift --- .../Helpers/LocalNotificationManager.swift | 93 ++++++++++--------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index 7fe17865..817c90a4 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -4,36 +4,36 @@ import OSLog class LocalNotificationManager { - var notifications = [Notification]() + var notifications = [Notification]() let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title: "πŸ‘ \(Tapbacks.thumbsUp.description)", options: []) let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title: "πŸ‘Ž \(Tapbacks.thumbsDown.description)", options: []) let replyInputAction = UNTextInputNotificationAction(identifier: "messageNotification.replyInputAction", title: "reply".localized, options: []) - // Step 1 Request Permissions for notifications - private func requestAuthorization() { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in + // Step 1 Request Permissions for notifications + private func requestAuthorization() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in - if granted == true && error == nil { + if granted == true && error == nil { self.scheduleNotifications() - } - } - } + } + } + } func schedule() { - UNUserNotificationCenter.current().getNotificationSettings { settings in - switch settings.authorizationStatus { - case .notDetermined: + UNUserNotificationCenter.current().getNotificationSettings { settings in + switch settings.authorizationStatus { + case .notDetermined: self.requestAuthorization() - case .authorized, .provisional: - self.scheduleNotifications() - default: - break // Do nothing - } - } - } + case .authorized, .provisional: + self.scheduleNotifications() + default: + break // Do nothing + } + } + } - // This function iterates over the Notification objects in the notifications array and schedules them for delivery in the future - private func scheduleNotifications() { + // This function iterates over the Notification objects in the notifications array and schedules them for delivery in the future + private func scheduleNotifications() { let messageNotificationCategory = UNNotificationCategory( identifier: "messageNotificationCategory", actions: [thumbsUpAction, thumbsDownAction, replyInputAction], @@ -43,15 +43,13 @@ class LocalNotificationManager { UNUserNotificationCenter.current().setNotificationCategories([messageNotificationCategory]) - for notification in notifications { - let content = UNMutableNotificationContent() - if let subtitle = notification.subtitle { - content.subtitle = subtitle - } - content.title = notification.title - content.body = notification.content - content.sound = .default - content.interruptionLevel = .timeSensitive + for notification in notifications { + let content = UNMutableNotificationContent() + content.subtitle = notification.subtitle + content.title = notification.title + content.body = notification.content + content.sound = .default + content.interruptionLevel = .timeSensitive if notification.target != nil { content.userInfo["target"] = notification.target @@ -69,38 +67,41 @@ class LocalNotificationManager { if notification.userNum != nil { content.userInfo["userNum"] = notification.userNum } + if notification.critical { + content.sound = UNNotificationSound.defaultCritical + } -let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: nil) + let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: nil) - - UNUserNotificationCenter.current().add(request) { error in + UNUserNotificationCenter.current().add(request) { error in if let error { Logger.services.error("Error Scheduling Notification: \(error.localizedDescription)") } - } - } - } + } + } + } - // Check and debug what local notifications have been scheduled - func listScheduledNotifications() { - UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in + // Check and debug what local notifications have been scheduled + func listScheduledNotifications() { + UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in - for notification in notifications { + for notification in notifications { Logger.services.debug("\(notification, privacy: .public)") - } - } - } + } + } + } } struct Notification { - var id: String - var title: String - var subtitle: String? - var content: String + var id: String + var title: String + var subtitle: String + var content: String var target: String? var path: String? var messageId: Int64? var channel: Int32? var userNum: Int64? + var critical: Bool = false }