From e287ad6b78b34ef166187f2b6c0a09e684baca8a Mon Sep 17 00:00:00 2001 From: Blake McAnally Date: Thu, 6 Jun 2024 15:09:18 -0500 Subject: [PATCH] Refactor how mesh packets are persisted to the data model --- Meshtastic.xcodeproj/project.pbxproj | 12 + .../CoreData/PositionEntityExtension.swift | 17 +- .../CoreData/UserEntityExtension.swift | 37 ++- .../Protobufs/NodeInfoExtensions.swift | 11 + Meshtastic/Helpers/MeshPackets.swift | 243 +++++++----------- Meshtastic/Persistence/UpdateCoreData.swift | 28 +- 6 files changed, 161 insertions(+), 187 deletions(-) create mode 100644 Meshtastic/Extensions/Protobufs/NodeInfoExtensions.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 40d646aa..3e446152 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 25A978592C124FA70003AAE7 /* NodeInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978572C124FA70003AAE7 /* NodeInfoExtensions.swift */; }; 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; }; 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; }; 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; }; @@ -240,6 +241,7 @@ /* Begin PBXFileReference section */ 258EE1262C0E833D0025A5FB /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 25A978572C124FA70003AAE7 /* NodeInfoExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeInfoExtensions.swift; sourceTree = ""; }; 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = ""; }; 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = ""; }; 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; @@ -513,6 +515,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 25A978582C124FA70003AAE7 /* Protobufs */ = { + isa = PBXGroup; + children = ( + 25A978572C124FA70003AAE7 /* NodeInfoExtensions.swift */, + ); + path = Protobufs; + sourceTree = ""; + }; C9483F6B2773016700998F6B /* MapKitMap */ = { isa = PBXGroup; children = ( @@ -951,6 +961,7 @@ DDDB443E29F79A9400EE2349 /* Extensions */ = { isa = PBXGroup; children = ( + 25A978582C124FA70003AAE7 /* Protobufs */, DD007BB12AA59B9A00F5FA12 /* CoreData */, DDFFA7462B3A7F3C004730DB /* Bundle.swift */, DDDB444529F8A96500EE2349 /* Character.swift */, @@ -1314,6 +1325,7 @@ DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */, + 25A978592C124FA70003AAE7 /* NodeInfoExtensions.swift in Sources */, D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */, DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */, DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */, diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 0c634779..37261ed6 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -11,7 +11,22 @@ import MapKit import SwiftUI extension PositionEntity { - + convenience init( + context: NSManagedObjectContext, + nodeInfo: NodeInfo + ) { + self.init(context: context) + self.latest = true + self.seqNo = Int32(nodeInfo.position.seqNumber) + self.latitudeI = nodeInfo.position.latitudeI + self.longitudeI = nodeInfo.position.longitudeI + self.altitude = nodeInfo.position.altitude + self.satsInView = Int32(nodeInfo.position.satsInView) + self.speed = Int32(nodeInfo.position.groundSpeed) + self.heading = Int32(nodeInfo.position.groundTrack) + self.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) + } + static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() request.fetchLimit = 1000 diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index a8adeaaf..886f90ca 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -9,6 +9,31 @@ import Foundation import CoreData extension UserEntity { + convenience init( + context: NSManagedObjectContext, + user: User, + num: Int + ) { + self.init(context: context) + self.userId = user.id + self.num = Int64(num) + self.longName = user.longName + self.shortName = user.shortName + self.hwModel = String(describing: user.hwModel).uppercased() + self.isLicensed = user.isLicensed + self.role = Int32(user.role.rawValue) + } + + convenience init(context: NSManagedObjectContext, num: Int) { + self.init(context: context) + self.num = Int64(num) + let userId = String(format: "!%2X", num) + self.userId = userId + let last4 = String(userId.suffix(4)) + self.longName = "Meshtastic \(last4)" + self.shortName = last4 + self.hwModel = "UNSET" + } var messageList: [MessageEntity] { self.value(forKey: "allMessages") as? [MessageEntity] ?? [MessageEntity]() @@ -27,15 +52,3 @@ extension UserEntity { return unreadMessages.count } } - -public func createUser(num: Int64, context: NSManagedObjectContext) -> UserEntity { - let newUser = UserEntity(context: context) - newUser.num = Int64(num) - let userId = String(format: "%2X", num) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" - return newUser -} diff --git a/Meshtastic/Extensions/Protobufs/NodeInfoExtensions.swift b/Meshtastic/Extensions/Protobufs/NodeInfoExtensions.swift new file mode 100644 index 00000000..ff20c78a --- /dev/null +++ b/Meshtastic/Extensions/Protobufs/NodeInfoExtensions.swift @@ -0,0 +1,11 @@ +import Foundation + +extension NodeInfo { + var isValidPosition: Bool { + hasPosition && + position.longitudeI != 0 && + position.latitudeI != 0 && + position.latitudeI != 373346000 && + position.longitudeI != -1220090000 + } +} diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 18748059..5611ead7 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -251,182 +251,111 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? { - let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, String(nodeInfo.num)) + let logString = String.localizedStringWithFormat( + "mesh.log.nodeinfo.received %@ %@".localized, + String(nodeInfo.num), + String(nodeInfo.viaMqtt) + ) MeshLogger.log("📟 \(logString)") - guard nodeInfo.num > 0 else { return nil } + guard nodeInfo.num > 0 else { + Logger.data.error("nodeInfo \(nodeInfo.num) invalid") + return nil + } let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeInfo.num)) do { - guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { + guard let fetchedNodes = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { return nil } - // 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.hopsAway = Int32(nodeInfo.hopsAway) - newNode.viaMqtt = nodeInfo.viaMqtt - - 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) + let node: NodeInfoEntity + if let update = fetchedNodes.first { + node = update + } else { + node = NodeInfoEntity(context: context) + } + + node.id = Int64(nodeInfo.num) + node.num = Int64(nodeInfo.num) + node.channel = Int32(nodeInfo.channel) + node.favorite = nodeInfo.isFavorite + node.hopsAway = Int32(nodeInfo.hopsAway) + node.viaMqtt = nodeInfo.viaMqtt + + 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 + + var telemetries: [TelemetryEntity] + if let tele = node.telemetries?.array as? [TelemetryEntity] { + telemetries = tele + telemetries.append(newTelemetry) + } else { + telemetries = [newTelemetry] } - - newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - newNode.snr = nodeInfo.snr - if nodeInfo.hasUser { - - let newUser = UserEntity(context: context) - newUser.userId = nodeInfo.user.id - newUser.num = Int64(nodeInfo.num) - newUser.longName = nodeInfo.user.longName - newUser.shortName = nodeInfo.user.shortName - newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() - newUser.isLicensed = nodeInfo.user.isLicensed - newUser.role = Int32(nodeInfo.user.role.rawValue) - newNode.user = newUser - } else if nodeInfo.num > Int16.max { - let newUser = createUser(num: Int64(nodeInfo.num), context: context) - newNode.user = newUser + node.telemetries = NSOrderedSet(array: telemetries) + } + + + node.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + node.snr = nodeInfo.snr + + // User + var user: UserEntity? + if nodeInfo.hasUser { + user = UserEntity( + context: context, + user: nodeInfo.user, + num: Int(nodeInfo.num) + ) + } else if nodeInfo.num > Int16.max { + user = UserEntity( + context: context, + num: Int(nodeInfo.num) + ) + } + node.user = user + + // Position + if nodeInfo.isValidPosition { + let position = PositionEntity( + context: context, + nodeInfo: nodeInfo + ) + + if let positions = node.positions?.mutableCopy() as? NSMutableOrderedSet { + positions.add(position) + node.positions = positions + } else { + node.positions = NSOrderedSet(object: position) } + } - 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: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) - - do { - guard let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as? [MyInfoEntity] else { - return nil - } - if fetchedMyInfo.count > 0 { - newNode.myInfo = fetchedMyInfo[0] - } + // MyInfo + do { + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate( + format: "myNodeNum == %lld", Int64(nodeInfo.num) + ) + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if let myInfo = fetchedMyInfo.first { + node.myInfo = myInfo do { try context.save() Logger.data.info("💾 Saved a new Node Info For: \(String(nodeInfo.num))") - return newNode + return node } catch { context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError)") - } - } catch { - Logger.data.error("Fetch MyInfo Error") - } - } else if nodeInfo.num > 0 { - - fetchedNode[0].id = Int64(nodeInfo.num) - fetchedNode[0].num = Int64(nodeInfo.num) - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - fetchedNode[0].snr = nodeInfo.snr - fetchedNode[0].channel = Int32(nodeInfo.channel) - fetchedNode[0].favorite = nodeInfo.isFavorite - fetchedNode[0].hopsAway = Int32(nodeInfo.hopsAway) - fetchedNode[0].viaMqtt = nodeInfo.viaMqtt - - if nodeInfo.hasUser { - if fetchedNode[0].user == nil { - fetchedNode[0].user = UserEntity(context: context) - } - fetchedNode[0].user!.userId = nodeInfo.user.id - fetchedNode[0].user!.num = Int64(nodeInfo.num) - fetchedNode[0].user!.numString = String(nodeInfo.num) - fetchedNode[0].user!.longName = nodeInfo.user.longName - fetchedNode[0].user!.shortName = nodeInfo.user.shortName - fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed - fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue) - fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() - } else { - if fetchedNode[0].user == nil && nodeInfo.num > Int16.max { - - let newUser = createUser(num: Int64(nodeInfo.num), context: context) - fetchedNode[0].user = newUser + Logger.data.error("Error Saving Core Data NodeInfoEntity: \(error.localizedDescription)") } } - - if nodeInfo.hasDeviceMetrics { - - let newTelemetry = TelemetryEntity(context: context) - newTelemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) - newTelemetry.voltage = nodeInfo.deviceMetrics.voltage - newTelemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization - newTelemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx - guard let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as? NSMutableOrderedSet else { - return nil - } - fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet - } - - if nodeInfo.hasPosition { - - if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { - - let position = PositionEntity(context: context) - position.latitudeI = nodeInfo.position.latitudeI - position.longitudeI = nodeInfo.position.longitudeI - position.altitude = nodeInfo.position.altitude - position.satsInView = Int32(nodeInfo.position.satsInView) - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) - guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else { - return nil - } - fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet - } - - } - - // Look for a MyInfo - let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) - - do { - guard let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as? [MyInfoEntity] else { - return nil - } - if fetchedMyInfo.count > 0 { - fetchedNode[0].myInfo = fetchedMyInfo[0] - } - do { - try context.save() - Logger.data.info("💾 NodeInfo saved for \(nodeInfo.num)") - return fetchedNode[0] - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError)") - } - } catch { - Logger.data.error("Fetch MyInfo Error") - } + } catch { + Logger.data.error("Fetch MyInfo Error: \(error.localizedDescription)") } } catch { Logger.data.error("Fetch NodeInfoEntity Error") diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 1ef04ff7..9472fd05 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -166,20 +166,14 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) if let newUserMessage = try? User(serializedData: packet.decoded.payload) { - if newUserMessage.id.isEmpty { - if packet.from > Int16.max { - let newUser = createUser(num: Int64(packet.from), context: context) - newNode.user = newUser - } + if newUserMessage.id.isEmpty, packet.from > Int16.max { + newNode.user = UserEntity(context: context, num: Int(packet.from)) } else { - - let newUser = UserEntity(context: context) - newUser.userId = newUserMessage.id - 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() + let newUser = UserEntity( + context: context, + user: newUserMessage, + num: Int(packet.from) + ) newNode.user = newUser if UserDefaults.newNodeNotifications { @@ -199,13 +193,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } } else { if packet.from > Int16.max { - let newUser = createUser(num: Int64(packet.from), context: context) + let newUser = UserEntity(context: context, num: Int(packet.from)) fetchedNode[0].user = newUser } } if newNode.user == nil && packet.from > Int16.max { - newNode.user = createUser(num: Int64(packet.from), context: context) + newNode.user = UserEntity(context: context, num: Int(packet.from)) } let myInfoEntity = MyInfoEntity(context: context) @@ -265,8 +259,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) } if fetchedNode[0].user == nil { - let newUser = createUser(num: Int64(packet.from), context: context) - fetchedNode[0].user! = newUser + let newUser = UserEntity(context: context, num: Int(packet.from)) + fetchedNode[0].user = newUser } do { try context.save()