// // 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 } 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 } 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.") } }