mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
* Fix: "Retrieving nodes" significantly slower after reconnect (#1424) The node database retrieval was calling context.save() for every single NodeInfo packet received (250 saves for 250 nodes). This caused severe performance degradation on reconnect when CoreData had accumulated state. Root Cause: - nodeInfoPacket() called context.save() immediately for each node - With 250 nodes, this meant 250 individual CoreData save operations - On first connection, CoreData is fresh and fast - On reconnect, CoreData has accumulated change tracking, undo management, and memory pressure, making each save progressively slower - This resulted in 10+ second retrieval times vs 1-2 seconds initially Solution: - Added deferSave parameter to nodeInfoPacket() function - During database retrieval (.retrievingDatabase state), defer all saves - Perform a single batch save when database retrieval completes (when NONCE_ONLY_DB configCompleteID is received) - This reduces 250 saves to 1 save Performance Impact: - Eliminates N individual saves during node database sync - Reduces database retrieval time back to 1-2 seconds on reconnect - Matches first-connection performance consistently Fixes #1424 * Revert *MessageListUnified files --------- Co-authored-by: Martin Bogomolni <martinbogo@gmail.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
1223 lines
59 KiB
Swift
1223 lines
59 KiB
Swift
//
|
||
// MeshPackets.swift
|
||
// Meshtastic Apple
|
||
//
|
||
// Created by Garth Vander Houwen on 5/27/22.
|
||
//
|
||
|
||
import Foundation
|
||
import CoreData
|
||
import MeshtasticProtobufs
|
||
import SwiftUI
|
||
import RegexBuilder
|
||
import OSLog
|
||
#if canImport(ActivityKit)
|
||
import ActivityKit
|
||
#endif
|
||
|
||
// Simple extension to concisely pass values through a has_XXX boolean check
|
||
fileprivate extension Bool {
|
||
func then<T>(_ value: T) -> T? {
|
||
self ? value : nil
|
||
}
|
||
}
|
||
|
||
func generateMessageMarkdown (message: String) -> String {
|
||
if !message.isEmoji() {
|
||
let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber]
|
||
guard let detector = try? NSDataDetector(types: types.rawValue) else {
|
||
return message
|
||
}
|
||
let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count))
|
||
var messageWithMarkdown = message
|
||
if matches.count > 0 {
|
||
for match in matches {
|
||
guard let range = Range(match.range, in: message) else { continue }
|
||
if match.resultType == .address {
|
||
let address = message[range]
|
||
let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics)
|
||
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))")
|
||
} else if match.resultType == .phoneNumber {
|
||
let phone = messageWithMarkdown[range]
|
||
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))")
|
||
} else if match.resultType == .link {
|
||
let start = match.range.lowerBound
|
||
let stop = match.range.upperBound
|
||
let url = message[start ..< stop]
|
||
let absoluteUrl = match.url?.absoluteString ?? ""
|
||
let markdownUrl = "[\(url)](\(absoluteUrl))"
|
||
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: markdownUrl)
|
||
}
|
||
}
|
||
}
|
||
return messageWithMarkdown
|
||
}
|
||
return message
|
||
}
|
||
|
||
func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) {
|
||
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:
|
||
#if DEBUG
|
||
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:
|
||
#if DEBUG
|
||
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.isClientMuted
|
||
}
|
||
guard let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as? NSMutableOrderedSet else {
|
||
return
|
||
}
|
||
if let oldChannel = mutableChannels.first(where: {($0 as AnyObject).index == newChannel.index }) as? ChannelEntity {
|
||
let index = mutableChannels.index(of: oldChannel as Any)
|
||
mutableChannels.replaceObject(at: index, with: newChannel)
|
||
} else {
|
||
mutableChannels.add(newChannel)
|
||
}
|
||
fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet
|
||
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
|
||
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
|
||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num))
|
||
|
||
do {
|
||
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
|
||
if fetchedMyInfo.count > 0 {
|
||
newNode.myInfo = fetchedMyInfo[0]
|
||
}
|
||
do {
|
||
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 {
|
||
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)")
|
||
}
|
||
return fetchedNode[0]
|
||
} catch {
|
||
context.rollback()
|
||
let nsError = error as NSError
|
||
Logger.data.error("💥 Error Saving Core Data NodeInfoEntity: \(nsError, privacy: .public)")
|
||
}
|
||
} catch {
|
||
Logger.data.error("💥 Fetch MyInfo Error")
|
||
}
|
||
}
|
||
} catch {
|
||
Logger.data.error("💥 Fetch NodeInfoEntity Error")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||
|
||
if let adminMessage = try? AdminMessage(serializedBytes: packet.decoded.payload) {
|
||
|
||
if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) {
|
||
|
||
if let cmmc = try? CannedMessageModuleConfig(serializedBytes: packet.decoded.payload) {
|
||
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)")
|
||
}
|
||
// Save an ack for the admin message log for each admin message response received as we stopped sending acks if there is also a response to reduce airtime.
|
||
adminResponseAck(packet: packet, context: context)
|
||
}
|
||
}
|
||
|
||
func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) {
|
||
|
||
let fetchedAdminMessageRequest = MessageEntity.fetchRequest()
|
||
fetchedAdminMessageRequest.predicate = NSPredicate(format: "messageId == %lld", packet.decoded.requestID)
|
||
do {
|
||
let fetchedMessage = try context.fetch(fetchedAdminMessageRequest)
|
||
if fetchedMessage.count > 0 {
|
||
fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970)
|
||
fetchedMessage[0].ackError = Int32(RoutingError.none.rawValue)
|
||
fetchedMessage[0].receivedACK = true
|
||
fetchedMessage[0].realACK = true
|
||
fetchedMessage[0].ackSNR = packet.rxSnr
|
||
if fetchedMessage[0].fromUser != nil {
|
||
fetchedMessage[0].fromUser?.objectWillChange.send()
|
||
}
|
||
do {
|
||
try context.save()
|
||
} catch {
|
||
Logger.data.error("Failed to save admin message response as an ack: \(error.localizedDescription, 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
|
||
}
|
||
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, 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
|
||
}
|
||
}
|
||
fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue)
|
||
if routingMessage.errorReason == Routing.Error.none {
|
||
fetchedMessage[0].receivedACK = true
|
||
}
|
||
|
||
fetchedMessage[0].ackSNR = packet.rxSnr
|
||
if packet.rxTime > 0 {
|
||
fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime)
|
||
} else {
|
||
fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970)
|
||
}
|
||
|
||
if fetchedMessage[0].toUser != nil {
|
||
fetchedMessage[0].toUser!.objectWillChange.send()
|
||
} else {
|
||
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
|
||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", connectedNodeNum)
|
||
do {
|
||
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
|
||
if fetchedMyInfo.count > 0 {
|
||
|
||
for ch in fetchedMyInfo[0].channels!.array as? [ChannelEntity] ?? [] where ch.index == packet.channel {
|
||
ch.objectWillChange.send()
|
||
}
|
||
}
|
||
} catch { }
|
||
}
|
||
|
||
} else {
|
||
return
|
||
}
|
||
try context.save()
|
||
Logger.data.info("💾 ACK Saved for Message: \(packet.decoded.requestID, 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<MeshActivityAttributes>.activities.first(where: { $0.attributes.nodeNum == connectedNode })
|
||
if meshActivity != nil {
|
||
Task {
|
||
await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration)
|
||
// await meshActivity?.update(updatedContent)
|
||
Logger.services.debug("Updated live activity.")
|
||
}
|
||
}
|
||
#endif
|
||
#endif
|
||
}
|
||
} catch {
|
||
context.rollback()
|
||
let nsError = error as NSError
|
||
Logger.data.error("💥 Error Saving Telemetry for Node \(packet.from, privacy: .public) Error: \(nsError, privacy: .public)")
|
||
}
|
||
} else {
|
||
Logger.data.error("💥 Error Fetching NodeInfoEntity for Node \(packet.from.toHex(), privacy: .public)")
|
||
}
|
||
}
|
||
}
|
||
|
||
func textMessageAppPacket(
|
||
packet: MeshPacket,
|
||
wantRangeTestPackets: Bool,
|
||
critical: Bool = false,
|
||
connectedNode: Int64,
|
||
storeForward: Bool = false,
|
||
context: NSManagedObjectContext,
|
||
appState: AppState?
|
||
) {
|
||
var messageText = String(bytes: packet.decoded.payload, encoding: .utf8)
|
||
let rangeRef = Reference(Int.self)
|
||
let rangeTestRegex = Regex {
|
||
"seq "
|
||
TryCapture(as: rangeRef) {
|
||
OneOrMore(.digit)
|
||
} transform: { match in
|
||
Int(match)
|
||
}
|
||
}
|
||
let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false
|
||
|
||
if !wantRangeTestPackets && rangeTest {
|
||
return
|
||
}
|
||
var storeForwardBroadcast = false
|
||
if storeForward {
|
||
if let storeAndForwardMessage = try? StoreAndForward(serializedBytes: packet.decoded.payload) {
|
||
messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8)
|
||
if storeAndForwardMessage.rr == .routerTextBroadcast {
|
||
storeForwardBroadcast = true
|
||
}
|
||
}
|
||
}
|
||
|
||
if messageText?.count ?? 0 > 0 {
|
||
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)
|
||
}
|
||
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
|
||
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 {
|
||
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
|
||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode))
|
||
do {
|
||
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
|
||
if !fetchedMyInfo.isEmpty {
|
||
appState?.unreadChannelMessages = fetchedMyInfo[0].unreadMessages(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 {
|
||
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))
|
||
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)
|
||
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.")
|
||
}
|
||
}
|