diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift index 456001d8..a088e55d 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift @@ -61,7 +61,7 @@ extension AccessoryManager { self.updateState(.communicating) self.connectionEventTask = Task { for await event in eventStream { - self.didReceive(event) + await self.didReceive(event) } Logger.transport.info("[Accessory] Event stream closed") } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift index d8e70f0e..46d4f767 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift @@ -65,7 +65,7 @@ extension AccessoryManager { Logger.services.error("โš ๏ธ Client Notification: \(clientNotification.message, privacy: .public)") } - func handleMyInfo(_ myNodeInfo: MyNodeInfo) { + func handleMyInfo(_ myNodeInfo: MyNodeInfo) async { // TODO: this works for connections like BLE that have a uniqueId, but what about ones like serial? guard let connectedDeviceId = activeConnection?.device.id.uuidString else { Logger.services.error("โš ๏ธ Failed to decode MyInfo, no connected device ID") @@ -75,7 +75,8 @@ extension AccessoryManager { updateDevice(key: \.num, value: Int64(myNodeInfo.myNodeNum)) - if let myInfo = myInfoPacket(myInfo: myNodeInfo, peripheralId: connectedDeviceId, context: context) { + if let myInfoId = await MeshPackets.shared.myInfoPacket(myInfo: myNodeInfo, peripheralId: connectedDeviceId), + let myInfo = try? context.existingObject(with: myInfoId) as? MyInfoEntity { if let bleName = myInfo.bleName { updateDevice(key: \.name, value: bleName) updateDevice(key: \.longName, value: bleName) @@ -97,7 +98,7 @@ extension AccessoryManager { initializeTAKBridge() } - func handleNodeInfo(_ nodeInfo: NodeInfo) { + func handleNodeInfo(_ nodeInfo: NodeInfo) async { if let continuation = self.firstDatabaseNodeInfoContinuation { continuation.resume() self.firstDatabaseNodeInfoContinuation = nil @@ -109,10 +110,13 @@ extension AccessoryManager { } // Check if we're in database retrieval mode to defer saves for performance - let isRetrievingDatabase = if case .retrievingDatabase = self.state { true } else { false } + // Commented out: No need to defer save when nodeInfoPacket is now happening off the main thread + // let isRetrievingDatabase = if case .retrievingDatabase = self.state { true } else { false } // TODO: nodeInfoPacket's channel: parameter is not used - if let nodeInfo = nodeInfoPacket(nodeInfo: nodeInfo, channel: 0, context: context, deferSave: isRetrievingDatabase) { + // deferSave hard coded: No need to defer save when nodeInfoPacket is now happening off the main thread + if let nodeInfoId = await MeshPackets.shared.nodeInfoPacket(nodeInfo: nodeInfo, channel: 0, deferSave: false), + let nodeInfo = try? context.existingObject(with: nodeInfoId) as? NodeInfoEntity { if let activeDevice = activeConnection?.device, activeDevice.num == nodeInfo.num { if let user = nodeInfo.user { updateDevice(deviceId: activeDevice.id, key: \.shortName, value: user.shortName ?? "?") @@ -138,24 +142,24 @@ extension AccessoryManager { } - func handleChannel(_ channel: Channel) { + func handleChannel(_ channel: Channel) async { guard let deviceNum = activeConnection?.device.num else { Logger.data.error("Attempt to process channel information when no connected device.") return } - channelPacket(channel: channel, fromNum: Int64(truncatingIfNeeded: deviceNum), context: context) + await MeshPackets.shared.channelPacket(channel: channel, fromNum: Int64(truncatingIfNeeded: deviceNum)) } - func handleConfig(_ config: Config) { + func handleConfig(_ config: Config) async { guard let device = activeConnection?.device, let deviceNum = device.num, let longName = device.longName else { Logger.data.error("Attempt to process channel information when no connected device.") return } // Local config parses out the variants. Should we do that here maybe? - localConfig(config: config, context: context, nodeNum: Int64(truncatingIfNeeded: deviceNum), nodeLongName: longName) + await MeshPackets.shared.localConfig(config: config, nodeNum: Int64(truncatingIfNeeded: deviceNum), nodeLongName: longName) // Handle Timezone if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { @@ -169,12 +173,12 @@ extension AccessoryManager { } } - func handleModuleConfig(_ moduleConfigPacket: ModuleConfig) { + func handleModuleConfig(_ moduleConfigPacket: ModuleConfig) async { guard let device = activeConnection?.device, let deviceNum = device.num, let longName = device.longName else { Logger.services.error("Attempt to process channel information when no connected device.") return } - moduleConfig(config: moduleConfigPacket, context: context, nodeNum: Int64(truncatingIfNeeded: deviceNum), nodeLongName: longName) + await MeshPackets.shared.moduleConfig(config: moduleConfigPacket, nodeNum: Int64(truncatingIfNeeded: deviceNum), nodeLongName: longName) // Get Canned Message Message List if the Module is Canned Messages if moduleConfigPacket.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfigPacket.cannedMessage) { try? getCannedMessageModuleMessages(destNum: deviceNum, wantResponse: true) @@ -185,7 +189,7 @@ extension AccessoryManager { } } - func handleDeviceMetadata(_ metadata: DeviceMetadata) { + func handleDeviceMetadata(_ metadata: DeviceMetadata) async { // Note: moved firmware version check to be inline with connection process guard let device = activeConnection?.device, let deviceNum = device.num else { Logger.services.error("Attempt to process device metadata information when no connected device.") @@ -196,7 +200,7 @@ extension AccessoryManager { updateDevice(key: \.firmwareVersion, value: metadata.firmwareVersion) - deviceMetadataPacket(metadata: metadata, fromNum: deviceNum, context: context) + await MeshPackets.shared.deviceMetadataPacket(metadata: metadata, fromNum: deviceNum) } internal func tryClearExistingChannels() { @@ -227,17 +231,16 @@ extension AccessoryManager { } - func handleTextMessageAppPacket(_ packet: MeshPacket) { + func handleTextMessageAppPacket(_ packet: MeshPacket) async { guard let device = activeConnection?.device, let deviceNum = device.num else { Logger.services.error("Attempt to handle text message when no connected device.") return } - textMessageAppPacket( + await MeshPackets.shared.textMessageAppPacket( packet: packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: deviceNum, - context: context, appState: appState ) @@ -322,25 +325,27 @@ extension AccessoryManager { case .UNRECOGNIZED: Logger.mesh.info("\("๐Ÿ“ฎ Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")") case .routerTextDirect: - Logger.mesh.info("\("๐Ÿ’ฌ Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")") - textMessageAppPacket( - packet: packet, - wantRangeTestPackets: false, - connectedNode: connectedNodeNum, - storeForward: true, - context: context, - appState: appState - ) + Task { + Logger.mesh.info("\("๐Ÿ’ฌ Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")") + await MeshPackets.shared.textMessageAppPacket( + packet: packet, + wantRangeTestPackets: false, + connectedNode: connectedNodeNum, + storeForward: true, + appState: appState + ) + } case .routerTextBroadcast: - Logger.mesh.info("\("โœ‰๏ธ Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")") - textMessageAppPacket( - packet: packet, - wantRangeTestPackets: false, - connectedNode: connectedNodeNum, - storeForward: true, - context: context, - appState: appState - ) + Task { + Logger.mesh.info("\("โœ‰๏ธ Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")") + await MeshPackets.shared.textMessageAppPacket( + packet: packet, + wantRangeTestPackets: false, + connectedNode: connectedNodeNum, + storeForward: true, + appState: appState + ) + } } } } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 97f005c3..731a2ac1 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -166,7 +166,7 @@ extension AccessoryManager { // Update local database with the new node info // FUTURE: after https://github.com/meshtastic/firmware/pull/8495 is merged, `favorite: true` becomes `favorite: (connectedDeviceRole != DeviceRoles.clientBase)` - upsertNodeInfoPacket(packet: nodeMeshPacket, favorite: true, context: context) + await MeshPackets.shared.upsertNodeInfoPacket(packet: nodeMeshPacket, favorite: true) } } catch { Logger.data.error("Failed to decode contact data: \(error.localizedDescription, privacy: .public)") @@ -864,7 +864,7 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: toUser.num) return Int64(meshPacket.id) @@ -920,7 +920,7 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) return Int64(meshPacket.id) } @@ -1003,7 +1003,7 @@ extension AccessoryManager { let messageDescription = "๐Ÿ›Ÿ Saved Detection Sensor Module Config for \(toUser.longName ?? "Unknown".localized)" try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertDetectionSensorModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertDetectionSensorModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) return Int64(meshPacket.id) } @@ -1057,7 +1057,7 @@ extension AccessoryManager { let messageDescription = "๐Ÿ›Ÿ Saved External Notification Module Config for \(toUser.longName ?? "Unknown".localized)" try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertExternalNotificationModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertExternalNotificationModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) return Int64(meshPacket.id) } @@ -1087,7 +1087,7 @@ extension AccessoryManager { let messageDescription = "๐Ÿ›Ÿ Saved PAX Counter Module Config for \(toUser.longName ?? "Unknown".localized)" try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertPaxCounterModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertPaxCounterModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) return Int64(meshPacket.id) } @@ -1117,7 +1117,7 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: toUser.num, context: context) return Int64(meshPacket.id) } @@ -1148,7 +1148,7 @@ extension AccessoryManager { let messageDescription = "๐Ÿ›Ÿ Saved MQTT Config for \(toUser.longName ?? "Unknown".localized)" try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertMqttModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertMqttModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) return Int64(meshPacket.id) } @@ -1178,7 +1178,7 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertRangeTestModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertRangeTestModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) return Int64(meshPacket.id) } @@ -1208,7 +1208,7 @@ extension AccessoryManager { let messageDescription = "๐Ÿ›Ÿ Saved Serial Module Config for \(toUser.longName ?? "Unknown".localized)" try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertSerialModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertSerialModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) return Int64(meshPacket.id) } @@ -1387,7 +1387,7 @@ extension AccessoryManager { let messageDescription = "๐Ÿ›Ÿ Saved Store & Forward Module Config for \(toUser.longName ?? "Unknown".localized)" try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertStoreForwardModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertStoreForwardModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) return Int64(meshPacket.id) } @@ -1623,7 +1623,7 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertPositionConfigPacket(config: config, nodeNum: toUser.num, context: context) + try await MeshPackets.shared.upsertPositionConfigPacket(config: config, nodeNum: toUser.num) return Int64(meshPacket.id) } @@ -1677,7 +1677,7 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertPowerConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertPowerConfigPacket(config: config, nodeNum: toUser.num) return Int64(meshPacket.id) } @@ -1733,7 +1733,7 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertNetworkConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertNetworkConfigPacket(config: config, nodeNum: toUser.num) return Int64(meshPacket.id) } @@ -1764,7 +1764,7 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertSecurityConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertSecurityConfigPacket(config: config, nodeNum: toUser.num) return Int64(meshPacket.id) } @@ -1898,7 +1898,7 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) + await MeshPackets.shared.upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey) return Int64(meshPacket.id) } @@ -1928,7 +1928,7 @@ extension AccessoryManager { let messageDescription = "Saved Telemetry Module Config for \(toUser.longName ?? "Unknown".localized)" try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertTelemetryModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) + await MeshPackets.shared.upsertTelemetryModuleConfigPacket(config: config, nodeNum: toUser.num, context: context) return Int64(meshPacket.id) } @@ -1981,7 +1981,7 @@ extension AccessoryManager { let messageDescription = "๐Ÿ›Ÿ Saved Display Config for \(toUser.longName ?? "Unknown".localized)" try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) + await MeshPackets.shared.upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey) return Int64(meshPacket.id) } @@ -2060,7 +2060,7 @@ extension AccessoryManager { let messageDescription = "๐Ÿ›Ÿ Saved Device Config for \(toUser.longName ?? "Unknown".localized)" try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) + await MeshPackets.shared.upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey) return Int64(meshPacket.id) } @@ -2089,7 +2089,7 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) - upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) + await MeshPackets.shared.upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey) return Int64(meshPacket.id) } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index e92fce8b..6ab94a0b 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -198,7 +198,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { return } - _ = clearStaleNodes(nodeExpireDays: Int(UserDefaults.purgeStaleNodeDays), context: self.context) + _ = await MeshPackets.shared.clearStaleNodes(nodeExpireDays: Int(UserDefaults.purgeStaleNodeDays)) try await withTaskCancellationHandler { var toRadio: ToRadio = ToRadio() @@ -370,13 +370,13 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { } } - func didReceive(_ event: ConnectionEvent) { + func didReceive(_ event: ConnectionEvent) async { packetsReceived += 1 switch event { case .data(let fromRadio): // Logger.transport.info("โœ… [Accessory] didReceive: \(fromRadio.payloadVariant.debugDescription)") - self.processFromRadio(fromRadio) + await self.processFromRadio(fromRadio) Task { await self.heartbeatResponseTimer?.cancel(withReason: "Data packet received") await self.heartbeatTimer?.reset(delay: .seconds(15.0)) @@ -484,7 +484,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { } } - private func processFromRadio(_ decodedInfo: FromRadio) { + private func processFromRadio(_ decodedInfo: FromRadio) async { switch decodedInfo.payloadVariant { case .mqttClientProxyMessage(let mqttClientProxyMessage): handleMqttClientProxyMessage(mqttClientProxyMessage) @@ -493,12 +493,12 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { handleClientNotification(clientNotification) case .myInfo(let myNodeInfo): - handleMyInfo(myNodeInfo) + await handleMyInfo(myNodeInfo) case .packet(let packet): // All received packets get passed through updateAnyPacketFrom to update lastHeard, rxSnr, etc. (like firmware's NodeDB::updateFrom). if let connectedNodeNum = self.activeDeviceNum { - updateAnyPacketFrom(packet: packet, activeDeviceNum: connectedNodeNum, context: context) + await MeshPackets.shared.updateAnyPacketFrom(packet: packet, activeDeviceNum: connectedNodeNum) } else { Logger.mesh.error("๐Ÿ•ธ๏ธ Unable to determine connectedNodeNum for updateAnyPacketFrom. Skipping.") } @@ -507,20 +507,20 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { if case let .decoded(data) = packet.payloadVariant { switch data.portnum { case .textMessageApp, .detectionSensorApp, .alertApp: - handleTextMessageAppPacket(packet) + await handleTextMessageAppPacket(packet) case .remoteHardwareApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .positionApp: - upsertPositionPacket(packet: packet, context: context) + await MeshPackets.shared.upsertPositionPacket(packet: packet) case .waypointApp: - waypointPacket(packet: packet, context: context) + await MeshPackets.shared.waypointPacket(packet: packet) case .nodeinfoApp: guard let connectedNodeNum = self.activeDeviceNum else { Logger.mesh.error("๐Ÿ•ธ๏ธ Unable to determine connectedNodeNum for node info upsert.") return } if packet.from != connectedNodeNum { - upsertNodeInfoPacket(packet: packet, context: context) + await MeshPackets.shared.upsertNodeInfoPacket(packet: packet) } else { Logger.mesh.error("๐Ÿ•ธ๏ธ Received a node info packet from ourselves over the mesh. Dropping.") } @@ -529,16 +529,16 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { Logger.mesh.error("๐Ÿ•ธ๏ธ No active connection. Unable to determine connectedNodeNum for routingPacket.") return } - routingPacket(packet: packet, connectedNodeNum: deviceNum, context: context) + await MeshPackets.shared.routingPacket(packet: packet, connectedNodeNum: deviceNum) case .adminApp: - adminAppPacket(packet: packet, context: context) + await MeshPackets.shared.adminAppPacket(packet: packet) case .replyApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Reply App handling as a text message") guard let deviceNum = activeConnection?.device.num else { Logger.mesh.error("๐Ÿ•ธ๏ธ No active connection. Unable to determine connectedNodeNum for replyApp.") return } - textMessageAppPacket(packet: packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: deviceNum, context: context, appState: appState) + await MeshPackets.shared.textMessageAppPacket(packet: packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: deviceNum, appState: appState) case .ipTunnelApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for IP Tunnel App UNHANDLED UNHANDLED") case .serialApp: @@ -555,11 +555,10 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { return } if wantRangeTestPackets { - textMessageAppPacket( + await MeshPackets.shared.textMessageAppPacket( packet: packet, wantRangeTestPackets: true, connectedNode: deviceNum, - context: context, appState: appState ) } else { @@ -570,7 +569,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { Logger.mesh.error("๐Ÿ•ธ๏ธ No active connection. Unable to determine connectedNodeNum for telemetryApp.") return } - telemetryPacket(packet: packet, connectedNode: deviceNum, context: context) + await MeshPackets.shared.telemetryPacket(packet: packet, connectedNode: deviceNum) case .textMessageCompressedApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Text Message Compressed App UNHANDLED") case .zpsApp: @@ -592,7 +591,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Neighbor Info App UNHANDLED \((try? neighborInfo.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } case .paxcounterApp: - paxCounterPacket(packet: decodedInfo.packet, context: context) + await MeshPackets.shared.paxCounterPacket(packet: decodedInfo.packet) case .mapReportApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received Map Report App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .UNRECOGNIZED: @@ -615,19 +614,19 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { } case .nodeInfo(let nodeInfo): - handleNodeInfo(nodeInfo) + await handleNodeInfo(nodeInfo) case .channel(let channel): - handleChannel(channel) + await handleChannel(channel) case .config(let config): - handleConfig(config) + await handleConfig(config) case .moduleConfig(let moduleConfig): - handleModuleConfig(moduleConfig) + await handleModuleConfig(moduleConfig) case .metadata(let metadata): - handleDeviceMetadata(metadata) + await handleDeviceMetadata(metadata) case .deviceuiConfig: #if DEBUG diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index ffb716c2..4b478a30 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -2,6 +2,7 @@ import Foundation import SwiftUI import OSLog +@MainActor class LocalNotificationManager { var notifications = [Notification]() @@ -10,20 +11,23 @@ class LocalNotificationManager { 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() + private func requestAuthorization() async { + do { + let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) + if granted { + self.scheduleNotifications() } + } catch { + Logger.services.error("Error requesting notification authorization: \(error.localizedDescription, privacy: .public)") } } func schedule() { - UNUserNotificationCenter.current().getNotificationSettings { settings in + Task { @MainActor in + let settings = await UNUserNotificationCenter.current().notificationSettings() switch settings.authorizationStatus { case .notDetermined: - self.requestAuthorization() + await self.requestAuthorization() case .authorized, .provisional: self.scheduleNotifications() default: @@ -97,7 +101,7 @@ class LocalNotificationManager { for notification in notifications { if let userInfo = notification.content.userInfo["messageId"] as? Int64, userInfo == messageId { Logger.services.debug("Cancelling notification with id: \(notification.identifier)") - center.removePendingNotificationRequests(withIdentifiers: [notification.identifier]) + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notification.identifier]) } } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 8b7a7423..54e1661f 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -55,1040 +55,1048 @@ func generateMessageMarkdown (message: String) -> String { return message } -func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { - switch config.payloadVariant { - case .bluetooth: - upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum, context: context) - case .device: - upsertDeviceConfigPacket(config: config.device, nodeNum: nodeNum, context: context) - case .display: - upsertDisplayConfigPacket(config: config.display, nodeNum: nodeNum, context: context) - case .lora: - upsertLoRaConfigPacket(config: config.lora, nodeNum: nodeNum, context: context) - case .network: - upsertNetworkConfigPacket(config: config.network, nodeNum: nodeNum, context: context) - case .position: - upsertPositionConfigPacket(config: config.position, nodeNum: nodeNum, context: context) - case .power: - upsertPowerConfigPacket(config: config.power, nodeNum: nodeNum, context: context) - case .security: - upsertSecurityConfigPacket(config: config.security, nodeNum: nodeNum, context: context) - default: +actor MeshPackets { + static let shared = MeshPackets() + + // Create an actor-level background context + // We keep this alive so sequential writes happen on the same context (efficient) + lazy var backgroundContext: NSManagedObjectContext = { + let ctx = PersistenceController.shared.container.newBackgroundContext() + ctx.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // Handle conflicts automatically + return ctx + }() + + func localConfig (config: Config, nodeNum: Int64, nodeLongName: String) async { + switch config.payloadVariant { + case .bluetooth: + await self.upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum) + case .device: + await self.upsertDeviceConfigPacket(config: config.device, nodeNum: nodeNum) + case .display: + await self.upsertDisplayConfigPacket(config: config.display, nodeNum: nodeNum) + case .lora: + await self.upsertLoRaConfigPacket(config: config.lora, nodeNum: nodeNum) + case .network: + await self.upsertNetworkConfigPacket(config: config.network, nodeNum: nodeNum) + case .position: + await self.upsertPositionConfigPacket(config: config.position, nodeNum: nodeNum) + case .power: + await self.upsertPowerConfigPacket(config: config.power, nodeNum: nodeNum) + case .security: + await self.upsertSecurityConfigPacket(config: config.security, nodeNum: nodeNum) + default: #if DEBUG - Logger.services.error("โ‰๏ธ Unknown Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)") + Logger.services.error("โ‰๏ธ Unknown Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)") #endif + } } -} - -func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { - switch config.payloadVariant { - case .ambientLighting: - upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum, context: context) - case .cannedMessage: - upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum, context: context) - case .detectionSensor: - upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum, context: context) - case .externalNotification: - upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum, context: context) - case .mqtt: - upsertMqttModuleConfigPacket(config: config.mqtt, nodeNum: nodeNum, context: context) - case .paxcounter: - upsertPaxCounterModuleConfigPacket(config: config.paxcounter, nodeNum: nodeNum, context: context) - case .rangeTest: - upsertRangeTestModuleConfigPacket(config: config.rangeTest, nodeNum: nodeNum, context: context) - case .serial: - upsertSerialModuleConfigPacket(config: config.serial, nodeNum: nodeNum, context: context) - case .telemetry: - upsertTelemetryModuleConfigPacket(config: config.telemetry, nodeNum: nodeNum, context: context) - case .storeForward: - upsertStoreForwardModuleConfigPacket(config: config.storeForward, nodeNum: nodeNum, context: context) - default: + + func moduleConfig (config: ModuleConfig, nodeNum: Int64, nodeLongName: String) async { + switch config.payloadVariant { + case .ambientLighting: + await self.upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum) + case .cannedMessage: + await self.upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum) + case .detectionSensor: + await self.upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum) + case .externalNotification: + await self.upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum) + case .mqtt: + await self.upsertMqttModuleConfigPacket(config: config.mqtt, nodeNum: nodeNum) + case .paxcounter: + await self.upsertPaxCounterModuleConfigPacket(config: config.paxcounter, nodeNum: nodeNum) + case .rangeTest: + await self.upsertRangeTestModuleConfigPacket(config: config.rangeTest, nodeNum: nodeNum) + case .serial: + await self.upsertSerialModuleConfigPacket(config: config.serial, nodeNum: nodeNum) + case .telemetry: + await self.upsertTelemetryModuleConfigPacket(config: config.telemetry, nodeNum: nodeNum) + case .storeForward: + await self.upsertStoreForwardModuleConfigPacket(config: config.storeForward, nodeNum: nodeNum) + default: #if DEBUG - Logger.services.error("โ‰๏ธ Unknown Module Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)") + Logger.services.error("โ‰๏ธ Unknown Module Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)") #endif - } -} - -func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedObjectContext) -> MyInfoEntity? { - - let logString = String.localizedStringWithFormat("MyInfo received: %@".localized, String(myInfo.myNodeNum)) - Logger.mesh.info("โ„น๏ธ \(logString, privacy: .public)") - - 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)) - Logger.mesh.info("๐ŸŽ›๏ธ \(logString, privacy: .public)") - - 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.isMuted - } - 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 - 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, privacy: .public) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum, privacy: .public)") - } 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("Device Metadata received from: %@".localized, fromNum.toHex()) - Logger.mesh.info("๐Ÿท๏ธ \(logString, privacy: .public)") - - 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) - newMetadata.excludedModules = Int32(metadata.excludedModules) - // 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, deferSave: Bool = false) -> NodeInfoEntity? { - - let logString = String.localizedStringWithFormat("[NodeInfo] received for: %@".localized, String(nodeInfo.num)) - Logger.mesh.info("๐Ÿ“Ÿ \(logString, privacy: .public)") - - 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) - } - if nodeInfo.lastHeard > 0 { - newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - } else { - newNode.firstHeard = Date() - newNode.lastHeard = Date() - } - newNode.snr = nodeInfo.snr - if nodeInfo.hasUser { - - let newUser = UserEntity(context: context) - newUser.userId = nodeInfo.num.toHex() - 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 - } - /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default - if nodeInfo.user.hasIsUnmessagable { - newUser.unmessagable = nodeInfo.user.isUnmessagable - } else { - let roles = [2, 4, 5, 6, 7, 10, 11] - let containsRole = roles.contains(Int(newUser.role)) - if containsRole { - newUser.unmessagable = true - } else { - newUser.unmessagable = false - }} - newNode.user = newUser - } else if nodeInfo.num > Constants.minimumNodeNum { - do { - let newUser = try createUser(num: Int64(nodeInfo.num), context: context) - newNode.user = newUser - } catch CoreDataError.invalidInput(let message) { - Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)") - } catch { - Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") - } - } - - 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 + + func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String) async -> NSManagedObjectID? { + let context = self.backgroundContext + return await context.perform { + let logString = String.localizedStringWithFormat("MyInfo received: %@".localized, String(myInfo.myNodeNum)) + Logger.mesh.info("โ„น๏ธ \(logString, privacy: .public)") + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) - + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(myInfo.myNodeNum)) + do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if fetchedMyInfo.count > 0 { - newNode.myInfo = fetchedMyInfo[0] - } - do { - if !deferSave { - try context.save() - Logger.data.info("๐Ÿ’พ Saved a new Node Info For: \(String(nodeInfo.num), privacy: .public)") - } - return newNode - } 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") - } - } 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.num.toHex() - 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) - /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default - if nodeInfo.user.hasIsUnmessagable { - fetchedNode[0].user?.unmessagable = nodeInfo.user.isUnmessagable - } else { - let roles = [-1, 2, 4, 5, 6, 7, 10, 11] - let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1)) - if containsRole { - fetchedNode[0].user?.unmessagable = true - } else { - fetchedNode[0].user?.unmessagable = false - } - } - Task { - Api().loadDeviceHardwareData { (hw: [DeviceHardware]) in - guard !hw.isEmpty, - let firstNode = fetchedNode.first, - let user = firstNode.user else { - Logger.data.error("Error: Required DeviceHardware data is missing or array is empty.") - return - } - - let dh = hw.first(where: { $0.hwModel == user.hwModelId }) - - if let deviceHardware = dh { - firstNode.user?.hwDisplayName = deviceHardware.displayName - } else { - Logger.data.error("No matching hardware model found for ID: \(user.hwModelId, privacy: .public)") - } - } - } - } else { - if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum { + // 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 { - let newUser = try createUser(num: Int64(nodeInfo.num), context: context) - fetchedNode[0].user = newUser - } catch CoreDataError.invalidInput(let message) { - Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)") - } catch { - Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") - } - } - } - - 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 - } - mutableTelemetries.add(newTelemetry) - 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 { - if !deferSave { try context.save() - Logger.data.info("๐Ÿ’พ [NodeInfo] saved for \(nodeInfo.num.toHex(), privacy: .public)") + Logger.data.info("๐Ÿ’พ Saved a new myInfo for node: \(myInfo.myNodeNum.toHex(), privacy: .public)") + return myInfoEntity.objectID + } 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].objectID + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ Error Updating Core Data MyInfoEntity: \(nsError, 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") } + return nil } - } 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) { - let logString = String.localizedStringWithFormat("Canned Messages Messages Received For: %@".localized, packet.from.toHex()) - Logger.mesh.info("๐Ÿฅซ \(logString, privacy: .public)") - - 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) - .components(separatedBy: "\n").first ?? "" - fetchedNode[0].cannedMessageConfig?.messages = messages - do { - try context.save() - Logger.data.info("๐Ÿ’พ Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex() ?? "Unknown".localized, 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) { - if let rt = try? RTTTLConfig(serializedBytes: packet.decoded.payload) { - upsertRtttlConfigPacket(ringtone: rt.ringtone, nodeNum: Int64(packet.from), context: context) - } - } else { - Logger.mesh.error("๐Ÿ•ธ๏ธ MESH PACKET received Admin App UNHANDLED \((try? packet.decoded.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + + func channelPacket (channel: Channel, fromNum: Int64) async { + let context = self.backgroundContext + await context.perform { + self.channelPacket(channel: channel, fromNum: fromNum, context: context) } - // 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].relayNode = Int64(packet.relayNode) - fetchedMessage[0].ackSNR = packet.rxSnr - if fetchedMessage[0].fromUser != nil { - fetchedMessage[0].fromUser?.objectWillChange.send() - } + + nonisolated private 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)) + Logger.mesh.info("๐ŸŽ›๏ธ \(logString, privacy: .public)") + + let fetchedMyInfoRequest = MyInfoEntity.fetchRequest() + fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", fromNum) + do { - try context.save() + 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.isMuted + } + 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 + 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, privacy: .public) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum, privacy: .public)") + } 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 { - Logger.data.error("Failed to save admin message response as an ack: \(error.localizedDescription, privacy: .public)") + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ Error Saving MyInfo Channel from ADMIN_APP \(nsError, privacy: .public)") } } - } catch { - Logger.data.error("Failed to fetch admin message by requestID: \(error.localizedDescription, privacy: .public)") } -} -func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("PAX Counter message received from: %@".localized, String(packet.from)) - Logger.mesh.info("๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ \(logString, privacy: .public)") - - 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 + + func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.deviceMetadataPacket(metadata: metadata, fromNum: fromNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated private func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + if metadata.isInitialized { + let logString = String.localizedStringWithFormat("Device Metadata received from: %@".localized, fromNum.toHex()) + Logger.mesh.info("๐Ÿท๏ธ \(logString, privacy: .public)") + + 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) + newMetadata.excludedModules = Int32(metadata.excludedModules) + // 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) } - mutablePax.add(newPax) - fetchedNode[0].pax = mutablePax do { try context.save() } catch { - Logger.data.error("Failed to save pax: \(error.localizedDescription, privacy: .public)") + Logger.data.error("๐Ÿ’ฅ Failed to save device metadata: \(error.localizedDescription, privacy: .public)") } - } else { - Logger.data.info("Node Info Not Found") + 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)") } } - } 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("Routing received for RequestID: %@ Ack Status: %@".localized, String(packet.decoded.requestID), routingErrorString) - Logger.mesh.info("๐Ÿ•ธ๏ธ \(logString, privacy: .public)") - - 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 + + func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, deferSave: Bool = false) async -> NSManagedObjectID? { + let context = self.backgroundContext + return await context.perform { () -> NSManagedObjectID? in + let logString = String.localizedStringWithFormat("[NodeInfo] received for: %@".localized, String(nodeInfo.num)) + Logger.mesh.info("๐Ÿ“Ÿ \(logString, privacy: .public)") + + 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) } - } - fetchedMessage[0].relayNode = Int64(packet.relayNode) - fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue) - if routingMessage.errorReason == Routing.Error.none { - fetchedMessage[0].receivedACK = true - fetchedMessage[0].relays += 1 - } - - 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 { + if nodeInfo.lastHeard > 0 { + newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + } else { + newNode.firstHeard = Date() + newNode.lastHeard = Date() + } + newNode.snr = nodeInfo.snr + if nodeInfo.hasUser { + + let newUser = UserEntity(context: context) + newUser.userId = nodeInfo.num.toHex() + 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 + } + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default + if nodeInfo.user.hasIsUnmessagable { + newUser.unmessagable = nodeInfo.user.isUnmessagable + } else { + let roles = [2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(newUser.role)) + if containsRole { + newUser.unmessagable = true + } else { + newUser.unmessagable = false + }} + newNode.user = newUser + } else if nodeInfo.num > Constants.minimumNodeNum { + do { + let newUser = try createUser(num: Int64(nodeInfo.num), context: context) + newNode.user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } + } + + 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", connectedNodeNum) + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) + 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() + newNode.myInfo = fetchedMyInfo[0] + } + do { + if !deferSave { + try context.save() + Logger.data.info("๐Ÿ’พ Saved a new Node Info For: \(String(nodeInfo.num), privacy: .public)") + } + return newNode.objectID + } 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") + } + } 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.num.toHex() + 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) + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default + if nodeInfo.user.hasIsUnmessagable { + fetchedNode[0].user?.unmessagable = nodeInfo.user.isUnmessagable + } else { + let roles = [-1, 2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1)) + if containsRole { + fetchedNode[0].user?.unmessagable = true + } else { + fetchedNode[0].user?.unmessagable = false } } - } catch { } - } - - } else { - return - } - try context.save() - Logger.data.info("๐Ÿ’พ ACK Saved for Message: \(packet.decoded.requestID, privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving ACK for message: \(packet.id, privacy: .public) Error: \(nsError, privacy: .public)") - } - } -} - -func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { - Task { @MainActor in - if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) { - 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) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) { - /// 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 - Logger.data.info("๐Ÿ“ˆ [Telemetry] Device Metrics Received for Node: \(packet.from.toHex(), privacy: .public)") - telemetry.airUtilTx = telemetryMessage.deviceMetrics.hasAirUtilTx.then(telemetryMessage.deviceMetrics.airUtilTx) - telemetry.channelUtilization = telemetryMessage.deviceMetrics.hasChannelUtilization.then(telemetryMessage.deviceMetrics.channelUtilization) - telemetry.batteryLevel = telemetryMessage.deviceMetrics.hasBatteryLevel.then(Int32(telemetryMessage.deviceMetrics.batteryLevel)) - telemetry.voltage = telemetryMessage.deviceMetrics.hasVoltage.then(telemetryMessage.deviceMetrics.voltage) - telemetry.uptimeSeconds = telemetryMessage.deviceMetrics.hasUptimeSeconds.then(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 - Logger.data.info("๐Ÿ“ˆ [Telemetry] Environment Metrics Received for Node: \(packet.from.toHex(), privacy: .public)") - telemetry.barometricPressure = telemetryMessage.environmentMetrics.hasBarometricPressure.then(telemetryMessage.environmentMetrics.barometricPressure) - telemetry.iaq = telemetryMessage.environmentMetrics.hasIaq.then(Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.iaq)) - telemetry.gasResistance = telemetryMessage.environmentMetrics.hasGasResistance.then(telemetryMessage.environmentMetrics.gasResistance) - telemetry.relativeHumidity = telemetryMessage.environmentMetrics.hasRelativeHumidity.then(telemetryMessage.environmentMetrics.relativeHumidity) - telemetry.temperature = telemetryMessage.environmentMetrics.hasTemperature.then(telemetryMessage.environmentMetrics.temperature) - telemetry.current = telemetryMessage.environmentMetrics.hasCurrent.then(telemetryMessage.environmentMetrics.current) - telemetry.voltage = telemetryMessage.environmentMetrics.hasVoltage.then(telemetryMessage.environmentMetrics.voltage) - telemetry.weight = telemetryMessage.environmentMetrics.hasWeight.then(telemetryMessage.environmentMetrics.weight) - telemetry.distance = telemetryMessage.environmentMetrics.hasDistance.then(telemetryMessage.environmentMetrics.distance) - telemetry.windSpeed = telemetryMessage.environmentMetrics.hasWindSpeed.then(telemetryMessage.environmentMetrics.windSpeed) - telemetry.windGust = telemetryMessage.environmentMetrics.hasWindGust.then(telemetryMessage.environmentMetrics.windGust) - telemetry.windLull = telemetryMessage.environmentMetrics.hasWindLull.then(telemetryMessage.environmentMetrics.windLull) - telemetry.windDirection = telemetryMessage.environmentMetrics.hasWindDirection.then(Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.windDirection)) - telemetry.irLux = telemetryMessage.environmentMetrics.hasIrLux.then(telemetryMessage.environmentMetrics.irLux) - telemetry.lux = telemetryMessage.environmentMetrics.hasLux.then(telemetryMessage.environmentMetrics.lux) - telemetry.whiteLux = telemetryMessage.environmentMetrics.hasWhiteLux.then(telemetryMessage.environmentMetrics.whiteLux) - telemetry.uvLux = telemetryMessage.environmentMetrics.hasUvLux.then(telemetryMessage.environmentMetrics.uvLux) - telemetry.radiation = telemetryMessage.environmentMetrics.hasRadiation.then(telemetryMessage.environmentMetrics.radiation) - telemetry.rainfall1H = telemetryMessage.environmentMetrics.hasRainfall1H.then(telemetryMessage.environmentMetrics.rainfall1H) - telemetry.rainfall24H = telemetryMessage.environmentMetrics.hasRainfall24H.then(telemetryMessage.environmentMetrics.rainfall24H) - telemetry.soilTemperature = telemetryMessage.environmentMetrics.hasSoilTemperature.then(telemetryMessage.environmentMetrics.soilTemperature) - telemetry.soilMoisture = telemetryMessage.environmentMetrics.hasSoilMoisture.then(telemetryMessage.environmentMetrics.soilMoisture) - 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)") - } else if telemetryMessage.variant == Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) { - Logger.data.info("๐Ÿ“ˆ [Telemetry] Power Metrics Received for Node: \(packet.from.toHex(), privacy: .public)") - telemetry.powerCh1Voltage = telemetryMessage.powerMetrics.hasCh1Voltage.then(telemetryMessage.powerMetrics.ch1Voltage) - telemetry.powerCh1Current = telemetryMessage.powerMetrics.hasCh1Current.then(telemetryMessage.powerMetrics.ch1Current) - telemetry.powerCh2Voltage = telemetryMessage.powerMetrics.hasCh2Voltage.then(telemetryMessage.powerMetrics.ch2Voltage) - telemetry.powerCh2Current = telemetryMessage.powerMetrics.hasCh2Current.then(telemetryMessage.powerMetrics.ch2Current) - telemetry.powerCh3Voltage = telemetryMessage.powerMetrics.hasCh3Voltage.then(telemetryMessage.powerMetrics.ch3Voltage) - telemetry.powerCh3Current = telemetryMessage.powerMetrics.hasCh3Current.then(telemetryMessage.powerMetrics.ch3Current) - telemetry.metricsType = 2 - } - 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", privacy: .public) Saved for Node: \(packet.from.toHex(), privacy: .public)") - if telemetry.metricsType == 0 { - // Connected Device Metrics - // ------------------------ - // Low Battery notification - if connectedNode == Int64(packet.from) { - let batteryLevel = telemetry.batteryLevel ?? 0 - if UserDefaults.lowBatteryNotifications && batteryLevel > 0 && 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?.formatted(.number) ?? Constants.nilValueIndicator)% 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: telemetry.uptimeSeconds.map { UInt32($0) }, - 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.") + Api().loadDeviceHardwareData { (hw: [DeviceHardware]) in + guard !hw.isEmpty, + let firstNode = fetchedNode.first, + let user = firstNode.user else { + Logger.data.error("Error: Required DeviceHardware data is missing or array is empty.") + return + } + + let dh = hw.first(where: { $0.hwModel == user.hwModelId }) + + if let deviceHardware = dh { + firstNode.user?.hwDisplayName = deviceHardware.displayName + } else { + Logger.data.error("No matching hardware model found for ID: \(user.hwModelId, privacy: .public)") + } + } } - } -#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 { - Logger.mesh.info("๐Ÿ’ฌ \("Message received from the text message app.".localized, privacy: .public)") - 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) - } - if packet.relayNode != 0 { - newMessage.relayNode = Int64(packet.relayNode) - } - 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) - } - // Updated logic for handling toUser - 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 if storeForwardBroadcast { - // For S&F broadcast messages, treat as a channel message (not a DM) - newMessage.toUser = nil } else { - do { - let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) - newMessage.toUser = newUser - } catch CoreDataError.invalidInput(let message) { - Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.to, privacy: .public) Error: \(message, privacy: .public)") - } catch { - Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.to, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum { + do { + let newUser = try createUser(num: Int64(nodeInfo.num), context: context) + fetchedNode[0].user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } } } - } - 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) . . .") + 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 + } + mutableTelemetries.add(newTelemetry) + 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 } - } 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 - do { - let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) - let newNode = NodeInfoEntity(context: context) - newNode.id = Int64(newUser.num) - newNode.num = Int64(newUser.num) - newNode.user = newUser - newMessage.fromUser = newUser - } catch CoreDataError.invalidInput(let message) { - Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") - } catch { - Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") - } - } - 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, privacy: .public)") - messageSaved = true - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Failed to save new MessageEntity \(nsError, privacy: .public)") - } - // Send notifications if the message saved properly to core data - 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 { - let unreadCount = newMessage.toUser?.unreadMessages(context: context, skipLastMessageCheck: true) ?? 0 // skipLastMessageCheck=true because we don't update lastMessage on our own connected node - Task { @MainActor in - appState?.unreadDirectMessages = unreadCount - } - } - if !(newMessage.fromUser?.mute ?? false) && newMessage.isEmoji == false { - // Create an iOS Notification for the received DM message - 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.isEmoji ? newMessage.replyID : 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, privacy: .public)") - } - } else if newMessage.fromUser != nil && newMessage.toUser == nil { + + // Look for a MyInfo let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) + do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) - if !fetchedMyInfo.isEmpty { - appState?.unreadChannelMessages = fetchedMyInfo[0].unreadMessages(context: context) - for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { - if channel.index == newMessage.channel { - context.refresh(channel, mergeChanges: true) + if fetchedMyInfo.count > 0 { + fetchedNode[0].myInfo = fetchedMyInfo[0] + } + do { + if !deferSave { + try context.save() + Logger.data.info("๐Ÿ’พ [NodeInfo] saved for \(nodeInfo.num.toHex(), privacy: .public)") + } + return fetchedNode[0].objectID + } 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) async { + let context = self.backgroundContext + await context.perform { + 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) { + let logString = String.localizedStringWithFormat("Canned Messages Messages Received For: %@".localized, packet.from.toHex()) + Logger.mesh.info("๐Ÿฅซ \(logString, privacy: .public)") + + 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) + .components(separatedBy: "\n").first ?? "" + fetchedNode[0].cannedMessageConfig?.messages = messages + do { + try context.save() + Logger.data.info("๐Ÿ’พ Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex() ?? "Unknown".localized, privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ Error Saving NodeInfoEntity from POSITION_APP \(nsError, privacy: .public)") } - if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications && newMessage.isEmoji == false { - // Create an iOS Notification for the received channel message + } + } catch { + Logger.data.error("๐Ÿ’ฅ Error Deserializing ADMIN_APP packet.") + } + } + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { + self.channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) { + self.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) { + MeshPackets.shared.upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { + MeshPackets.shared.upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { + self.upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { + self.upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { + self.upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { + self.upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { + self.upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { + self.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) { + self.upsertAmbientLightingModuleConfigPacket(config: moduleConfig.ambientLighting, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) { + self.upsertCannedMessagesModuleConfigPacket(config: moduleConfig.cannedMessage, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(moduleConfig.detectionSensor) { + self.upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(moduleConfig.externalNotification) { + self.upsertExternalNotificationModuleConfigPacket(config: moduleConfig.externalNotification, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(moduleConfig.mqtt) { + self.upsertMqttModuleConfigPacket(config: moduleConfig.mqtt, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(moduleConfig.rangeTest) { + self.upsertRangeTestModuleConfigPacket(config: moduleConfig.rangeTest, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(moduleConfig.serial) { + self.upsertSerialModuleConfigPacket(config: moduleConfig.serial, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.storeForward(moduleConfig.storeForward) { + self.upsertStoreForwardModuleConfigPacket(config: moduleConfig.storeForward, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(moduleConfig.telemetry) { + self.upsertTelemetryModuleConfigPacket(config: moduleConfig.telemetry, nodeNum: Int64(packet.from), context: context) + } + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getRingtoneResponse(adminMessage.getRingtoneResponse) { + if let rt = try? RTTTLConfig(serializedBytes: packet.decoded.payload) { + self.upsertRtttlConfigPacket(ringtone: rt.ringtone, nodeNum: Int64(packet.from), context: context) + } + } else { + Logger.mesh.error("๐Ÿ•ธ๏ธ MESH PACKET received Admin App UNHANDLED \((try? packet.decoded.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + } + // 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. + self.adminResponseAck(packet: packet, context: context) + } + } + } + + nonisolated private 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].relayNode = Int64(packet.relayNode) + 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, privacy: .public)") + } + } + } catch { + Logger.data.error("Failed to fetch admin message by requestID: \(error.localizedDescription, privacy: .public)") + } + + } + + func paxCounterPacket (packet: MeshPacket) async { + let context = self.backgroundContext + await context.perform { + let logString = String.localizedStringWithFormat("PAX Counter message received from: %@".localized, String(packet.from)) + Logger.mesh.info("๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ \(logString, privacy: .public)") + + 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, privacy: .public)") + } + } else { + Logger.data.info("Node Info Not Found") + } + } + } catch { + + } + } + } + + func routingPacket (packet: MeshPacket, connectedNodeNum: Int64) async { + let context = self.backgroundContext + await context.perform { + 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("Routing received for RequestID: %@ Ack Status: %@".localized, String(packet.decoded.requestID), routingErrorString) + Logger.mesh.info("๐Ÿ•ธ๏ธ \(logString, privacy: .public)") + + 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].relayNode = Int64(packet.relayNode) + fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue) + if routingMessage.errorReason == Routing.Error.none { + fetchedMessage[0].receivedACK = true + fetchedMessage[0].relays += 1 + } + + 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, privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving ACK for message: \(packet.id, privacy: .public) Error: \(nsError, privacy: .public)") + } + } + } + } + + func telemetryPacket(packet: MeshPacket, connectedNode: Int64) async { + let context = self.backgroundContext + + await context.perform { + if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) { + 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) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) { + /// 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 + Logger.data.info("๐Ÿ“ˆ [Telemetry] Device Metrics Received for Node: \(packet.from.toHex(), privacy: .public)") + telemetry.airUtilTx = telemetryMessage.deviceMetrics.hasAirUtilTx.then(telemetryMessage.deviceMetrics.airUtilTx) + telemetry.channelUtilization = telemetryMessage.deviceMetrics.hasChannelUtilization.then(telemetryMessage.deviceMetrics.channelUtilization) + telemetry.batteryLevel = telemetryMessage.deviceMetrics.hasBatteryLevel.then(Int32(telemetryMessage.deviceMetrics.batteryLevel)) + telemetry.voltage = telemetryMessage.deviceMetrics.hasVoltage.then(telemetryMessage.deviceMetrics.voltage) + telemetry.uptimeSeconds = telemetryMessage.deviceMetrics.hasUptimeSeconds.then(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 + Logger.data.info("๐Ÿ“ˆ [Telemetry] Environment Metrics Received for Node: \(packet.from.toHex(), privacy: .public)") + telemetry.barometricPressure = telemetryMessage.environmentMetrics.hasBarometricPressure.then(telemetryMessage.environmentMetrics.barometricPressure) + telemetry.iaq = telemetryMessage.environmentMetrics.hasIaq.then(Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.iaq)) + telemetry.gasResistance = telemetryMessage.environmentMetrics.hasGasResistance.then(telemetryMessage.environmentMetrics.gasResistance) + telemetry.relativeHumidity = telemetryMessage.environmentMetrics.hasRelativeHumidity.then(telemetryMessage.environmentMetrics.relativeHumidity) + telemetry.temperature = telemetryMessage.environmentMetrics.hasTemperature.then(telemetryMessage.environmentMetrics.temperature) + telemetry.current = telemetryMessage.environmentMetrics.hasCurrent.then(telemetryMessage.environmentMetrics.current) + telemetry.voltage = telemetryMessage.environmentMetrics.hasVoltage.then(telemetryMessage.environmentMetrics.voltage) + telemetry.weight = telemetryMessage.environmentMetrics.hasWeight.then(telemetryMessage.environmentMetrics.weight) + telemetry.distance = telemetryMessage.environmentMetrics.hasDistance.then(telemetryMessage.environmentMetrics.distance) + telemetry.windSpeed = telemetryMessage.environmentMetrics.hasWindSpeed.then(telemetryMessage.environmentMetrics.windSpeed) + telemetry.windGust = telemetryMessage.environmentMetrics.hasWindGust.then(telemetryMessage.environmentMetrics.windGust) + telemetry.windLull = telemetryMessage.environmentMetrics.hasWindLull.then(telemetryMessage.environmentMetrics.windLull) + telemetry.windDirection = telemetryMessage.environmentMetrics.hasWindDirection.then(Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.windDirection)) + telemetry.irLux = telemetryMessage.environmentMetrics.hasIrLux.then(telemetryMessage.environmentMetrics.irLux) + telemetry.lux = telemetryMessage.environmentMetrics.hasLux.then(telemetryMessage.environmentMetrics.lux) + telemetry.whiteLux = telemetryMessage.environmentMetrics.hasWhiteLux.then(telemetryMessage.environmentMetrics.whiteLux) + telemetry.uvLux = telemetryMessage.environmentMetrics.hasUvLux.then(telemetryMessage.environmentMetrics.uvLux) + telemetry.radiation = telemetryMessage.environmentMetrics.hasRadiation.then(telemetryMessage.environmentMetrics.radiation) + telemetry.rainfall1H = telemetryMessage.environmentMetrics.hasRainfall1H.then(telemetryMessage.environmentMetrics.rainfall1H) + telemetry.rainfall24H = telemetryMessage.environmentMetrics.hasRainfall24H.then(telemetryMessage.environmentMetrics.rainfall24H) + telemetry.soilTemperature = telemetryMessage.environmentMetrics.hasSoilTemperature.then(telemetryMessage.environmentMetrics.soilTemperature) + telemetry.soilMoisture = telemetryMessage.environmentMetrics.hasSoilMoisture.then(telemetryMessage.environmentMetrics.soilMoisture) + 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)") + } else if telemetryMessage.variant == Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) { + Logger.data.info("๐Ÿ“ˆ [Telemetry] Power Metrics Received for Node: \(packet.from.toHex(), privacy: .public)") + telemetry.powerCh1Voltage = telemetryMessage.powerMetrics.hasCh1Voltage.then(telemetryMessage.powerMetrics.ch1Voltage) + telemetry.powerCh1Current = telemetryMessage.powerMetrics.hasCh1Current.then(telemetryMessage.powerMetrics.ch1Current) + telemetry.powerCh2Voltage = telemetryMessage.powerMetrics.hasCh2Voltage.then(telemetryMessage.powerMetrics.ch2Voltage) + telemetry.powerCh2Current = telemetryMessage.powerMetrics.hasCh2Current.then(telemetryMessage.powerMetrics.ch2Current) + telemetry.powerCh3Voltage = telemetryMessage.powerMetrics.hasCh3Voltage.then(telemetryMessage.powerMetrics.ch3Voltage) + telemetry.powerCh3Current = telemetryMessage.powerMetrics.hasCh3Current.then(telemetryMessage.powerMetrics.ch3Current) + telemetry.metricsType = 2 + } + 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", privacy: .public) Saved for Node: \(packet.from.toHex(), privacy: .public)") + if telemetry.metricsType == 0 { + // Connected Device Metrics + // ------------------------ + // Low Battery notification + if connectedNode == Int64(packet.from) { + let batteryLevel = telemetry.batteryLevel ?? 0 + Task {@MainActor in + if UserDefaults.lowBatteryNotifications && batteryLevel > 0 && 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?.formatted(.number) ?? Constants.nilValueIndicator)% 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: telemetry.uptimeSeconds.map { UInt32($0) }, + 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, + appState: AppState? + ) async { + let context = self.backgroundContext + await context.perform { + 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 { + Logger.mesh.info("๐Ÿ’ฌ \("Message received from the text message app.".localized, privacy: .public)") + 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) + } + if packet.relayNode != 0 { + newMessage.relayNode = Int64(packet.relayNode) + } + 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) + } + // Updated logic for handling toUser + 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 if storeForwardBroadcast { + // For S&F broadcast messages, treat as a channel message (not a DM) + newMessage.toUser = nil + } else { + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) + newMessage.toUser = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.to, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.to, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } + } + } + 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 + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + let newNode = NodeInfoEntity(context: context) + newNode.id = Int64(newUser.num) + newNode.num = Int64(newUser.num) + newNode.user = newUser + newMessage.fromUser = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } + } + 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, privacy: .public)") + messageSaved = true + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Failed to save new MessageEntity \(nsError, privacy: .public)") + } + // Send notifications if the message saved properly to core data + 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 { + let unreadCount = newMessage.toUser?.unreadMessages(context: context, skipLastMessageCheck: true) ?? 0 // skipLastMessageCheck=true because we don't update lastMessage on our own connected node + Task { @MainActor in + appState?.unreadDirectMessages = unreadCount + } + } + if !(newMessage.fromUser?.mute ?? false) && newMessage.isEmoji == false { + // Create an iOS Notification for the received DM message + Task {@MainActor in let manager = LocalNotificationManager() manager.notifications = [ Notification( @@ -1097,134 +1105,177 @@ func textMessageAppPacket( subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText!, target: "messages", - path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.isEmoji ? newMessage.replyID : newMessage.messageId)", + path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.isEmoji ? newMessage.replyID : newMessage.messageId)", messageId: newMessage.messageId, channel: newMessage.channel, - userNum: Int64(newMessage.fromUser?.userId ?? "0"), + userNum: Int64(packet.from), critical: critical ) ] manager.schedule() + Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)") } } + } 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 { + Task {@MainActor in + appState?.unreadChannelMessages = fetchedMyInfo[0].unreadMessages(context: context) + 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 && newMessage.isEmoji == false { + // Create an iOS Notification for the received channel message + 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.isEmoji ? newMessage.replyID : 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, privacy: .public)") + } + } + } + } + } catch { + // Handle error + } } - } catch { - // Handle error } + } catch { + Logger.data.error("Fetch Message To and From Users Error") } } - } catch { - Logger.data.error("Fetch Message To and From Users Error") } } -} - -func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Waypoint Packet received from node: %@".localized, String(packet.from)) - Logger.mesh.info("๐Ÿ“ \(logString, privacy: .public)") - - do { - if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) { - // Fetch waypoint by waypointMessage.id, not packet.id - let fetchWaypointRequest = WaypointEntity.fetchRequest() - fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(waypointMessage.id)) - - let fetchedWaypoint = try context.fetch(fetchWaypointRequest) - // Fetch the node info to get the short name - var nodeShortName: String = "?" - let fetchNodeRequest = NodeInfoEntity.fetchRequest() - fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + func waypointPacket (packet: MeshPacket) async { + let context = self.backgroundContext + await context.perform { + let logString = String.localizedStringWithFormat("Waypoint Packet received from node: %@".localized, String(packet.from)) + Logger.mesh.info("๐Ÿ“ \(logString, privacy: .public)") + do { - let fetchedNode = try context.fetch(fetchNodeRequest) - if let node = fetchedNode.first, let user = node.user { - nodeShortName = user.shortName ?? node.user?.userId ?? String(packet.from.toHex()) - } - } catch { - Logger.data.error("Failed to fetch NodeInfoEntity for node \(packet.from.toHex(), privacy: .public): \(error)") - } - if fetchedWaypoint.isEmpty { - // Create a new waypoint - let waypoint = WaypointEntity(context: context) - waypoint.id = Int64(waypointMessage.id) // Use waypointMessage.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, privacy: .public)") - 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 From \(nodeShortName)", - 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, privacy: .public)") - manager.schedule() - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") - } - } else { - // Update existing waypoint - let existingWaypoint = fetchedWaypoint[0] - if existingWaypoint.locked == 0 || existingWaypoint.locked == packet.from { - let currentTime = Int64(Date().timeIntervalSince1970) - if waypointMessage.expire > 0 && waypointMessage.expire <= currentTime { - context.delete(existingWaypoint) + if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) { + // Fetch waypoint by waypointMessage.id, not packet.id + let fetchWaypointRequest = WaypointEntity.fetchRequest() + fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(waypointMessage.id)) + + let fetchedWaypoint = try context.fetch(fetchWaypointRequest) + // Fetch the node info to get the short name + var nodeShortName: String = "?" + let fetchNodeRequest = NodeInfoEntity.fetchRequest() + fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + do { + let fetchedNode = try context.fetch(fetchNodeRequest) + if let node = fetchedNode.first, let user = node.user { + nodeShortName = user.shortName ?? node.user?.userId ?? String(packet.from.toHex()) + } + } catch { + Logger.data.error("Failed to fetch NodeInfoEntity for node \(packet.from.toHex(), privacy: .public): \(error)") + } + if fetchedWaypoint.isEmpty { + // Create a new waypoint + let waypoint = WaypointEntity(context: context) + waypoint.id = Int64(waypointMessage.id) // Use waypointMessage.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("๐Ÿ’พ Deleted a waypoint") + Logger.data.info("๐Ÿ’พ Added Node Waypoint App Packet For: \(waypoint.id, privacy: .public)") + + Task { @MainActor in + 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 From \(nodeShortName)", + 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, privacy: .public)") + manager.schedule() + } } catch { context.rollback() let nsError = error as NSError Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") } } else { - existingWaypoint.name = waypointMessage.name - existingWaypoint.longDescription = waypointMessage.description_p - existingWaypoint.latitudeI = waypointMessage.latitudeI - existingWaypoint.longitudeI = waypointMessage.longitudeI - existingWaypoint.icon = Int64(waypointMessage.icon) - existingWaypoint.locked = Int64(waypointMessage.lockedTo) - if waypointMessage.expire >= 1 { - existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) - } else { - existingWaypoint.expire = nil - } - existingWaypoint.lastUpdated = Date() - do { - try context.save() - Logger.data.info("๐Ÿ’พ Updated Node Waypoint App Packet For: \(existingWaypoint.id, privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + // Update existing waypoint + let existingWaypoint = fetchedWaypoint[0] + if existingWaypoint.locked == 0 || existingWaypoint.locked == packet.from { + let currentTime = Int64(Date().timeIntervalSince1970) + if waypointMessage.expire > 0 && waypointMessage.expire <= currentTime { + context.delete(existingWaypoint) + do { + try context.save() + Logger.data.info("๐Ÿ’พ Deleted a waypoint") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + } + } else { + existingWaypoint.name = waypointMessage.name + existingWaypoint.longDescription = waypointMessage.description_p + existingWaypoint.latitudeI = waypointMessage.latitudeI + existingWaypoint.longitudeI = waypointMessage.longitudeI + existingWaypoint.icon = Int64(waypointMessage.icon) + existingWaypoint.locked = Int64(waypointMessage.lockedTo) + if waypointMessage.expire >= 1 { + existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } else { + existingWaypoint.expire = nil + } + existingWaypoint.lastUpdated = Date() + do { + try context.save() + Logger.data.info("๐Ÿ’พ Updated Node Waypoint App Packet For: \(existingWaypoint.id, privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") + } + } } } } + } catch { + Logger.mesh.error("Error Deserializing WAYPOINT_APP packet.") } } - } catch { - Logger.mesh.error("Error Deserializing WAYPOINT_APP packet.") } } + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c56644a5..ed5b2af2 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -8,263 +8,398 @@ import CoreData import MeshtasticProtobufs import OSLog -public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) -> Bool { - var nodeExpireTime: TimeInterval { - return TimeInterval(-nodeExpireDays * 86400) +extension MeshPackets { + public func clearStaleNodes(nodeExpireDays: Int) async -> Bool { + let context = self.backgroundContext + return await context.perform { + return self.clearStaleNodes(nodeExpireDays: nodeExpireDays, context: context) + } } - var nodePKIExpireTime: TimeInterval { - return TimeInterval((nodeExpireDays < 7 ? -7 : -nodeExpireDays) * 86400) - } - - if nodeExpireDays == 0 { - // Purge Disabled - Logger.data.info("๐Ÿ’พ [NodeInfoEntity] Skip clearing stale nodes") + + nonisolated public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) -> Bool { + var nodeExpireTime: TimeInterval { + return TimeInterval(-nodeExpireDays * 86400) + } + var nodePKIExpireTime: TimeInterval { + return TimeInterval((nodeExpireDays < 7 ? -7 : -nodeExpireDays) * 86400) + } + + if nodeExpireDays == 0 { + // Purge Disabled + Logger.data.info("๐Ÿ’พ [NodeInfoEntity] Skip clearing stale nodes") + return false + } + let fetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") + fetchRequest.predicate = NSPredicate(format: "favorite == false AND ignored == false AND ((user.pkiEncrypted == NO AND lastHeard < %@) OR (user.pkiEncrypted == YES AND lastHeard < %@))", + NSDate(timeIntervalSinceNow: nodeExpireTime), NSDate(timeIntervalSinceNow: nodePKIExpireTime)) + let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + batchDeleteRequest.resultType = .resultTypeCount + + do { + Logger.data.info("๐Ÿ’พ [NodeInfoEntity] Clearing nodes older than \(nodeExpireDays) days") + if let batchDeleteResult = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult { + try context.save() + let deletedNodes = batchDeleteResult.result as? Int ?? 0 + Logger.data.info("๐Ÿ’พ [NodeInfoEntity] Cleared \(deletedNodes) stale nodes") + if deletedNodes > 0 { + return true + } + } else { + Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] bad delete results") + } + } catch { + context.rollback() + Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] Error deleting stale nodes") + } return false } - let fetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") - fetchRequest.predicate = NSPredicate(format: "favorite == false AND ignored == false AND ((user.pkiEncrypted == NO AND lastHeard < %@) OR (user.pkiEncrypted == YES AND lastHeard < %@))", - NSDate(timeIntervalSinceNow: nodeExpireTime), NSDate(timeIntervalSinceNow: nodePKIExpireTime)) - let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - batchDeleteRequest.resultType = .resultTypeCount - - do { - Logger.data.info("๐Ÿ’พ [NodeInfoEntity] Clearing nodes older than \(nodeExpireDays) days") - if let batchDeleteResult = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult { - try context.save() - let deletedNodes = batchDeleteResult.result as? Int ?? 0 - Logger.data.info("๐Ÿ’พ [NodeInfoEntity] Cleared \(deletedNodes) stale nodes") - if deletedNodes > 0 { + + func clearPax(destNum: Int64) async -> Bool { + let context = self.backgroundContext + return await context.perform { + return self.clearPax(destNum: destNum, context: context) + } + } + + nonisolated public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool { + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(destNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let newPax = [PaxCounterLog]() + fetchedNode[0].pax? = NSOrderedSet(array: newPax) + do { + try context.save() return true + + } catch { + context.rollback() + return false } - } else { - Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] bad delete results") - } - } catch { - context.rollback() - Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] Error deleting stale nodes") - } - return false -} - -public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool { - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(destNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - let newPax = [PaxCounterLog]() - fetchedNode[0].pax? = NSOrderedSet(array: newPax) - do { - try context.save() - return true - } catch { - context.rollback() + Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] fetch data error") return false } - } catch { - Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] fetch data error") - return false } -} - -public func clearPositions(destNum: Int64, context: NSManagedObjectContext) -> Bool { - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(destNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - let newPostions = [PositionEntity]() - fetchedNode[0].positions? = NSOrderedSet(array: newPostions) + + public func clearPositions(destNum: Int64) async -> Bool { + let context = self.backgroundContext + return await context.perform { + return self.clearPositions(destNum: destNum, context: context) + } + } + + nonisolated public func clearPositions(destNum: Int64, context: NSManagedObjectContext) -> Bool { + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(destNum)) + do { - try context.save() - return true - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let newPostions = [PositionEntity]() + fetchedNode[0].positions? = NSOrderedSet(array: newPostions) + do { + try context.save() + return true + + } catch { + context.rollback() + return false + } } catch { - context.rollback() + Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] fetch data error") return false } - } catch { - Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] fetch data error") - return false } -} - -public func clearTelemetry(destNum: Int64, metricsType: Int32, context: NSManagedObjectContext) -> Bool { - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(destNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - let emptyTelemetry = [TelemetryEntity]() - fetchedNode[0].telemetries? = NSOrderedSet(array: emptyTelemetry) + + public func clearTelemetry(destNum: Int64, metricsType: Int32) async -> Bool { + let context = self.backgroundContext + return await context.perform { + return self.clearTelemetry(destNum: destNum, metricsType: metricsType, context: context) + } + } + + nonisolated public func clearTelemetry(destNum: Int64, metricsType: Int32, context: NSManagedObjectContext) -> Bool { + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(destNum)) + do { - try context.save() - return true - + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let emptyTelemetry = [TelemetryEntity]() + fetchedNode[0].telemetries? = NSOrderedSet(array: emptyTelemetry) + do { + try context.save() + return true + + } catch { + context.rollback() + return false + } } catch { - context.rollback() + Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] fetch data error") return false } - } catch { - Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] fetch data error") - return false } -} - -public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) { - do { - let objects = channel.allPrivateMessages - for object in objects { - context.delete(object) - } - try context.save() - } catch let error as NSError { - Logger.data.error("\(error.localizedDescription, privacy: .public)") - } -} - -public func deleteUserMessages(user: UserEntity, context: NSManagedObjectContext) { - - do { - let objects = user.messageList - for object in objects { - context.delete(object) - } - try context.save() - } catch let error as NSError { - Logger.data.error("\(error.localizedDescription, privacy: .public)") - } -} - -public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes: Bool) { - - let persistenceController = PersistenceController.shared.container - for i in 0...persistenceController.managedObjectModel.entities.count-1 { - - let entity = persistenceController.managedObjectModel.entities[i] - let query = NSFetchRequest(entityName: entity.name!) - var deleteRequest = NSBatchDeleteRequest(fetchRequest: query) - let entityName = entity.name ?? "UNK" - - if includeRoutes { - deleteRequest = NSBatchDeleteRequest(fetchRequest: query) - } else if !includeRoutes { - if !(entityName.contains("RouteEntity") || entityName.contains("LocationEntity")) { - deleteRequest = NSBatchDeleteRequest(fetchRequest: query) + + public func deleteChannelMessages(channel: ChannelEntity) async { + let context = self.backgroundContext + let objectId = channel.objectID + await context.perform { + if let channelObject = context.object(with: objectId) as? ChannelEntity { + self.deleteChannelMessages(channel: channelObject, context: context) } } + } + + nonisolated public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) { do { - try context.executeAndMergeChanges(using: deleteRequest) - } catch { + let objects = channel.allPrivateMessages + for object in objects { + context.delete(object) + } + try context.save() + } catch let error as NSError { Logger.data.error("\(error.localizedDescription, privacy: .public)") } } -} - -func updateAnyPacketFrom (packet: MeshPacket, activeDeviceNum: Int64, context: NSManagedObjectContext) { - // Update NodeInfoEntity for any packet received. This mirrors the firmware's NodeDB::updateFrom, which sniffs ALL received packets and updates the radio's nodeDB with packet.from's: - // - last_heard (from rxTime) - // - snr - // - via_mqtt - // - hops_away - - // However, unlike the firmware, this function will NOT create a new NodeInfoEntity if we don't have it already. We'll leave that to the existing code paths. - - // We do NOT update fetchedNode[0].channel, because we may hear a node over multiple channels, and only some packet types should update what we consider the node's channel to be. (Example: primary private channel, secondary public channel. A text message on the secondary public channel should NOT change fetchedNode[0].channel.) - - guard packet.from > 0 else { return } - guard packet.from != activeDeviceNum else { return } // Ignore if packet is from our own node - - let fetchNodeInfoAppRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) - if fetchedNode.count >= 1 { - fetchedNode[0].id = Int64(packet.from) - fetchedNode[0].num = Int64(packet.from) - - if packet.rxTime > 0 { - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - Logger.data.info("๐Ÿ’พ [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) lastHeard from rxTime=\(packet.rxTime)") - } else { - fetchedNode[0].lastHeard = Date() - Logger.data.info("๐Ÿ’พ [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) lastHeard to now (rxTime==0)") - } - - fetchedNode[0].snr = packet.rxSnr - fetchedNode[0].rssi = packet.rxRssi - fetchedNode[0].viaMqtt = packet.viaMqtt - - if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { - fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) - Logger.data.info("๐Ÿ’พ [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) hopsAway=\(fetchedNode[0].hopsAway)") - } - - do { - try context.save() - Logger.data.info("๐Ÿ’พ [updateAnyPacketFrom] Updating node \(fetchedNode[0].num.toHex(), privacy: .public) snr=\(fetchedNode[0].snr), rssi=\(fetchedNode[0].rssi) from packet \(packet.id.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [updateAnyPacketFrom] Error Saving node \(fetchedNode[0].num.toHex(), privacy: .public) from packet \(packet.id.toHex(), privacy: .public) \(nsError, privacy: .public)") + + public func deleteUserMessages(user: UserEntity) async { + let context = self.backgroundContext + let objectId = user.objectID + await context.perform { + if let userObject = context.object(with: objectId) as? UserEntity { + self.deleteUserMessages(user: userObject, context: context) } } - } catch { - Logger.data.error("๐Ÿ’ฅ [updateAnyPacketFrom] fetch data error") } -} - -func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false, context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("[NodeInfo] received for: %@".localized, packet.from.toHex()) - Logger.mesh.info("๐Ÿ“Ÿ \(logString, privacy: .public)") - - guard packet.from > 0 else { return } - - let fetchNodeInfoAppRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - - do { - - let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) - if fetchedNode.count == 0 { - // Not Found Insert - let newNode = NodeInfoEntity(context: context) - newNode.id = Int64(packet.from) - newNode.num = Int64(packet.from) - newNode.favorite = favorite - if packet.rxTime > 0 { - newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - } else { - newNode.firstHeard = Date() - newNode.lastHeard = Date() + + nonisolated public func deleteUserMessages(user: UserEntity, context: NSManagedObjectContext) { + do { + let objects = user.messageList + for object in objects { + context.delete(object) } - newNode.snr = packet.rxSnr - newNode.rssi = packet.rxRssi - newNode.viaMqtt = packet.viaMqtt - - if packet.to == Constants.maximumNodeNum || packet.to == UserDefaults.preferredPeripheralNum { - newNode.channel = Int32(packet.channel) - } - if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) { - if nodeInfoMessage.hasHopsAway { - newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) + try context.save() + } catch let error as NSError { + Logger.data.error("\(error.localizedDescription, privacy: .public)") + } + } + + public func clearCoreDataDatabase(includeRoutes: Bool) async { + let context = self.backgroundContext + await context.perform { + self.clearCoreDataDatabase(context: context, includeRoutes: includeRoutes) + } + } + + nonisolated public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes: Bool) { + let persistenceController = PersistenceController.shared.container + for i in 0...persistenceController.managedObjectModel.entities.count-1 { + + let entity = persistenceController.managedObjectModel.entities[i] + let query = NSFetchRequest(entityName: entity.name!) + var deleteRequest = NSBatchDeleteRequest(fetchRequest: query) + let entityName = entity.name ?? "UNK" + + if includeRoutes { + deleteRequest = NSBatchDeleteRequest(fetchRequest: query) + } else if !includeRoutes { + if !(entityName.contains("RouteEntity") || entityName.contains("LocationEntity")) { + deleteRequest = NSBatchDeleteRequest(fetchRequest: query) } - newNode.favorite = nodeInfoMessage.isFavorite } - - if let newUserMessage = try? User(serializedBytes: packet.decoded.payload) { - - if newUserMessage.id.isEmpty { + do { + try context.executeAndMergeChanges(using: deleteRequest) + } catch { + Logger.data.error("\(error.localizedDescription, privacy: .public)") + } + } + } + + func updateAnyPacketFrom (packet: MeshPacket, activeDeviceNum: Int64) async { + let context = self.backgroundContext + await context.perform { + self.updateAnyPacketFrom(packet: packet, activeDeviceNum: activeDeviceNum, context: context) + } + } + + nonisolated func updateAnyPacketFrom (packet: MeshPacket, activeDeviceNum: Int64, context: NSManagedObjectContext) { + // Update NodeInfoEntity for any packet received. This mirrors the firmware's NodeDB::updateFrom, which sniffs ALL received packets and updates the radio's nodeDB with packet.from's: + // - last_heard (from rxTime) + // - snr + // - via_mqtt + // - hops_away + + // However, unlike the firmware, this function will NOT create a new NodeInfoEntity if we don't have it already. We'll leave that to the existing code paths. + + // We do NOT update fetchedNode[0].channel, because we may hear a node over multiple channels, and only some packet types should update what we consider the node's channel to be. (Example: primary private channel, secondary public channel. A text message on the secondary public channel should NOT change fetchedNode[0].channel.) + + guard packet.from > 0 else { return } + guard packet.from != activeDeviceNum else { return } // Ignore if packet is from our own node + + let fetchNodeInfoAppRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) + if fetchedNode.count >= 1 { + fetchedNode[0].id = Int64(packet.from) + fetchedNode[0].num = Int64(packet.from) + + if packet.rxTime > 0 { + fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + Logger.data.info("๐Ÿ’พ [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) lastHeard from rxTime=\(packet.rxTime)") + } else { + fetchedNode[0].lastHeard = Date() + Logger.data.info("๐Ÿ’พ [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) lastHeard to now (rxTime==0)") + } + + fetchedNode[0].snr = packet.rxSnr + fetchedNode[0].rssi = packet.rxRssi + fetchedNode[0].viaMqtt = packet.viaMqtt + + if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { + fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) + Logger.data.info("๐Ÿ’พ [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) hopsAway=\(fetchedNode[0].hopsAway)") + } + + do { + try context.save() + Logger.data.info("๐Ÿ’พ [updateAnyPacketFrom] Updating node \(fetchedNode[0].num.toHex(), privacy: .public) snr=\(fetchedNode[0].snr), rssi=\(fetchedNode[0].rssi) from packet \(packet.id.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [updateAnyPacketFrom] Error Saving node \(fetchedNode[0].num.toHex(), privacy: .public) from packet \(packet.id.toHex(), privacy: .public) \(nsError, privacy: .public)") + } + } + } catch { + Logger.data.error("๐Ÿ’ฅ [updateAnyPacketFrom] fetch data error") + } + } + + func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false) async { + let context = self.backgroundContext + await context.perform { + self.upsertNodeInfoPacket(packet: packet, favorite: favorite, context: context) + } + } + + nonisolated func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("[NodeInfo] received for: %@".localized, packet.from.toHex()) + Logger.mesh.info("๐Ÿ“Ÿ \(logString, privacy: .public)") + + guard packet.from > 0 else { return } + + let fetchNodeInfoAppRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) + if fetchedNode.count == 0 { + // Not Found Insert + let newNode = NodeInfoEntity(context: context) + newNode.id = Int64(packet.from) + newNode.num = Int64(packet.from) + newNode.favorite = favorite + if packet.rxTime > 0 { + newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + } else { + newNode.firstHeard = Date() + newNode.lastHeard = Date() + } + newNode.snr = packet.rxSnr + newNode.rssi = packet.rxRssi + newNode.viaMqtt = packet.viaMqtt + + if packet.to == Constants.maximumNodeNum || packet.to == UserDefaults.preferredPeripheralNum { + newNode.channel = Int32(packet.channel) + } + if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) { + if nodeInfoMessage.hasHopsAway { + newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) + } + newNode.favorite = nodeInfoMessage.isFavorite + } + + if let newUserMessage = try? User(serializedBytes: packet.decoded.payload) { + + if newUserMessage.id.isEmpty { + if packet.from > Constants.minimumNodeNum { + do { + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + newNode.user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + } catch { + Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") + } + } + } else { + + let newUser = UserEntity(context: context) + newUser.userId = newNode.num.toHex() + newUser.num = Int64(packet.from) + newUser.longName = newUserMessage.longName + newUser.shortName = newUserMessage.shortName + newUser.role = Int32(newUserMessage.role.rawValue) + newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() + newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default + if newUserMessage.hasIsUnmessagable { + newUser.unmessagable = newUserMessage.isUnmessagable + } else { + let roles = [2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(newUser.role)) + if containsRole { + newUser.unmessagable = true + } else { + newUser.unmessagable = false + } + } + if !newUserMessage.publicKey.isEmpty { + newUser.pkiEncrypted = true + newUser.publicKey = newUserMessage.publicKey + } + + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) + newUser.hwDisplayName = dh?.displayName + } + } + newNode.user = newUser + + if UserDefaults.newNodeNotifications { + Task { @MainActor in + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: (UUID().uuidString), + title: "New Node".localized, + subtitle: "\(newUser.longName ?? "Unknown".localized)", + content: "New Node has been discovered".localized, + target: "nodes", + path: "meshtastic:///nodes?nodenum=\(newUser.num)" + ) + ] + manager.schedule() + } + } + } + } else { if packet.from > Constants.minimumNodeNum { do { let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + if !packet.publicKey.isEmpty { + newNode.user?.pkiEncrypted = true + newNode.user?.publicKey = packet.publicKey + } newNode.user = newUser } catch CoreDataError.invalidInput(let message) { Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") @@ -272,1306 +407,1382 @@ func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false, context: Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") } } - } else { - - let newUser = UserEntity(context: context) - newUser.userId = newNode.num.toHex() - newUser.num = Int64(packet.from) - newUser.longName = newUserMessage.longName - newUser.shortName = newUserMessage.shortName - newUser.role = Int32(newUserMessage.role.rawValue) - newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() - newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) - /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default - if newUserMessage.hasIsUnmessagable { - newUser.unmessagable = newUserMessage.isUnmessagable - } else { - let roles = [2, 4, 5, 6, 7, 10, 11] - let containsRole = roles.contains(Int(newUser.role)) - if containsRole { - newUser.unmessagable = true - } else { - newUser.unmessagable = false - } - } - if !newUserMessage.publicKey.isEmpty { - newUser.pkiEncrypted = true - newUser.publicKey = newUserMessage.publicKey - } - - Task { - Api().loadDeviceHardwareData { (hw) in - let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) - newUser.hwDisplayName = dh?.displayName - } - } - newNode.user = newUser - - if UserDefaults.newNodeNotifications { - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: (UUID().uuidString), - title: "New Node".localized, - subtitle: "\(newUser.longName ?? "Unknown".localized)", - content: "New Node has been discovered".localized, - target: "nodes", - path: "meshtastic:///nodes?nodenum=\(newUser.num)" - ) - ] - manager.schedule() - } } - } else { - if packet.from > Constants.minimumNodeNum { + // User is messed up and has failed to create at least once, if this fails bail out + if newNode.user == nil && packet.from > Constants.minimumNodeNum { do { - let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) - if !packet.publicKey.isEmpty { - newNode.user?.pkiEncrypted = true - newNode.user?.publicKey = packet.publicKey - } + let newUser = try createUser(num: Int64(packet.from), context: context) newNode.user = newUser } catch CoreDataError.invalidInput(let message) { Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") + context.rollback() + return } catch { Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") - } - } - } - // User is messed up and has failed to create at least once, if this fails bail out - if newNode.user == nil && packet.from > Constants.minimumNodeNum { - do { - let newUser = try createUser(num: Int64(packet.from), context: context) - newNode.user = newUser - } catch CoreDataError.invalidInput(let message) { - Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") - context.rollback() - return - } catch { - Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") - context.rollback() - return - } - } - - let myInfoEntity = MyInfoEntity(context: context) - myInfoEntity.myNodeNum = Int64(packet.from) - myInfoEntity.rebootCount = 0 - newNode.myInfo = myInfoEntity - do { - try context.save() - Logger.data.info("๐Ÿ’พ [NodeInfo] Saved a NodeInfo for node number: \(packet.from.toHex(), privacy: .public)") - Logger.data.info("๐Ÿ’พ [MyInfoEntity] Saved a new myInfo for node number: \(packet.from.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [MyInfoEntity] Error Inserting New Core Data: \(nsError, privacy: .public)") - } - - } else { - // Update an existing node - if packet.to == Constants.maximumNodeNum || packet.to == UserDefaults.preferredPeripheralNum { - fetchedNode[0].channel = Int32(packet.channel) - } - - if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) { - - fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway) - fetchedNode[0].favorite = nodeInfoMessage.isFavorite - if nodeInfoMessage.hasDeviceMetrics { - let telemetry = TelemetryEntity(context: context) - telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) - telemetry.voltage = nodeInfoMessage.deviceMetrics.voltage - telemetry.channelUtilization = nodeInfoMessage.deviceMetrics.channelUtilization - telemetry.airUtilTx = nodeInfoMessage.deviceMetrics.airUtilTx - var newTelemetries = [TelemetryEntity]() - newTelemetries.append(telemetry) - fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) - } - if nodeInfoMessage.hasUser { - fetchedNode[0].user?.userId = nodeInfoMessage.num.toHex() - fetchedNode[0].user?.num = Int64(nodeInfoMessage.num) - fetchedNode[0].user?.longName = nodeInfoMessage.user.longName - fetchedNode[0].user?.shortName = nodeInfoMessage.user.shortName - fetchedNode[0].user?.role = Int32(nodeInfoMessage.user.role.rawValue) - fetchedNode[0].user?.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() - fetchedNode[0].user?.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) - /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default - if nodeInfoMessage.user.hasIsUnmessagable { - fetchedNode[0].user?.unmessagable = nodeInfoMessage.user.isUnmessagable - } else { - let roles = [-1, 2, 4, 5, 6, 7, 10, 11] - let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1)) - if containsRole { - fetchedNode[0].user?.unmessagable = true - } else { - fetchedNode[0].user?.unmessagable = false - } - } - if !nodeInfoMessage.user.publicKey.isEmpty { - fetchedNode[0].user?.pkiEncrypted = true - fetchedNode[0].user?.publicKey = nodeInfoMessage.user.publicKey - } - Task { - Api().loadDeviceHardwareData { (hw) in - let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) - fetchedNode[0].user?.hwDisplayName = dh?.displayName - } - } - } - } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { - fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) - } - if fetchedNode[0].user == nil { - do { - let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) - fetchedNode[0].user = newUser - } catch CoreDataError.invalidInput(let message) { - Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") - } catch { - Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") - } - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [NodeInfoEntity] Updated from Node Info App Packet For: \(fetchedNode[0].num.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] Error Saving from NODEINFO_APP \(nsError, privacy: .public)") - } - } - } catch { - Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] fetch data error for NODEINFO_APP") - } -} - -func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("[Position] received from node: %@".localized, String(packet.from)) - Logger.mesh.info("๐Ÿ“ \(logString, privacy: .public)") - - let fetchNodePositionRequest = NodeInfoEntity.fetchRequest() - fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - - do { - - if let positionMessage = try? Position(serializedBytes: packet.decoded.payload) { - - /// Don't save empty position packets from null island or apple park - if (positionMessage.longitudeI != 0 && positionMessage.latitudeI != 0) && (positionMessage.latitudeI != 373346000 && positionMessage.longitudeI != -1220090000) { - let fetchedNode = try context.fetch(fetchNodePositionRequest) - if fetchedNode.count == 1 { - - // Unset the current latest position for this node - let fetchCurrentLatestPositionsRequest = PositionEntity.fetchRequest() - fetchCurrentLatestPositionsRequest.predicate = NSPredicate(format: "nodePosition.num == %lld && latest = true", Int64(packet.from)) - - let fetchedPositions = try context.fetch(fetchCurrentLatestPositionsRequest) - if fetchedPositions.count > 0 { - for position in fetchedPositions { - position.latest = false - } - } - let position = PositionEntity(context: context) - position.latest = true - position.snr = packet.rxSnr - position.rssi = packet.rxRssi - position.seqNo = Int32(positionMessage.seqNumber) - position.latitudeI = positionMessage.latitudeI - position.longitudeI = positionMessage.longitudeI - position.altitude = positionMessage.altitude - position.satsInView = Int32(positionMessage.satsInView) - position.speed = Int32(positionMessage.groundSpeed) - let heading = Int32(positionMessage.groundTrack) - // Throw out bad haeadings from the device - if heading >= 0 && heading <= 360 { - position.heading = Int32(positionMessage.groundTrack) - } - position.precisionBits = Int32(positionMessage.precisionBits) - if positionMessage.timestamp != 0 { - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp))) - } else { - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) - } - guard let mutablePositions = fetchedNode[0].positions?.mutableCopy() as? NSMutableOrderedSet else { + context.rollback() return } - /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. - if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) { - if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 9.0 { - mutablePositions.remove(mostRecent) - } - } else if mutablePositions.count > 0 { - /// Don't store any history for reduced accuracy positions, we will just show a circle - mutablePositions.removeAllObjects() - } - mutablePositions.add(position) - + } + + let myInfoEntity = MyInfoEntity(context: context) + myInfoEntity.myNodeNum = Int64(packet.from) + myInfoEntity.rebootCount = 0 + newNode.myInfo = myInfoEntity + do { + try context.save() + Logger.data.info("๐Ÿ’พ [NodeInfo] Saved a NodeInfo for node number: \(packet.from.toHex(), privacy: .public)") + Logger.data.info("๐Ÿ’พ [MyInfoEntity] Saved a new myInfo for node number: \(packet.from.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [MyInfoEntity] Error Inserting New Core Data: \(nsError, privacy: .public)") + } + + } else { + // Update an existing node + if packet.to == Constants.maximumNodeNum || packet.to == UserDefaults.preferredPeripheralNum { fetchedNode[0].channel = Int32(packet.channel) - fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet - + } + + if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) { + + fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway) + fetchedNode[0].favorite = nodeInfoMessage.isFavorite + if nodeInfoMessage.hasDeviceMetrics { + let telemetry = TelemetryEntity(context: context) + telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) + telemetry.voltage = nodeInfoMessage.deviceMetrics.voltage + telemetry.channelUtilization = nodeInfoMessage.deviceMetrics.channelUtilization + telemetry.airUtilTx = nodeInfoMessage.deviceMetrics.airUtilTx + var newTelemetries = [TelemetryEntity]() + newTelemetries.append(telemetry) + fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) + } + if nodeInfoMessage.hasUser { + fetchedNode[0].user?.userId = nodeInfoMessage.num.toHex() + fetchedNode[0].user?.num = Int64(nodeInfoMessage.num) + fetchedNode[0].user?.longName = nodeInfoMessage.user.longName + fetchedNode[0].user?.shortName = nodeInfoMessage.user.shortName + fetchedNode[0].user?.role = Int32(nodeInfoMessage.user.role.rawValue) + fetchedNode[0].user?.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() + fetchedNode[0].user?.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) + /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default + if nodeInfoMessage.user.hasIsUnmessagable { + fetchedNode[0].user?.unmessagable = nodeInfoMessage.user.isUnmessagable + } else { + let roles = [-1, 2, 4, 5, 6, 7, 10, 11] + let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1)) + if containsRole { + fetchedNode[0].user?.unmessagable = true + } else { + fetchedNode[0].user?.unmessagable = false + } + } + if !nodeInfoMessage.user.publicKey.isEmpty { + fetchedNode[0].user?.pkiEncrypted = true + fetchedNode[0].user?.publicKey = nodeInfoMessage.user.publicKey + } + Task { + Api().loadDeviceHardwareData { (hw) in + let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) + fetchedNode[0].user?.hwDisplayName = dh?.displayName + } + } + } + } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { + fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) + } + if fetchedNode[0].user == nil { do { - try context.save() - Logger.data.info("๐Ÿ’พ [Position] Saved from Position App Packet For: \(fetchedNode[0].num.toHex(), privacy: .public)") + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + fetchedNode[0].user = newUser + } catch CoreDataError.invalidInput(let message) { + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)") } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ Error Saving NodeInfoEntity from POSITION_APP \(nsError, privacy: .public)") + Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)") } } - } else { - Logger.data.error("๐Ÿ’ฅ Empty POSITION_APP Packet: \((try? packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") - } - } - } catch { - Logger.data.error("๐Ÿ’ฅ Error Deserializing POSITION_APP packet.") - } -} - -func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Bluetooth config received: %@".localized, String(nodeNum)) - Logger.mesh.info("๐Ÿ“ถ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Device Config - if !fetchedNode.isEmpty { - if fetchedNode[0].bluetoothConfig == nil { - let newBluetoothConfig = BluetoothConfigEntity(context: context) - newBluetoothConfig.enabled = config.enabled - newBluetoothConfig.mode = Int32(config.mode.rawValue) - newBluetoothConfig.fixedPin = Int32(config.fixedPin) - fetchedNode[0].bluetoothConfig = newBluetoothConfig - } else { - fetchedNode[0].bluetoothConfig?.enabled = config.enabled - fetchedNode[0].bluetoothConfig?.mode = Int32(config.mode.rawValue) - fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.fixedPin) - } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [BluetoothConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [BluetoothConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [BluetoothConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Bluetooth Config") - } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [BluetoothConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") - } -} - -func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Device config received: %@".localized, String(nodeNum)) - Logger.mesh.info("๐Ÿ“Ÿ \(logString, privacy: .public)") - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Device Config - if !fetchedNode.isEmpty { - if fetchedNode[0].deviceConfig == nil { - let newDeviceConfig = DeviceConfigEntity(context: context) - newDeviceConfig.role = Int32(config.role.rawValue) - newDeviceConfig.buttonGpio = Int32(config.buttonGpio) - newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio) - newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) - newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) - newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress - newDeviceConfig.tripleClickAsAdHocPing = !config.disableTripleClick - newDeviceConfig.ledHeartbeatEnabled = !config.ledHeartbeatDisabled - newDeviceConfig.isManaged = config.isManaged - newDeviceConfig.tzdef = config.tzdef - fetchedNode[0].deviceConfig = newDeviceConfig - } else { - fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue) - fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio) - fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio) - fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) - fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) - fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress - fetchedNode[0].deviceConfig?.tripleClickAsAdHocPing = !config.disableTripleClick - fetchedNode[0].deviceConfig?.ledHeartbeatEnabled = !config.ledHeartbeatDisabled - fetchedNode[0].deviceConfig?.isManaged = config.isManaged - fetchedNode[0].deviceConfig?.tzdef = config.tzdef - } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [DeviceConfigEntity] Updated Device Config for node number: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [DeviceConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [DeviceConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") - } -} - -func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Display config received: %@".localized, nodeNum.toHex()) - Logger.data.info("๐Ÿ–ฅ๏ธ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Device Config - if !fetchedNode.isEmpty { - - if fetchedNode[0].displayConfig == nil { - - let newDisplayConfig = DisplayConfigEntity(context: context) - newDisplayConfig.screenOnSeconds = Int32(truncatingIfNeeded: config.screenOnSecs) - newDisplayConfig.screenCarouselInterval = Int32(truncatingIfNeeded: config.autoScreenCarouselSecs) - newDisplayConfig.compassNorthTop = config.compassNorthTop - newDisplayConfig.flipScreen = config.flipScreen - newDisplayConfig.oledType = Int32(config.oled.rawValue) - newDisplayConfig.displayMode = Int32(config.displaymode.rawValue) - newDisplayConfig.units = Int32(config.units.rawValue) - newDisplayConfig.headingBold = config.headingBold - newDisplayConfig.use12HClock = config.use12HClock - fetchedNode[0].displayConfig = newDisplayConfig - } else { - fetchedNode[0].displayConfig?.screenOnSeconds = Int32(truncatingIfNeeded: config.screenOnSecs) - fetchedNode[0].displayConfig?.screenCarouselInterval = Int32(truncatingIfNeeded: config.autoScreenCarouselSecs) - fetchedNode[0].displayConfig?.compassNorthTop = config.compassNorthTop - fetchedNode[0].displayConfig?.flipScreen = config.flipScreen - fetchedNode[0].displayConfig?.oledType = Int32(config.oled.rawValue) - fetchedNode[0].displayConfig?.displayMode = Int32(config.displaymode.rawValue) - fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue) - fetchedNode[0].displayConfig?.headingBold = config.headingBold - fetchedNode[0].displayConfig?.use12HClock = config.use12HClock - } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - - try context.save() - Logger.data.info("๐Ÿ’พ [DisplayConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [DisplayConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [DisplayConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Display Config") - } - - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [DisplayConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") - } -} - -func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("LoRa config received: %@".localized, nodeNum.toHex()) - Logger.data.info("๐Ÿ“ป \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", nodeNum) - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save LoRa Config - if fetchedNode.count > 0 { - if fetchedNode[0].loRaConfig == nil { - // No lora config for node, save a new lora config - let newLoRaConfig = LoRaConfigEntity(context: context) - newLoRaConfig.regionCode = Int32(config.region.rawValue) - newLoRaConfig.usePreset = config.usePreset - newLoRaConfig.modemPreset = Int32(config.modemPreset.rawValue) - newLoRaConfig.bandwidth = Int32(config.bandwidth) - newLoRaConfig.spreadFactor = Int32(config.spreadFactor) - newLoRaConfig.codingRate = Int32(config.codingRate) - newLoRaConfig.frequencyOffset = config.frequencyOffset - newLoRaConfig.overrideFrequency = config.overrideFrequency - newLoRaConfig.overrideDutyCycle = config.overrideDutyCycle - newLoRaConfig.hopLimit = Int32(config.hopLimit) - newLoRaConfig.txPower = Int32(config.txPower) - newLoRaConfig.txEnabled = config.txEnabled - newLoRaConfig.channelNum = Int32(config.channelNum) - newLoRaConfig.sx126xRxBoostedGain = config.sx126XRxBoostedGain - newLoRaConfig.ignoreMqtt = config.ignoreMqtt - newLoRaConfig.okToMqtt = config.configOkToMqtt - fetchedNode[0].loRaConfig = newLoRaConfig - } else { - fetchedNode[0].loRaConfig?.regionCode = Int32(config.region.rawValue) - fetchedNode[0].loRaConfig?.usePreset = config.usePreset - fetchedNode[0].loRaConfig?.modemPreset = Int32(config.modemPreset.rawValue) - fetchedNode[0].loRaConfig?.bandwidth = Int32(config.bandwidth) - fetchedNode[0].loRaConfig?.spreadFactor = Int32(config.spreadFactor) - fetchedNode[0].loRaConfig?.codingRate = Int32(config.codingRate) - fetchedNode[0].loRaConfig?.frequencyOffset = config.frequencyOffset - fetchedNode[0].loRaConfig?.overrideFrequency = config.overrideFrequency - fetchedNode[0].loRaConfig?.overrideDutyCycle = config.overrideDutyCycle - fetchedNode[0].loRaConfig?.hopLimit = Int32(config.hopLimit) - fetchedNode[0].loRaConfig?.txPower = Int32(config.txPower) - fetchedNode[0].loRaConfig?.txEnabled = config.txEnabled - fetchedNode[0].loRaConfig?.channelNum = Int32(config.channelNum) - fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain - fetchedNode[0].loRaConfig?.ignoreMqtt = config.ignoreMqtt - fetchedNode[0].loRaConfig?.okToMqtt = config.configOkToMqtt - fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain - } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [LoRaConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [LoRaConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [LoRaConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Lora Config") - } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [LoRaConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") - } -} - -func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Network config received: %@".localized, String(nodeNum)) - Logger.data.info("๐ŸŒ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save WiFi Config - if !fetchedNode.isEmpty { - if fetchedNode[0].networkConfig == nil { - let newNetworkConfig = NetworkConfigEntity(context: context) - newNetworkConfig.wifiEnabled = config.wifiEnabled - newNetworkConfig.wifiSsid = config.wifiSsid - newNetworkConfig.wifiPsk = config.wifiPsk - newNetworkConfig.ethEnabled = config.ethEnabled - newNetworkConfig.enabledProtocols = Int32(config.enabledProtocols) - fetchedNode[0].networkConfig = newNetworkConfig - } else { - fetchedNode[0].networkConfig?.ethEnabled = config.ethEnabled - fetchedNode[0].networkConfig?.wifiEnabled = config.wifiEnabled - fetchedNode[0].networkConfig?.wifiSsid = config.wifiSsid - fetchedNode[0].networkConfig?.wifiPsk = config.wifiPsk - fetchedNode[0].networkConfig?.enabledProtocols = Int32(config.enabledProtocols) - } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [NetworkConfigEntity] Updated Network Config for node: \(nodeNum.toHex(), privacy: .public)") - - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [NetworkConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [NetworkConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Network Config") - } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [NetworkConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") - } -} - -func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Position config received: %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿ—บ๏ธ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save LoRa Config - if !fetchedNode.isEmpty { - if fetchedNode[0].positionConfig == nil { - let newPositionConfig = PositionConfigEntity(context: context) - newPositionConfig.smartPositionEnabled = config.positionBroadcastSmartEnabled - newPositionConfig.deviceGpsEnabled = config.gpsEnabled - newPositionConfig.gpsMode = Int32(truncatingIfNeeded: config.gpsMode.rawValue) - newPositionConfig.rxGpio = Int32(truncatingIfNeeded: config.rxGpio) - newPositionConfig.txGpio = Int32(truncatingIfNeeded: config.txGpio) - newPositionConfig.gpsEnGpio = Int32(truncatingIfNeeded: config.gpsEnGpio) - newPositionConfig.fixedPosition = config.fixedPosition - newPositionConfig.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs) - newPositionConfig.broadcastSmartMinimumIntervalSecs = Int32(truncatingIfNeeded: config.broadcastSmartMinimumIntervalSecs) - newPositionConfig.broadcastSmartMinimumDistance = Int32(truncatingIfNeeded: config.broadcastSmartMinimumDistance) - newPositionConfig.positionFlags = Int32(truncatingIfNeeded: config.positionFlags) - newPositionConfig.gpsAttemptTime = 900 - newPositionConfig.gpsUpdateInterval = Int32(truncatingIfNeeded: config.gpsUpdateInterval) - fetchedNode[0].positionConfig = newPositionConfig - } else { - fetchedNode[0].positionConfig?.smartPositionEnabled = config.positionBroadcastSmartEnabled - fetchedNode[0].positionConfig?.deviceGpsEnabled = config.gpsEnabled - fetchedNode[0].positionConfig?.gpsMode = Int32(truncatingIfNeeded: config.gpsMode.rawValue) - fetchedNode[0].positionConfig?.rxGpio = Int32(truncatingIfNeeded: config.rxGpio) - fetchedNode[0].positionConfig?.txGpio = Int32(truncatingIfNeeded: config.txGpio) - fetchedNode[0].positionConfig?.gpsEnGpio = Int32(truncatingIfNeeded: config.gpsEnGpio) - fetchedNode[0].positionConfig?.fixedPosition = config.fixedPosition - fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs) - fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(truncatingIfNeeded: config.broadcastSmartMinimumIntervalSecs) - fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(truncatingIfNeeded: config.broadcastSmartMinimumDistance) - fetchedNode[0].positionConfig?.gpsAttemptTime = 900 - fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(truncatingIfNeeded: config.gpsUpdateInterval) - fetchedNode[0].positionConfig?.positionFlags = Int32(truncatingIfNeeded: config.positionFlags) - } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [PositionConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [PositionConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [PositionConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Position Config") - } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [PositionConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") - } -} - -func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - let logString = String.localizedStringWithFormat("Power config received: %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿ—บ๏ธ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Power Config - if !fetchedNode.isEmpty { - if fetchedNode[0].powerConfig == nil { - let newPowerConfig = PowerConfigEntity(context: context) - newPowerConfig.adcMultiplierOverride = config.adcMultiplierOverride - newPowerConfig.deviceBatteryInaAddress = Int32(config.deviceBatteryInaAddress) - newPowerConfig.isPowerSaving = config.isPowerSaving - newPowerConfig.lsSecs = Int32(truncatingIfNeeded: config.lsSecs) - newPowerConfig.minWakeSecs = Int32(truncatingIfNeeded: config.minWakeSecs) - newPowerConfig.onBatteryShutdownAfterSecs = Int32(truncatingIfNeeded: config.onBatteryShutdownAfterSecs) - newPowerConfig.waitBluetoothSecs = Int32(truncatingIfNeeded: config.waitBluetoothSecs) - fetchedNode[0].powerConfig = newPowerConfig - } else { - fetchedNode[0].powerConfig?.adcMultiplierOverride = config.adcMultiplierOverride - fetchedNode[0].powerConfig?.deviceBatteryInaAddress = Int32(config.deviceBatteryInaAddress) - fetchedNode[0].powerConfig?.isPowerSaving = config.isPowerSaving - fetchedNode[0].powerConfig?.lsSecs = Int32(truncatingIfNeeded: config.lsSecs) - fetchedNode[0].powerConfig?.minWakeSecs = Int32(truncatingIfNeeded: config.minWakeSecs) - fetchedNode[0].powerConfig?.onBatteryShutdownAfterSecs = Int32(truncatingIfNeeded: config.onBatteryShutdownAfterSecs) - fetchedNode[0].powerConfig?.waitBluetoothSecs = Int32(truncatingIfNeeded: config.waitBluetoothSecs) - } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [PowerConfigEntity] Updated Power Config for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [PowerConfigEntity] Error Updating Core Data PowerConfigEntity: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [PowerConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Power Config") - } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [PowerConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") - } -} - -func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("mesh.log.security.config %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿ›ก๏ธ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Security Config - if !fetchedNode.isEmpty { - if fetchedNode[0].securityConfig == nil { - let newSecurityConfig = SecurityConfigEntity(context: context) - newSecurityConfig.publicKey = config.publicKey - newSecurityConfig.privateKey = config.privateKey - if config.adminKey.count > 0 { - newSecurityConfig.adminKey = config.adminKey[0] + do { + try context.save() + Logger.data.info("๐Ÿ’พ [NodeInfoEntity] Updated from Node Info App Packet For: \(fetchedNode[0].num.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] Error Saving from NODEINFO_APP \(nsError, privacy: .public)") } - newSecurityConfig.isManaged = config.isManaged - newSecurityConfig.serialEnabled = config.serialEnabled - newSecurityConfig.debugLogApiEnabled = config.debugLogApiEnabled - newSecurityConfig.adminChannelEnabled = config.adminChannelEnabled - fetchedNode[0].securityConfig = newSecurityConfig - } else { - fetchedNode[0].securityConfig?.publicKey = config.publicKey - fetchedNode[0].securityConfig?.privateKey = config.privateKey - if config.adminKey.count > 0 { - fetchedNode[0].securityConfig?.adminKey = config.adminKey[0] - if config.adminKey.count > 1 { - fetchedNode[0].securityConfig?.adminKey2 = config.adminKey[1] + } + } catch { + Logger.data.error("๐Ÿ’ฅ [NodeInfoEntity] fetch data error for NODEINFO_APP") + } + } + + func upsertPositionPacket (packet: MeshPacket) async { + let context = self.backgroundContext + await context.perform { + self.upsertPositionPacket(packet: packet, context: context) + } + } + + nonisolated func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("[Position] received from node: %@".localized, String(packet.from)) + Logger.mesh.info("๐Ÿ“ \(logString, privacy: .public)") + + let fetchNodePositionRequest = NodeInfoEntity.fetchRequest() + fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + + if let positionMessage = try? Position(serializedBytes: packet.decoded.payload) { + + /// Don't save empty position packets from null island or apple park + if (positionMessage.longitudeI != 0 && positionMessage.latitudeI != 0) && (positionMessage.latitudeI != 373346000 && positionMessage.longitudeI != -1220090000) { + let fetchedNode = try context.fetch(fetchNodePositionRequest) + if fetchedNode.count == 1 { + + // Unset the current latest position for this node + let fetchCurrentLatestPositionsRequest = PositionEntity.fetchRequest() + fetchCurrentLatestPositionsRequest.predicate = NSPredicate(format: "nodePosition.num == %lld && latest = true", Int64(packet.from)) + + let fetchedPositions = try context.fetch(fetchCurrentLatestPositionsRequest) + if fetchedPositions.count > 0 { + for position in fetchedPositions { + position.latest = false + } + } + let position = PositionEntity(context: context) + position.latest = true + position.snr = packet.rxSnr + position.rssi = packet.rxRssi + position.seqNo = Int32(positionMessage.seqNumber) + position.latitudeI = positionMessage.latitudeI + position.longitudeI = positionMessage.longitudeI + position.altitude = positionMessage.altitude + position.satsInView = Int32(positionMessage.satsInView) + position.speed = Int32(positionMessage.groundSpeed) + let heading = Int32(positionMessage.groundTrack) + // Throw out bad haeadings from the device + if heading >= 0 && heading <= 360 { + position.heading = Int32(positionMessage.groundTrack) + } + position.precisionBits = Int32(positionMessage.precisionBits) + if positionMessage.timestamp != 0 { + position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp))) + } else { + position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) + } + guard let mutablePositions = fetchedNode[0].positions?.mutableCopy() as? NSMutableOrderedSet else { + return + } + /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. + if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) { + if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 9.0 { + mutablePositions.remove(mostRecent) + } + } else if mutablePositions.count > 0 { + /// Don't store any history for reduced accuracy positions, we will just show a circle + mutablePositions.removeAllObjects() + } + mutablePositions.add(position) + + fetchedNode[0].channel = Int32(packet.channel) + fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet + + do { + try context.save() + Logger.data.info("๐Ÿ’พ [Position] Saved from Position App Packet 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)") + } } - if config.adminKey.count > 2 { - fetchedNode[0].securityConfig?.adminKey3 = config.adminKey[2] + } else { + Logger.data.error("๐Ÿ’ฅ Empty POSITION_APP Packet: \((try? packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + } + } + } catch { + Logger.data.error("๐Ÿ’ฅ Error Deserializing POSITION_APP packet.") + } + } + + func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertBluetoothConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Bluetooth config received: %@".localized, String(nodeNum)) + Logger.mesh.info("๐Ÿ“ถ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Device Config + if !fetchedNode.isEmpty { + if fetchedNode[0].bluetoothConfig == nil { + let newBluetoothConfig = BluetoothConfigEntity(context: context) + newBluetoothConfig.enabled = config.enabled + newBluetoothConfig.mode = Int32(config.mode.rawValue) + newBluetoothConfig.fixedPin = Int32(config.fixedPin) + fetchedNode[0].bluetoothConfig = newBluetoothConfig + } else { + fetchedNode[0].bluetoothConfig?.enabled = config.enabled + fetchedNode[0].bluetoothConfig?.mode = Int32(config.mode.rawValue) + fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.fixedPin) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [BluetoothConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [BluetoothConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("๐Ÿ’ฅ [BluetoothConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Bluetooth Config") + } + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [BluetoothConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } + } + + func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertDeviceConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Device config received: %@".localized, String(nodeNum)) + Logger.mesh.info("๐Ÿ“Ÿ \(logString, privacy: .public)") + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Device Config + if !fetchedNode.isEmpty { + if fetchedNode[0].deviceConfig == nil { + let newDeviceConfig = DeviceConfigEntity(context: context) + newDeviceConfig.role = Int32(config.role.rawValue) + newDeviceConfig.buttonGpio = Int32(config.buttonGpio) + newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio) + newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) + newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) + newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress + newDeviceConfig.tripleClickAsAdHocPing = !config.disableTripleClick + newDeviceConfig.ledHeartbeatEnabled = !config.ledHeartbeatDisabled + newDeviceConfig.isManaged = config.isManaged + newDeviceConfig.tzdef = config.tzdef + fetchedNode[0].deviceConfig = newDeviceConfig + } else { + fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue) + fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio) + fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio) + fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) + fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) + fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress + fetchedNode[0].deviceConfig?.tripleClickAsAdHocPing = !config.disableTripleClick + fetchedNode[0].deviceConfig?.ledHeartbeatEnabled = !config.ledHeartbeatDisabled + fetchedNode[0].deviceConfig?.isManaged = config.isManaged + fetchedNode[0].deviceConfig?.tzdef = config.tzdef + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [DeviceConfigEntity] Updated Device Config for node number: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [DeviceConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [DeviceConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } + } + + func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertDisplayConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Display config received: %@".localized, nodeNum.toHex()) + Logger.data.info("๐Ÿ–ฅ๏ธ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Device Config + if !fetchedNode.isEmpty { + + if fetchedNode[0].displayConfig == nil { + + let newDisplayConfig = DisplayConfigEntity(context: context) + newDisplayConfig.screenOnSeconds = Int32(truncatingIfNeeded: config.screenOnSecs) + newDisplayConfig.screenCarouselInterval = Int32(truncatingIfNeeded: config.autoScreenCarouselSecs) + newDisplayConfig.compassNorthTop = config.compassNorthTop + newDisplayConfig.flipScreen = config.flipScreen + newDisplayConfig.oledType = Int32(config.oled.rawValue) + newDisplayConfig.displayMode = Int32(config.displaymode.rawValue) + newDisplayConfig.units = Int32(config.units.rawValue) + newDisplayConfig.headingBold = config.headingBold + newDisplayConfig.use12HClock = config.use12HClock + fetchedNode[0].displayConfig = newDisplayConfig + } else { + fetchedNode[0].displayConfig?.screenOnSeconds = Int32(truncatingIfNeeded: config.screenOnSecs) + fetchedNode[0].displayConfig?.screenCarouselInterval = Int32(truncatingIfNeeded: config.autoScreenCarouselSecs) + fetchedNode[0].displayConfig?.compassNorthTop = config.compassNorthTop + fetchedNode[0].displayConfig?.flipScreen = config.flipScreen + fetchedNode[0].displayConfig?.oledType = Int32(config.oled.rawValue) + fetchedNode[0].displayConfig?.displayMode = Int32(config.displaymode.rawValue) + fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue) + fetchedNode[0].displayConfig?.headingBold = config.headingBold + fetchedNode[0].displayConfig?.use12HClock = config.use12HClock + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + + try context.save() + Logger.data.info("๐Ÿ’พ [DisplayConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [DisplayConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("๐Ÿ’ฅ [DisplayConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Display Config") + } + + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [DisplayConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } + } + + func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertLoRaConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("LoRa config received: %@".localized, nodeNum.toHex()) + Logger.data.info("๐Ÿ“ป \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", nodeNum) + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save LoRa Config + if fetchedNode.count > 0 { + if fetchedNode[0].loRaConfig == nil { + // No lora config for node, save a new lora config + let newLoRaConfig = LoRaConfigEntity(context: context) + newLoRaConfig.regionCode = Int32(config.region.rawValue) + newLoRaConfig.usePreset = config.usePreset + newLoRaConfig.modemPreset = Int32(config.modemPreset.rawValue) + newLoRaConfig.bandwidth = Int32(config.bandwidth) + newLoRaConfig.spreadFactor = Int32(config.spreadFactor) + newLoRaConfig.codingRate = Int32(config.codingRate) + newLoRaConfig.frequencyOffset = config.frequencyOffset + newLoRaConfig.overrideFrequency = config.overrideFrequency + newLoRaConfig.overrideDutyCycle = config.overrideDutyCycle + newLoRaConfig.hopLimit = Int32(config.hopLimit) + newLoRaConfig.txPower = Int32(config.txPower) + newLoRaConfig.txEnabled = config.txEnabled + newLoRaConfig.channelNum = Int32(config.channelNum) + newLoRaConfig.sx126xRxBoostedGain = config.sx126XRxBoostedGain + newLoRaConfig.ignoreMqtt = config.ignoreMqtt + newLoRaConfig.okToMqtt = config.configOkToMqtt + fetchedNode[0].loRaConfig = newLoRaConfig + } else { + fetchedNode[0].loRaConfig?.regionCode = Int32(config.region.rawValue) + fetchedNode[0].loRaConfig?.usePreset = config.usePreset + fetchedNode[0].loRaConfig?.modemPreset = Int32(config.modemPreset.rawValue) + fetchedNode[0].loRaConfig?.bandwidth = Int32(config.bandwidth) + fetchedNode[0].loRaConfig?.spreadFactor = Int32(config.spreadFactor) + fetchedNode[0].loRaConfig?.codingRate = Int32(config.codingRate) + fetchedNode[0].loRaConfig?.frequencyOffset = config.frequencyOffset + fetchedNode[0].loRaConfig?.overrideFrequency = config.overrideFrequency + fetchedNode[0].loRaConfig?.overrideDutyCycle = config.overrideDutyCycle + fetchedNode[0].loRaConfig?.hopLimit = Int32(config.hopLimit) + fetchedNode[0].loRaConfig?.txPower = Int32(config.txPower) + fetchedNode[0].loRaConfig?.txEnabled = config.txEnabled + fetchedNode[0].loRaConfig?.channelNum = Int32(config.channelNum) + fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain + fetchedNode[0].loRaConfig?.ignoreMqtt = config.ignoreMqtt + fetchedNode[0].loRaConfig?.okToMqtt = config.configOkToMqtt + fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [LoRaConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [LoRaConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("๐Ÿ’ฅ [LoRaConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Lora Config") + } + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [LoRaConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } + } + + func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertNetworkConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Network config received: %@".localized, String(nodeNum)) + Logger.data.info("๐ŸŒ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save WiFi Config + if !fetchedNode.isEmpty { + if fetchedNode[0].networkConfig == nil { + let newNetworkConfig = NetworkConfigEntity(context: context) + newNetworkConfig.wifiEnabled = config.wifiEnabled + newNetworkConfig.wifiSsid = config.wifiSsid + newNetworkConfig.wifiPsk = config.wifiPsk + newNetworkConfig.ethEnabled = config.ethEnabled + newNetworkConfig.enabledProtocols = Int32(config.enabledProtocols) + fetchedNode[0].networkConfig = newNetworkConfig + } else { + fetchedNode[0].networkConfig?.ethEnabled = config.ethEnabled + fetchedNode[0].networkConfig?.wifiEnabled = config.wifiEnabled + fetchedNode[0].networkConfig?.wifiSsid = config.wifiSsid + fetchedNode[0].networkConfig?.wifiPsk = config.wifiPsk + fetchedNode[0].networkConfig?.enabledProtocols = Int32(config.enabledProtocols) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [NetworkConfigEntity] Updated Network Config for node: \(nodeNum.toHex(), privacy: .public)") + + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [NetworkConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("๐Ÿ’ฅ [NetworkConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Network Config") + } + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [NetworkConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } + } + + func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertPositionConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Position config received: %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿ—บ๏ธ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save LoRa Config + if !fetchedNode.isEmpty { + if fetchedNode[0].positionConfig == nil { + let newPositionConfig = PositionConfigEntity(context: context) + newPositionConfig.smartPositionEnabled = config.positionBroadcastSmartEnabled + newPositionConfig.deviceGpsEnabled = config.gpsEnabled + newPositionConfig.gpsMode = Int32(truncatingIfNeeded: config.gpsMode.rawValue) + newPositionConfig.rxGpio = Int32(truncatingIfNeeded: config.rxGpio) + newPositionConfig.txGpio = Int32(truncatingIfNeeded: config.txGpio) + newPositionConfig.gpsEnGpio = Int32(truncatingIfNeeded: config.gpsEnGpio) + newPositionConfig.fixedPosition = config.fixedPosition + newPositionConfig.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs) + newPositionConfig.broadcastSmartMinimumIntervalSecs = Int32(truncatingIfNeeded: config.broadcastSmartMinimumIntervalSecs) + newPositionConfig.broadcastSmartMinimumDistance = Int32(truncatingIfNeeded: config.broadcastSmartMinimumDistance) + newPositionConfig.positionFlags = Int32(truncatingIfNeeded: config.positionFlags) + newPositionConfig.gpsAttemptTime = 900 + newPositionConfig.gpsUpdateInterval = Int32(truncatingIfNeeded: config.gpsUpdateInterval) + fetchedNode[0].positionConfig = newPositionConfig + } else { + fetchedNode[0].positionConfig?.smartPositionEnabled = config.positionBroadcastSmartEnabled + fetchedNode[0].positionConfig?.deviceGpsEnabled = config.gpsEnabled + fetchedNode[0].positionConfig?.gpsMode = Int32(truncatingIfNeeded: config.gpsMode.rawValue) + fetchedNode[0].positionConfig?.rxGpio = Int32(truncatingIfNeeded: config.rxGpio) + fetchedNode[0].positionConfig?.txGpio = Int32(truncatingIfNeeded: config.txGpio) + fetchedNode[0].positionConfig?.gpsEnGpio = Int32(truncatingIfNeeded: config.gpsEnGpio) + fetchedNode[0].positionConfig?.fixedPosition = config.fixedPosition + fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs) + fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(truncatingIfNeeded: config.broadcastSmartMinimumIntervalSecs) + fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(truncatingIfNeeded: config.broadcastSmartMinimumDistance) + fetchedNode[0].positionConfig?.gpsAttemptTime = 900 + fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(truncatingIfNeeded: config.gpsUpdateInterval) + fetchedNode[0].positionConfig?.positionFlags = Int32(truncatingIfNeeded: config.positionFlags) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [PositionConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [PositionConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("๐Ÿ’ฅ [PositionConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Position Config") + } + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [PositionConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } + } + + func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertPowerConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + let logString = String.localizedStringWithFormat("Power config received: %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿ—บ๏ธ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Power Config + if !fetchedNode.isEmpty { + if fetchedNode[0].powerConfig == nil { + let newPowerConfig = PowerConfigEntity(context: context) + newPowerConfig.adcMultiplierOverride = config.adcMultiplierOverride + newPowerConfig.deviceBatteryInaAddress = Int32(config.deviceBatteryInaAddress) + newPowerConfig.isPowerSaving = config.isPowerSaving + newPowerConfig.lsSecs = Int32(truncatingIfNeeded: config.lsSecs) + newPowerConfig.minWakeSecs = Int32(truncatingIfNeeded: config.minWakeSecs) + newPowerConfig.onBatteryShutdownAfterSecs = Int32(truncatingIfNeeded: config.onBatteryShutdownAfterSecs) + newPowerConfig.waitBluetoothSecs = Int32(truncatingIfNeeded: config.waitBluetoothSecs) + fetchedNode[0].powerConfig = newPowerConfig + } else { + fetchedNode[0].powerConfig?.adcMultiplierOverride = config.adcMultiplierOverride + fetchedNode[0].powerConfig?.deviceBatteryInaAddress = Int32(config.deviceBatteryInaAddress) + fetchedNode[0].powerConfig?.isPowerSaving = config.isPowerSaving + fetchedNode[0].powerConfig?.lsSecs = Int32(truncatingIfNeeded: config.lsSecs) + fetchedNode[0].powerConfig?.minWakeSecs = Int32(truncatingIfNeeded: config.minWakeSecs) + fetchedNode[0].powerConfig?.onBatteryShutdownAfterSecs = Int32(truncatingIfNeeded: config.onBatteryShutdownAfterSecs) + fetchedNode[0].powerConfig?.waitBluetoothSecs = Int32(truncatingIfNeeded: config.waitBluetoothSecs) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [PowerConfigEntity] Updated Power Config for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [PowerConfigEntity] Error Updating Core Data PowerConfigEntity: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("๐Ÿ’ฅ [PowerConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Power Config") + } + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [PowerConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } + } + + func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertSecurityConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("mesh.log.security.config %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿ›ก๏ธ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Security Config + if !fetchedNode.isEmpty { + if fetchedNode[0].securityConfig == nil { + let newSecurityConfig = SecurityConfigEntity(context: context) + newSecurityConfig.publicKey = config.publicKey + newSecurityConfig.privateKey = config.privateKey + if config.adminKey.count > 0 { + newSecurityConfig.adminKey = config.adminKey[0] } + newSecurityConfig.isManaged = config.isManaged + newSecurityConfig.serialEnabled = config.serialEnabled + newSecurityConfig.debugLogApiEnabled = config.debugLogApiEnabled + newSecurityConfig.adminChannelEnabled = config.adminChannelEnabled + fetchedNode[0].securityConfig = newSecurityConfig + } else { + fetchedNode[0].securityConfig?.publicKey = config.publicKey + fetchedNode[0].securityConfig?.privateKey = config.privateKey + if config.adminKey.count > 0 { + fetchedNode[0].securityConfig?.adminKey = config.adminKey[0] + if config.adminKey.count > 1 { + fetchedNode[0].securityConfig?.adminKey2 = config.adminKey[1] + } + if config.adminKey.count > 2 { + fetchedNode[0].securityConfig?.adminKey3 = config.adminKey[2] + } + } + fetchedNode[0].securityConfig?.isManaged = config.isManaged + fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled + fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled + fetchedNode[0].securityConfig?.adminChannelEnabled = config.adminChannelEnabled } - fetchedNode[0].securityConfig?.isManaged = config.isManaged - fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled - fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled - fetchedNode[0].securityConfig?.adminChannelEnabled = config.adminChannelEnabled - } - if sessionPasskey?.count != 0 { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [SecurityConfigEntity] Updated Security Config for node: \(nodeNum.toHex(), privacy: .public)") - - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [SecurityConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [SecurityConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Security Config") - } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [SecurityConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") - } -} - -func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Ambient Lighting module config received: %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿฎ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Ambient Lighting Config - if !fetchedNode.isEmpty { - - if fetchedNode[0].cannedMessageConfig == nil { - let newAmbientLightingConfig = AmbientLightingConfigEntity(context: context) - newAmbientLightingConfig.ledState = config.ledState - newAmbientLightingConfig.current = Int32(config.current) - newAmbientLightingConfig.red = Int32(config.red) - newAmbientLightingConfig.green = Int32(config.green) - newAmbientLightingConfig.blue = Int32(config.blue) - fetchedNode[0].ambientLightingConfig = newAmbientLightingConfig - } else { - - if fetchedNode[0].ambientLightingConfig == nil { - fetchedNode[0].ambientLightingConfig = AmbientLightingConfigEntity(context: context) + if sessionPasskey?.count != 0 { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [SecurityConfigEntity] Updated Security Config for node: \(nodeNum.toHex(), privacy: .public)") + + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [SecurityConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } - fetchedNode[0].ambientLightingConfig?.ledState = config.ledState - fetchedNode[0].ambientLightingConfig?.current = Int32(config.current) - fetchedNode[0].ambientLightingConfig?.red = Int32(config.red) - fetchedNode[0].ambientLightingConfig?.green = Int32(config.green) - fetchedNode[0].ambientLightingConfig?.blue = Int32(config.blue) - } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [AmbientLightingConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [AmbientLightingConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [AmbientLightingConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Ambient Lighting Module Config") - } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [AmbientLightingConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") - } -} - -func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Canned Message module config received: %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿฅซ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Canned Message Config - if !fetchedNode.isEmpty { - - if fetchedNode[0].cannedMessageConfig == nil { - let newCannedMessageConfig = CannedMessageConfigEntity(context: context) - newCannedMessageConfig.enabled = config.enabled - newCannedMessageConfig.sendBell = config.sendBell - newCannedMessageConfig.rotary1Enabled = config.rotary1Enabled - newCannedMessageConfig.updown1Enabled = config.updown1Enabled - newCannedMessageConfig.inputbrokerPinA = Int32(config.inputbrokerPinA) - newCannedMessageConfig.inputbrokerPinB = Int32(config.inputbrokerPinB) - newCannedMessageConfig.inputbrokerPinPress = Int32(config.inputbrokerPinPress) - newCannedMessageConfig.inputbrokerEventCw = Int32(config.inputbrokerEventCw.rawValue) - newCannedMessageConfig.inputbrokerEventCcw = Int32(config.inputbrokerEventCcw.rawValue) - newCannedMessageConfig.inputbrokerEventPress = Int32(config.inputbrokerEventPress.rawValue) - fetchedNode[0].cannedMessageConfig = newCannedMessageConfig } else { - fetchedNode[0].cannedMessageConfig?.enabled = config.enabled - fetchedNode[0].cannedMessageConfig?.sendBell = config.sendBell - fetchedNode[0].cannedMessageConfig?.rotary1Enabled = config.rotary1Enabled - fetchedNode[0].cannedMessageConfig?.updown1Enabled = config.updown1Enabled - fetchedNode[0].cannedMessageConfig?.inputbrokerPinA = Int32(config.inputbrokerPinA) - fetchedNode[0].cannedMessageConfig?.inputbrokerPinB = Int32(config.inputbrokerPinB) - fetchedNode[0].cannedMessageConfig?.inputbrokerPinPress = Int32(config.inputbrokerPinPress) - fetchedNode[0].cannedMessageConfig?.inputbrokerEventCw = Int32(config.inputbrokerEventCw.rawValue) - fetchedNode[0].cannedMessageConfig?.inputbrokerEventCcw = Int32(config.inputbrokerEventCcw.rawValue) - fetchedNode[0].cannedMessageConfig?.inputbrokerEventPress = Int32(config.inputbrokerEventPress.rawValue) + Logger.data.error("๐Ÿ’ฅ [SecurityConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Security Config") } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [CannedMessageConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [CannedMessageConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [CannedMessageConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Canned Message Module Config") + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [SecurityConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [CannedMessageConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } -} - -func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Detection Sensor module config received: %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿ•ต๏ธ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Detection Sensor Config - if !fetchedNode.isEmpty { - if fetchedNode[0].detectionSensorConfig == nil { - let newConfig = DetectionSensorConfigEntity(context: context) - newConfig.enabled = config.enabled - newConfig.sendBell = config.sendBell - newConfig.name = config.name - newConfig.monitorPin = Int32(config.monitorPin) - newConfig.triggerType = Int32(config.detectionTriggerType.rawValue) - newConfig.usePullup = config.usePullup - newConfig.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) - newConfig.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) - fetchedNode[0].detectionSensorConfig = newConfig + + func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Ambient Lighting module config received: %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿฎ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Ambient Lighting Config + if !fetchedNode.isEmpty { + + if fetchedNode[0].cannedMessageConfig == nil { + let newAmbientLightingConfig = AmbientLightingConfigEntity(context: context) + newAmbientLightingConfig.ledState = config.ledState + newAmbientLightingConfig.current = Int32(config.current) + newAmbientLightingConfig.red = Int32(config.red) + newAmbientLightingConfig.green = Int32(config.green) + newAmbientLightingConfig.blue = Int32(config.blue) + fetchedNode[0].ambientLightingConfig = newAmbientLightingConfig + } else { + + if fetchedNode[0].ambientLightingConfig == nil { + fetchedNode[0].ambientLightingConfig = AmbientLightingConfigEntity(context: context) + } + fetchedNode[0].ambientLightingConfig?.ledState = config.ledState + fetchedNode[0].ambientLightingConfig?.current = Int32(config.current) + fetchedNode[0].ambientLightingConfig?.red = Int32(config.red) + fetchedNode[0].ambientLightingConfig?.green = Int32(config.green) + fetchedNode[0].ambientLightingConfig?.blue = Int32(config.blue) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [AmbientLightingConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [AmbientLightingConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } } else { - fetchedNode[0].detectionSensorConfig?.enabled = config.enabled - fetchedNode[0].detectionSensorConfig?.sendBell = config.sendBell - fetchedNode[0].detectionSensorConfig?.name = config.name - fetchedNode[0].detectionSensorConfig?.monitorPin = Int32(config.monitorPin) - fetchedNode[0].detectionSensorConfig?.usePullup = config.usePullup - fetchedNode[0].detectionSensorConfig?.triggerType = Int32(config.detectionTriggerType.rawValue) - fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) - fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) + Logger.data.error("๐Ÿ’ฅ [AmbientLightingConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Ambient Lighting Module Config") } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [DetectionSensorConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [DetectionSensorConfigEntity] Error Updating Core Data : \(nsError, privacy: .public)") - } - - } else { - Logger.data.error("๐Ÿ’ฅ [DetectionSensorConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Detection Sensor Module Config") + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [AmbientLightingConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } - - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [DetectionSensorConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } -} - -func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("External Notification module config received: %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿ“ฃ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save External Notificaitone Config - if !fetchedNode.isEmpty { - - if fetchedNode[0].externalNotificationConfig == nil { - let newExternalNotificationConfig = ExternalNotificationConfigEntity(context: context) - newExternalNotificationConfig.enabled = config.enabled - newExternalNotificationConfig.usePWM = config.usePwm - newExternalNotificationConfig.alertBell = config.alertBell - newExternalNotificationConfig.alertBellBuzzer = config.alertBellBuzzer - newExternalNotificationConfig.alertBellVibra = config.alertBellVibra - newExternalNotificationConfig.alertMessage = config.alertMessage - newExternalNotificationConfig.alertMessageBuzzer = config.alertMessageBuzzer - newExternalNotificationConfig.alertMessageVibra = config.alertMessageVibra - newExternalNotificationConfig.active = config.active - newExternalNotificationConfig.output = Int32(config.output) - newExternalNotificationConfig.outputBuzzer = Int32(config.outputBuzzer) - newExternalNotificationConfig.outputVibra = Int32(config.outputVibra) - newExternalNotificationConfig.outputMilliseconds = Int32(config.outputMs) - newExternalNotificationConfig.nagTimeout = Int32(config.nagTimeout) - newExternalNotificationConfig.useI2SAsBuzzer = config.useI2SAsBuzzer - fetchedNode[0].externalNotificationConfig = newExternalNotificationConfig + + func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Canned Message module config received: %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿฅซ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Canned Message Config + if !fetchedNode.isEmpty { + + if fetchedNode[0].cannedMessageConfig == nil { + let newCannedMessageConfig = CannedMessageConfigEntity(context: context) + newCannedMessageConfig.enabled = config.enabled + newCannedMessageConfig.sendBell = config.sendBell + newCannedMessageConfig.rotary1Enabled = config.rotary1Enabled + newCannedMessageConfig.updown1Enabled = config.updown1Enabled + newCannedMessageConfig.inputbrokerPinA = Int32(config.inputbrokerPinA) + newCannedMessageConfig.inputbrokerPinB = Int32(config.inputbrokerPinB) + newCannedMessageConfig.inputbrokerPinPress = Int32(config.inputbrokerPinPress) + newCannedMessageConfig.inputbrokerEventCw = Int32(config.inputbrokerEventCw.rawValue) + newCannedMessageConfig.inputbrokerEventCcw = Int32(config.inputbrokerEventCcw.rawValue) + newCannedMessageConfig.inputbrokerEventPress = Int32(config.inputbrokerEventPress.rawValue) + fetchedNode[0].cannedMessageConfig = newCannedMessageConfig + } else { + fetchedNode[0].cannedMessageConfig?.enabled = config.enabled + fetchedNode[0].cannedMessageConfig?.sendBell = config.sendBell + fetchedNode[0].cannedMessageConfig?.rotary1Enabled = config.rotary1Enabled + fetchedNode[0].cannedMessageConfig?.updown1Enabled = config.updown1Enabled + fetchedNode[0].cannedMessageConfig?.inputbrokerPinA = Int32(config.inputbrokerPinA) + fetchedNode[0].cannedMessageConfig?.inputbrokerPinB = Int32(config.inputbrokerPinB) + fetchedNode[0].cannedMessageConfig?.inputbrokerPinPress = Int32(config.inputbrokerPinPress) + fetchedNode[0].cannedMessageConfig?.inputbrokerEventCw = Int32(config.inputbrokerEventCw.rawValue) + fetchedNode[0].cannedMessageConfig?.inputbrokerEventCcw = Int32(config.inputbrokerEventCcw.rawValue) + fetchedNode[0].cannedMessageConfig?.inputbrokerEventPress = Int32(config.inputbrokerEventPress.rawValue) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [CannedMessageConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [CannedMessageConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } } else { - fetchedNode[0].externalNotificationConfig?.enabled = config.enabled - fetchedNode[0].externalNotificationConfig?.usePWM = config.usePwm - fetchedNode[0].externalNotificationConfig?.alertBell = config.alertBell - fetchedNode[0].externalNotificationConfig?.alertBellBuzzer = config.alertBellBuzzer - fetchedNode[0].externalNotificationConfig?.alertBellVibra = config.alertBellVibra - fetchedNode[0].externalNotificationConfig?.alertMessage = config.alertMessage - fetchedNode[0].externalNotificationConfig?.alertMessageBuzzer = config.alertMessageBuzzer - fetchedNode[0].externalNotificationConfig?.alertMessageVibra = config.alertMessageVibra - fetchedNode[0].externalNotificationConfig?.active = config.active - fetchedNode[0].externalNotificationConfig?.output = Int32(config.output) - fetchedNode[0].externalNotificationConfig?.outputBuzzer = Int32(config.outputBuzzer) - fetchedNode[0].externalNotificationConfig?.outputVibra = Int32(config.outputVibra) - fetchedNode[0].externalNotificationConfig?.outputMilliseconds = Int32(config.outputMs) - fetchedNode[0].externalNotificationConfig?.nagTimeout = Int32(config.nagTimeout) - fetchedNode[0].externalNotificationConfig?.useI2SAsBuzzer = config.useI2SAsBuzzer + Logger.data.error("๐Ÿ’ฅ [CannedMessageConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Canned Message Module Config") } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [ExternalNotificationConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [ExternalNotificationConfigEntity] Error Updating Core Data : \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [ExternalNotificationConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save External Notification Module Config") + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [CannedMessageConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [ExternalNotificationConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } -} -func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertDetectionSensorModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } - let logString = String.localizedStringWithFormat("PAX Counter config received: %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save PAX Counter Config - if !fetchedNode.isEmpty { - if fetchedNode[0].paxCounterConfig == nil { - let newPaxCounterConfig = PaxCounterConfigEntity(context: context) - newPaxCounterConfig.enabled = config.enabled - newPaxCounterConfig.updateInterval = Int32(config.paxcounterUpdateInterval) - fetchedNode[0].paxCounterConfig = newPaxCounterConfig + nonisolated func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Detection Sensor module config received: %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿ•ต๏ธ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Detection Sensor Config + if !fetchedNode.isEmpty { + if fetchedNode[0].detectionSensorConfig == nil { + let newConfig = DetectionSensorConfigEntity(context: context) + newConfig.enabled = config.enabled + newConfig.sendBell = config.sendBell + newConfig.name = config.name + newConfig.monitorPin = Int32(config.monitorPin) + newConfig.triggerType = Int32(config.detectionTriggerType.rawValue) + newConfig.usePullup = config.usePullup + newConfig.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) + newConfig.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) + fetchedNode[0].detectionSensorConfig = newConfig + } else { + fetchedNode[0].detectionSensorConfig?.enabled = config.enabled + fetchedNode[0].detectionSensorConfig?.sendBell = config.sendBell + fetchedNode[0].detectionSensorConfig?.name = config.name + fetchedNode[0].detectionSensorConfig?.monitorPin = Int32(config.monitorPin) + fetchedNode[0].detectionSensorConfig?.usePullup = config.usePullup + fetchedNode[0].detectionSensorConfig?.triggerType = Int32(config.detectionTriggerType.rawValue) + fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) + fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [DetectionSensorConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [DetectionSensorConfigEntity] Error Updating Core Data : \(nsError, privacy: .public)") + } + } else { - fetchedNode[0].paxCounterConfig?.enabled = config.enabled - fetchedNode[0].paxCounterConfig?.updateInterval = Int32(config.paxcounterUpdateInterval) + Logger.data.error("๐Ÿ’ฅ [DetectionSensorConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Detection Sensor Module Config") } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [PaxCounterConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [PaxCounterConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [PaxCounterConfigEntity] No Nodes found in local database matching node number \(nodeNum.toHex(), privacy: .public) unable to save PAX Counter Module Config") + + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [DetectionSensorConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [PaxCounterConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } -} -func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertExternalNotificationModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } - let logString = String.localizedStringWithFormat("RTTTL Ringtone config received: %@".localized, String(nodeNum)) - Logger.data.info("โ›ฐ๏ธ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save RTTTL Config - if !fetchedNode.isEmpty { - if fetchedNode[0].rtttlConfig == nil { - let newRtttlConfig = RTTTLConfigEntity(context: context) - newRtttlConfig.ringtone = ringtone - fetchedNode[0].rtttlConfig = newRtttlConfig + nonisolated func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("External Notification module config received: %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿ“ฃ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save External Notificaitone Config + if !fetchedNode.isEmpty { + + if fetchedNode[0].externalNotificationConfig == nil { + let newExternalNotificationConfig = ExternalNotificationConfigEntity(context: context) + newExternalNotificationConfig.enabled = config.enabled + newExternalNotificationConfig.usePWM = config.usePwm + newExternalNotificationConfig.alertBell = config.alertBell + newExternalNotificationConfig.alertBellBuzzer = config.alertBellBuzzer + newExternalNotificationConfig.alertBellVibra = config.alertBellVibra + newExternalNotificationConfig.alertMessage = config.alertMessage + newExternalNotificationConfig.alertMessageBuzzer = config.alertMessageBuzzer + newExternalNotificationConfig.alertMessageVibra = config.alertMessageVibra + newExternalNotificationConfig.active = config.active + newExternalNotificationConfig.output = Int32(config.output) + newExternalNotificationConfig.outputBuzzer = Int32(config.outputBuzzer) + newExternalNotificationConfig.outputVibra = Int32(config.outputVibra) + newExternalNotificationConfig.outputMilliseconds = Int32(config.outputMs) + newExternalNotificationConfig.nagTimeout = Int32(config.nagTimeout) + newExternalNotificationConfig.useI2SAsBuzzer = config.useI2SAsBuzzer + fetchedNode[0].externalNotificationConfig = newExternalNotificationConfig + } else { + fetchedNode[0].externalNotificationConfig?.enabled = config.enabled + fetchedNode[0].externalNotificationConfig?.usePWM = config.usePwm + fetchedNode[0].externalNotificationConfig?.alertBell = config.alertBell + fetchedNode[0].externalNotificationConfig?.alertBellBuzzer = config.alertBellBuzzer + fetchedNode[0].externalNotificationConfig?.alertBellVibra = config.alertBellVibra + fetchedNode[0].externalNotificationConfig?.alertMessage = config.alertMessage + fetchedNode[0].externalNotificationConfig?.alertMessageBuzzer = config.alertMessageBuzzer + fetchedNode[0].externalNotificationConfig?.alertMessageVibra = config.alertMessageVibra + fetchedNode[0].externalNotificationConfig?.active = config.active + fetchedNode[0].externalNotificationConfig?.output = Int32(config.output) + fetchedNode[0].externalNotificationConfig?.outputBuzzer = Int32(config.outputBuzzer) + fetchedNode[0].externalNotificationConfig?.outputVibra = Int32(config.outputVibra) + fetchedNode[0].externalNotificationConfig?.outputMilliseconds = Int32(config.outputMs) + fetchedNode[0].externalNotificationConfig?.nagTimeout = Int32(config.nagTimeout) + fetchedNode[0].externalNotificationConfig?.useI2SAsBuzzer = config.useI2SAsBuzzer + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [ExternalNotificationConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [ExternalNotificationConfigEntity] Error Updating Core Data : \(nsError, privacy: .public)") + } } else { - fetchedNode[0].rtttlConfig?.ringtone = ringtone + Logger.data.error("๐Ÿ’ฅ [ExternalNotificationConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save External Notification Module Config") } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [RtttlConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [RtttlConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [RtttlConfigEntity] No nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save RTTTL Ringtone Config") + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [ExternalNotificationConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [RtttlConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } -} - -func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("MQTT module config received: %@".localized, String(nodeNum)) - Logger.data.info("๐ŸŒ‰ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save MQTT Config - if !fetchedNode.isEmpty { - if fetchedNode[0].mqttConfig == nil { - let newMQTTConfig = MQTTConfigEntity(context: context) - newMQTTConfig.enabled = config.enabled - newMQTTConfig.proxyToClientEnabled = config.proxyToClientEnabled - newMQTTConfig.address = config.address - newMQTTConfig.username = config.username - newMQTTConfig.password = config.password - newMQTTConfig.root = config.root - newMQTTConfig.encryptionEnabled = config.encryptionEnabled - newMQTTConfig.jsonEnabled = config.jsonEnabled - newMQTTConfig.tlsEnabled = config.tlsEnabled - newMQTTConfig.mapReportingEnabled = config.mapReportingEnabled - newMQTTConfig.mapReportingShouldReportLocation = config.mapReportSettings.shouldReportLocation - newMQTTConfig.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) - newMQTTConfig.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) - fetchedNode[0].mqttConfig = newMQTTConfig + + func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertPaxCounterModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("PAX Counter config received: %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save PAX Counter Config + if !fetchedNode.isEmpty { + if fetchedNode[0].paxCounterConfig == nil { + let newPaxCounterConfig = PaxCounterConfigEntity(context: context) + newPaxCounterConfig.enabled = config.enabled + newPaxCounterConfig.updateInterval = Int32(config.paxcounterUpdateInterval) + fetchedNode[0].paxCounterConfig = newPaxCounterConfig + } else { + fetchedNode[0].paxCounterConfig?.enabled = config.enabled + fetchedNode[0].paxCounterConfig?.updateInterval = Int32(config.paxcounterUpdateInterval) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [PaxCounterConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [PaxCounterConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } } else { - fetchedNode[0].mqttConfig?.enabled = config.enabled - fetchedNode[0].mqttConfig?.proxyToClientEnabled = config.proxyToClientEnabled - fetchedNode[0].mqttConfig?.address = config.address - fetchedNode[0].mqttConfig?.username = config.username - fetchedNode[0].mqttConfig?.password = config.password - fetchedNode[0].mqttConfig?.root = config.root - fetchedNode[0].mqttConfig?.encryptionEnabled = config.encryptionEnabled - fetchedNode[0].mqttConfig?.jsonEnabled = config.jsonEnabled - fetchedNode[0].mqttConfig?.tlsEnabled = config.tlsEnabled - fetchedNode[0].mqttConfig?.mapReportingEnabled = config.mapReportingEnabled - fetchedNode[0].mqttConfig?.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) - fetchedNode[0].mqttConfig?.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) + Logger.data.error("๐Ÿ’ฅ [PaxCounterConfigEntity] No Nodes found in local database matching node number \(nodeNum.toHex(), privacy: .public) unable to save PAX Counter Module Config") } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [MQTTConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [MQTTConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [MQTTConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save MQTT Module Config") + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [PaxCounterConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [MQTTConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } -} -func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Range Test module config received: %@".localized, String(nodeNum)) - Logger.data.info("โ›ฐ๏ธ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Device Config - if !fetchedNode.isEmpty { - if fetchedNode[0].rangeTestConfig == nil { - let newRangeTestConfig = RangeTestConfigEntity(context: context) - newRangeTestConfig.sender = Int32(config.sender) - newRangeTestConfig.enabled = config.enabled - newRangeTestConfig.save = config.save - fetchedNode[0].rangeTestConfig = newRangeTestConfig + func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("RTTTL Ringtone config received: %@".localized, String(nodeNum)) + Logger.data.info("โ›ฐ๏ธ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save RTTTL Config + if !fetchedNode.isEmpty { + if fetchedNode[0].rtttlConfig == nil { + let newRtttlConfig = RTTTLConfigEntity(context: context) + newRtttlConfig.ringtone = ringtone + fetchedNode[0].rtttlConfig = newRtttlConfig + } else { + fetchedNode[0].rtttlConfig?.ringtone = ringtone + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [RtttlConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [RtttlConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } } else { - fetchedNode[0].rangeTestConfig?.sender = Int32(config.sender) - fetchedNode[0].rangeTestConfig?.enabled = config.enabled - fetchedNode[0].rangeTestConfig?.save = config.save + Logger.data.error("๐Ÿ’ฅ [RtttlConfigEntity] No nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save RTTTL Ringtone Config") } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [RangeTestConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [RangeTestConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Range Test Module Config") + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [RtttlConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [RangeTestConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } -} -func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Serial module config received: %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿค– \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Device Config - if !fetchedNode.isEmpty { - if fetchedNode[0].serialConfig == nil { - let newSerialConfig = SerialConfigEntity(context: context) - newSerialConfig.enabled = config.enabled - newSerialConfig.echo = config.echo - newSerialConfig.rxd = Int32(config.rxd) - newSerialConfig.txd = Int32(config.txd) - newSerialConfig.baudRate = Int32(config.baud.rawValue) - newSerialConfig.timeout = Int32(config.timeout) - newSerialConfig.mode = Int32(config.mode.rawValue) - fetchedNode[0].serialConfig = newSerialConfig + func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertMqttModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("MQTT module config received: %@".localized, String(nodeNum)) + Logger.data.info("๐ŸŒ‰ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save MQTT Config + if !fetchedNode.isEmpty { + if fetchedNode[0].mqttConfig == nil { + let newMQTTConfig = MQTTConfigEntity(context: context) + newMQTTConfig.enabled = config.enabled + newMQTTConfig.proxyToClientEnabled = config.proxyToClientEnabled + newMQTTConfig.address = config.address + newMQTTConfig.username = config.username + newMQTTConfig.password = config.password + newMQTTConfig.root = config.root + newMQTTConfig.encryptionEnabled = config.encryptionEnabled + newMQTTConfig.jsonEnabled = config.jsonEnabled + newMQTTConfig.tlsEnabled = config.tlsEnabled + newMQTTConfig.mapReportingEnabled = config.mapReportingEnabled + newMQTTConfig.mapReportingShouldReportLocation = config.mapReportSettings.shouldReportLocation + newMQTTConfig.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) + newMQTTConfig.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) + fetchedNode[0].mqttConfig = newMQTTConfig + } else { + fetchedNode[0].mqttConfig?.enabled = config.enabled + fetchedNode[0].mqttConfig?.proxyToClientEnabled = config.proxyToClientEnabled + fetchedNode[0].mqttConfig?.address = config.address + fetchedNode[0].mqttConfig?.username = config.username + fetchedNode[0].mqttConfig?.password = config.password + fetchedNode[0].mqttConfig?.root = config.root + fetchedNode[0].mqttConfig?.encryptionEnabled = config.encryptionEnabled + fetchedNode[0].mqttConfig?.jsonEnabled = config.jsonEnabled + fetchedNode[0].mqttConfig?.tlsEnabled = config.tlsEnabled + fetchedNode[0].mqttConfig?.mapReportingEnabled = config.mapReportingEnabled + fetchedNode[0].mqttConfig?.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) + fetchedNode[0].mqttConfig?.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [MQTTConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [MQTTConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } } else { - fetchedNode[0].serialConfig?.enabled = config.enabled - fetchedNode[0].serialConfig?.echo = config.echo - fetchedNode[0].serialConfig?.rxd = Int32(config.rxd) - fetchedNode[0].serialConfig?.txd = Int32(config.txd) - fetchedNode[0].serialConfig?.baudRate = Int32(config.baud.rawValue) - fetchedNode[0].serialConfig?.timeout = Int32(config.timeout) - fetchedNode[0].serialConfig?.mode = Int32(config.mode.rawValue) + Logger.data.error("๐Ÿ’ฅ [MQTTConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save MQTT Module Config") } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [SerialConfigEntity]Updated Serial Module Config for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - - context.rollback() - - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [SerialConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [SerialConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Serial Module Config") + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [MQTTConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } - } catch { - - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [SerialConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } -} -func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Store & Forward module config received: %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿ“ฌ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Store & Forward Sensor Config - if !fetchedNode.isEmpty { - if fetchedNode[0].storeForwardConfig == nil { - let newConfig = StoreForwardConfigEntity(context: context) - newConfig.enabled = config.enabled - newConfig.heartbeat = config.heartbeat - newConfig.records = Int32(config.records) - newConfig.historyReturnMax = Int32(config.historyReturnMax) - newConfig.historyReturnWindow = Int32(config.historyReturnWindow) - newConfig.isRouter = config.isServer - fetchedNode[0].storeForwardConfig = newConfig + func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertRangeTestModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Range Test module config received: %@".localized, String(nodeNum)) + Logger.data.info("โ›ฐ๏ธ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Device Config + if !fetchedNode.isEmpty { + if fetchedNode[0].rangeTestConfig == nil { + let newRangeTestConfig = RangeTestConfigEntity(context: context) + newRangeTestConfig.sender = Int32(config.sender) + newRangeTestConfig.enabled = config.enabled + newRangeTestConfig.save = config.save + fetchedNode[0].rangeTestConfig = newRangeTestConfig + } else { + fetchedNode[0].rangeTestConfig?.sender = Int32(config.sender) + fetchedNode[0].rangeTestConfig?.enabled = config.enabled + fetchedNode[0].rangeTestConfig?.save = config.save + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [RangeTestConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } } else { - fetchedNode[0].storeForwardConfig?.enabled = config.enabled - fetchedNode[0].storeForwardConfig?.heartbeat = config.heartbeat - fetchedNode[0].storeForwardConfig?.records = Int32(config.records) - fetchedNode[0].storeForwardConfig?.historyReturnMax = Int32(config.historyReturnMax) - fetchedNode[0].storeForwardConfig?.historyReturnWindow = Int32(config.historyReturnWindow) + Logger.data.error("๐Ÿ’ฅ [RangeTestConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Range Test Module Config") } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [StoreForwardConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [StoreForwardConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - } else { - Logger.data.error("๐Ÿ’ฅ [StoreForwardConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Store & Forward Module Config") + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [RangeTestConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [StoreForwardConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } -} -func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat("Telemetry module config received: %@".localized, String(nodeNum)) - Logger.data.info("๐Ÿ“ˆ \(logString, privacy: .public)") - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - // Found a node, save Telemetry Config - if !fetchedNode.isEmpty { - if fetchedNode[0].telemetryConfig == nil { - let newTelemetryConfig = TelemetryConfigEntity(context: context) - newTelemetryConfig.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval) - newTelemetryConfig.deviceTelemetryEnabled = config.deviceTelemetryEnabled - newTelemetryConfig.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval) - newTelemetryConfig.environmentMeasurementEnabled = config.environmentMeasurementEnabled - newTelemetryConfig.environmentScreenEnabled = config.environmentScreenEnabled - newTelemetryConfig.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit - newTelemetryConfig.powerMeasurementEnabled = config.powerMeasurementEnabled - newTelemetryConfig.powerUpdateInterval = Int32(truncatingIfNeeded: config.powerUpdateInterval) - newTelemetryConfig.powerScreenEnabled = config.powerScreenEnabled - fetchedNode[0].telemetryConfig = newTelemetryConfig + func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertSerialModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Serial module config received: %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿค– \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Device Config + if !fetchedNode.isEmpty { + if fetchedNode[0].serialConfig == nil { + let newSerialConfig = SerialConfigEntity(context: context) + newSerialConfig.enabled = config.enabled + newSerialConfig.echo = config.echo + newSerialConfig.rxd = Int32(config.rxd) + newSerialConfig.txd = Int32(config.txd) + newSerialConfig.baudRate = Int32(config.baud.rawValue) + newSerialConfig.timeout = Int32(config.timeout) + newSerialConfig.mode = Int32(config.mode.rawValue) + fetchedNode[0].serialConfig = newSerialConfig + } else { + fetchedNode[0].serialConfig?.enabled = config.enabled + fetchedNode[0].serialConfig?.echo = config.echo + fetchedNode[0].serialConfig?.rxd = Int32(config.rxd) + fetchedNode[0].serialConfig?.txd = Int32(config.txd) + fetchedNode[0].serialConfig?.baudRate = Int32(config.baud.rawValue) + fetchedNode[0].serialConfig?.timeout = Int32(config.timeout) + fetchedNode[0].serialConfig?.mode = Int32(config.mode.rawValue) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [SerialConfigEntity]Updated Serial Module Config for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + + context.rollback() + + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [SerialConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } } else { - fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval) - fetchedNode[0].telemetryConfig?.deviceTelemetryEnabled = config.deviceTelemetryEnabled - fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval) - fetchedNode[0].telemetryConfig?.environmentMeasurementEnabled = config.environmentMeasurementEnabled - fetchedNode[0].telemetryConfig?.environmentScreenEnabled = config.environmentScreenEnabled - fetchedNode[0].telemetryConfig?.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit - fetchedNode[0].telemetryConfig?.powerMeasurementEnabled = config.powerMeasurementEnabled - fetchedNode[0].telemetryConfig?.powerUpdateInterval = Int32(truncatingIfNeeded: config.powerUpdateInterval) - fetchedNode[0].telemetryConfig?.powerScreenEnabled = config.powerScreenEnabled + Logger.data.error("๐Ÿ’ฅ [SerialConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Serial Module Config") } - if sessionPasskey != nil { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } - do { - try context.save() - Logger.data.info("๐Ÿ’พ [TelemetryConfigEntity] Updated Telemetry Module Config for node: \(nodeNum.toHex(), privacy: .public)") - - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [TelemetryConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") - } - - } else { - Logger.data.error("๐Ÿ’ฅ [TelemetryConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Telemetry Module Config") + } catch { + + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [SerialConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") } + } - } catch { - let nsError = error as NSError - Logger.data.error("๐Ÿ’ฅ [TelemetryConfigEntity] Fetching node for core data TelemetryConfigEntity failed: \(nsError, privacy: .public)") + func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertStoreForwardModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Store & Forward module config received: %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿ“ฌ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Store & Forward Sensor Config + if !fetchedNode.isEmpty { + if fetchedNode[0].storeForwardConfig == nil { + let newConfig = StoreForwardConfigEntity(context: context) + newConfig.enabled = config.enabled + newConfig.heartbeat = config.heartbeat + newConfig.records = Int32(config.records) + newConfig.historyReturnMax = Int32(config.historyReturnMax) + newConfig.historyReturnWindow = Int32(config.historyReturnWindow) + newConfig.isRouter = config.isServer + fetchedNode[0].storeForwardConfig = newConfig + } else { + fetchedNode[0].storeForwardConfig?.enabled = config.enabled + fetchedNode[0].storeForwardConfig?.heartbeat = config.heartbeat + fetchedNode[0].storeForwardConfig?.records = Int32(config.records) + fetchedNode[0].storeForwardConfig?.historyReturnMax = Int32(config.historyReturnMax) + fetchedNode[0].storeForwardConfig?.historyReturnWindow = Int32(config.historyReturnWindow) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [StoreForwardConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [StoreForwardConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("๐Ÿ’ฅ [StoreForwardConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Store & Forward Module Config") + } + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [StoreForwardConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } + } + + func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertTelemetryModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("Telemetry module config received: %@".localized, String(nodeNum)) + Logger.data.info("๐Ÿ“ˆ \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Telemetry Config + if !fetchedNode.isEmpty { + if fetchedNode[0].telemetryConfig == nil { + let newTelemetryConfig = TelemetryConfigEntity(context: context) + newTelemetryConfig.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval) + newTelemetryConfig.deviceTelemetryEnabled = config.deviceTelemetryEnabled + newTelemetryConfig.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval) + newTelemetryConfig.environmentMeasurementEnabled = config.environmentMeasurementEnabled + newTelemetryConfig.environmentScreenEnabled = config.environmentScreenEnabled + newTelemetryConfig.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit + newTelemetryConfig.powerMeasurementEnabled = config.powerMeasurementEnabled + newTelemetryConfig.powerUpdateInterval = Int32(truncatingIfNeeded: config.powerUpdateInterval) + newTelemetryConfig.powerScreenEnabled = config.powerScreenEnabled + fetchedNode[0].telemetryConfig = newTelemetryConfig + } else { + fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval) + fetchedNode[0].telemetryConfig?.deviceTelemetryEnabled = config.deviceTelemetryEnabled + fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval) + fetchedNode[0].telemetryConfig?.environmentMeasurementEnabled = config.environmentMeasurementEnabled + fetchedNode[0].telemetryConfig?.environmentScreenEnabled = config.environmentScreenEnabled + fetchedNode[0].telemetryConfig?.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit + fetchedNode[0].telemetryConfig?.powerMeasurementEnabled = config.powerMeasurementEnabled + fetchedNode[0].telemetryConfig?.powerUpdateInterval = Int32(truncatingIfNeeded: config.powerUpdateInterval) + fetchedNode[0].telemetryConfig?.powerScreenEnabled = config.powerScreenEnabled + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("๐Ÿ’พ [TelemetryConfigEntity] Updated Telemetry Module Config for node: \(nodeNum.toHex(), privacy: .public)") + + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [TelemetryConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + + } else { + Logger.data.error("๐Ÿ’ฅ [TelemetryConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Telemetry Module Config") + } + + } catch { + let nsError = error as NSError + Logger.data.error("๐Ÿ’ฅ [TelemetryConfigEntity] Fetching node for core data TelemetryConfigEntity failed: \(nsError, privacy: .public)") + } } } diff --git a/Meshtastic/Views/Connect/Connect.swift b/Meshtastic/Views/Connect/Connect.swift index bb43ae04..b66b1b59 100644 --- a/Meshtastic/Views/Connect/Connect.swift +++ b/Meshtastic/Views/Connect/Connect.swift @@ -511,23 +511,23 @@ struct ManualConnectionMenu: View { }) }.confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { Button("Connect to new radio?", role: .destructive) { - if let device = deviceForManualConnection { - UserDefaults.preferredPeripheralId = device.id.uuidString - UserDefaults.preferredPeripheralNum = 0 - if accessoryManager.allowDisconnect { - Task { try await accessoryManager.disconnect() } - } - clearCoreDataDatabase(context: context, includeRoutes: false) - clearNotifications() - Task { - try await selectedTransport?.transport.manuallyConnect(toDevice: device) - } - - // Clean up just in case - deviceForManualConnection = nil - } - } - } + Task { + if let device = deviceForManualConnection { + UserDefaults.preferredPeripheralId = device.id.uuidString + UserDefaults.preferredPeripheralNum = 0 + if accessoryManager.allowDisconnect { + try await accessoryManager.disconnect() + } + await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false) + clearNotifications() + try await selectedTransport?.transport.manuallyConnect(toDevice: device) + + // Clean up just in case + deviceForManualConnection = nil + } + } + } + } } } @@ -593,15 +593,17 @@ struct DeviceConnectRow: View { }.padding([.bottom, .top]) .confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { Button("Connect to new radio?", role: .destructive) { - UserDefaults.preferredPeripheralId = device.id.uuidString - UserDefaults.preferredPeripheralNum = 0 - if accessoryManager.allowDisconnect { - Task { try await accessoryManager.disconnect() } - } - clearCoreDataDatabase(context: context, includeRoutes: false) - clearNotifications() Task { + UserDefaults.preferredPeripheralId = device.id.uuidString + UserDefaults.preferredPeripheralNum = 0 + if accessoryManager.allowDisconnect { + try await accessoryManager.disconnect() + } + await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false) + clearNotifications() + try await accessoryManager.connect(to: device) + } } } diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 16660203..477a03ed 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -160,9 +160,11 @@ struct ChannelList: View { titleVisibility: .visible ) { Button(role: .destructive) { - deleteChannelMessages(channel: channelToDeleteMessages!, context: context) - context.refresh(myInfo, mergeChanges: true) - channelToDeleteMessages = nil + Task { + await MeshPackets.shared.deleteChannelMessages(channel: channelToDeleteMessages!) + context.refresh(myInfo, mergeChanges: true) + channelToDeleteMessages = nil + } } label: { Text("Delete") } diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 16ba0b7b..ba07a9bf 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -224,8 +224,10 @@ fileprivate struct FilteredUserList: View { titleVisibility: .visible ) { Button(role: .destructive) { - deleteUserMessages(user: userToDeleteMessages!, context: context) - context.refresh(node!.user!, mergeChanges: true) + Task { + await MeshPackets.shared.deleteUserMessages(user: userToDeleteMessages!) + context.refresh(node!.user!, mergeChanges: true) + } } label: { Text("Delete") } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 80d47fbd..90e8d119 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -199,10 +199,12 @@ struct DeviceMetricsLog: View { titleVisibility: .visible ) { Button("Delete all device metrics?", role: .destructive) { - if clearTelemetry(destNum: node.num, metricsType: 0, context: context) { - Logger.data.notice("Cleared Device Metrics for \(node.num, privacy: .public)") - } else { - Logger.data.error("Clear Device Metrics Log Failed") + Task { + if await MeshPackets.shared.clearTelemetry(destNum: node.num, metricsType: 0) { + Logger.data.notice("Cleared Device Metrics for \(node.num, privacy: .public)") + } else { + Logger.data.error("Clear Device Metrics Log Failed") + } } } } diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 6eec29f3..84148d2e 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -128,8 +128,10 @@ struct EnvironmentMetricsLog: View { titleVisibility: .visible ) { Button("Delete all environment metrics?", role: .destructive) { - if clearTelemetry(destNum: node.num, metricsType: 1, context: context) { - Logger.services.error("Clear Environment Metrics Log Failed") + Task { + if await MeshPackets.shared.clearTelemetry(destNum: node.num, metricsType: 1) { + Logger.services.error("Clear Environment Metrics Log Failed") + } } } } diff --git a/Meshtastic/Views/Nodes/PaxCounterLog.swift b/Meshtastic/Views/Nodes/PaxCounterLog.swift index 34285ddb..482e0d69 100644 --- a/Meshtastic/Views/Nodes/PaxCounterLog.swift +++ b/Meshtastic/Views/Nodes/PaxCounterLog.swift @@ -175,10 +175,12 @@ struct PaxCounterLog: View { titleVisibility: .visible ) { Button("Delete all pax data?", role: .destructive) { - if clearPax(destNum: node.num, context: context) { - Logger.services.info("Cleared Pax Counter for \(node.num, privacy: .public)") - } else { - Logger.services.error("Clear Pax Counter Log Failed") + Task { + if await MeshPackets.shared.clearPax(destNum: node.num) { + Logger.services.info("Cleared Pax Counter for \(node.num, privacy: .public)") + } else { + Logger.services.error("Clear Pax Counter Log Failed") + } } } } diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index b2be785d..af307f20 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -131,10 +131,12 @@ struct PositionLog: View { titleVisibility: .visible ) { Button("Delete all positions?", role: .destructive) { - if clearPositions(destNum: node.num, context: context) { - Logger.services.info("Successfully Cleared Position Log") - } else { - Logger.services.error("Clear Position Log Failed") + Task { + if await MeshPackets.shared.clearPositions(destNum: node.num) { + Logger.services.info("Successfully Cleared Position Log") + } else { + Logger.services.error("Clear Position Log Failed") + } } } } diff --git a/Meshtastic/Views/Nodes/PowerMetricsLog.swift b/Meshtastic/Views/Nodes/PowerMetricsLog.swift index a555effd..b4578a59 100644 --- a/Meshtastic/Views/Nodes/PowerMetricsLog.swift +++ b/Meshtastic/Views/Nodes/PowerMetricsLog.swift @@ -242,10 +242,12 @@ struct PowerMetricsLog: View { titleVisibility: .visible ) { Button("Delete Power metrics?", role: .destructive) { - if clearTelemetry(destNum: node.num, metricsType: 2, context: context) { - Logger.data.notice("Cleared Power Metrics for \(node.num, privacy: .public)") - } else { - Logger.data.error("Clear Power Metrics Log Failed") + Task { + if await MeshPackets.shared.clearTelemetry(destNum: node.num, metricsType: 2) { + Logger.data.notice("Cleared Power Metrics for \(node.num, privacy: .public)") + } else { + Logger.data.error("Clear Power Metrics Log Failed") + } } } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 243325b1..ca228943 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -138,30 +138,31 @@ struct AppSettings: View { Button("Erase all app data?", role: .destructive) { Task { try await accessoryManager.disconnect() - } - /// Delete any database backups too - if var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - url = url.appendingPathComponent("backup").appendingPathComponent(String(UserDefaults.preferredPeripheralNum)) - do { - try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite")) - /// Delete -shm file + + /// Delete any database backups too + if var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + url = url.appendingPathComponent("backup").appendingPathComponent(String(UserDefaults.preferredPeripheralNum)) do { - try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-wal")) + try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite")) + /// Delete -shm file do { - try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-shm")) + try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-wal")) + do { + try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-shm")) + } catch { + Logger.services.error("๐Ÿ—„ Error Deleting Meshtastic.sqlite-shm file \(error, privacy: .public)") + } } catch { - Logger.services.error("๐Ÿ—„ Error Deleting Meshtastic.sqlite-shm file \(error, privacy: .public)") + Logger.services.error("๐Ÿ—„ Error Deleting Meshtastic.sqlite-wal file \(error, privacy: .public)") } } catch { - Logger.services.error("๐Ÿ—„ Error Deleting Meshtastic.sqlite-wal file \(error, privacy: .public)") + Logger.services.error("๐Ÿ—„ Error Deleting Meshtastic.sqlite file \(error, privacy: .public)") } - } catch { - Logger.services.error("๐Ÿ—„ Error Deleting Meshtastic.sqlite file \(error, privacy: .public)") } + await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: true, context: context) + clearNotifications() + context.refreshAllObjects() } - clearCoreDataDatabase(context: context, includeRoutes: true) - clearNotifications() - context.refreshAllObjects() } } Button { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index a03c8a22..6ead6e3c 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -175,7 +175,7 @@ struct DeviceConfig: View { try await accessoryManager.sendNodeDBReset(fromUser: node!.user!, toUser: node!.user!) try await Task.sleep(for: .seconds(1)) try await accessoryManager.disconnect() - clearCoreDataDatabase(context: context, includeRoutes: false) + await MeshPackets.shared.clearCoreDataDatabase(context: context, includeRoutes: false) clearNotifications() } catch { Logger.mesh.error("NodeDB Reset Failed") @@ -200,7 +200,7 @@ struct DeviceConfig: View { try await accessoryManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!) try await Task.sleep(for: .seconds(1)) try await accessoryManager.disconnect() - clearCoreDataDatabase(context: context, includeRoutes: false) + await MeshPackets.shared.clearCoreDataDatabase(context: context, includeRoutes: false) clearNotifications() } catch { Logger.mesh.error("Factory Reset Failed") @@ -213,7 +213,7 @@ struct DeviceConfig: View { try await accessoryManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!, resetDevice: true) try? await Task.sleep(for: .seconds(1)) try await accessoryManager.disconnect() - clearCoreDataDatabase(context: context, includeRoutes: false) + await MeshPackets.shared.clearCoreDataDatabase(context: context, includeRoutes: false) clearNotifications() } catch { Logger.mesh.error("Factory Reset Failed")