diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 815a8db1..e9803faf 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -7,6 +7,19 @@ objects = { /* Begin PBXBuildFile section */ + 48E682ECE71246C3BFBD6B8F /* ChannelEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E6EDE744E3446FA1084499 /* ChannelEntity.swift */; }; + 898C6C4E60B640B3A285AF25 /* ConfigModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B88B4AE298C43D1B8F4516C /* ConfigModels.swift */; }; + B96123C9177242AFA768AA8F /* DeviceMetadataEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509A1C42A695463093654617 /* DeviceMetadataEntity.swift */; }; + 10D80CF7AD2B4269BE0A4933 /* MeshtasticSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DF7D72AA0A450399DBC592 /* MeshtasticSchema.swift */; }; + D75A5D73360C42D6B0CB2894 /* MessageEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3204EF969484E549C139730 /* MessageEntity.swift */; }; + 85D0F28AE7CE44C39B6BEBF8 /* MyInfoEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5970BC1CAF4B66999C7248 /* MyInfoEntity.swift */; }; + 9C744FC4C34549008EADCA4E /* NodeInfoEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7101240749F24E96835A040A /* NodeInfoEntity.swift */; }; + D02589957BD040969FCB8663 /* PositionEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97241903A924144A3EEB679 /* PositionEntity.swift */; }; + 93FFCD83202042FB85B47CFD /* RouteModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB55D64F064C45ADBD31ABF8 /* RouteModels.swift */; }; + 5D3AFC6F08ED4C909C830C55 /* TelemetryEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31483AF5F5354D3481698E32 /* TelemetryEntity.swift */; }; + 7B1684B5E1AF4137829C03B8 /* TraceRouteModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D66A4063FE4E868CD30D3C /* TraceRouteModels.swift */; }; + 70A5B362F7EC4F14A99A8C39 /* UserEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918722D2C1474B2D99ED01DC /* UserEntity.swift */; }; + E340A7CA75194A49AB8763C7 /* WaypointEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98797BFD67F449B9ACCEDA7 /* WaypointEntity.swift */; }; 102B5EAB2E172F41003D191E /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAA2E172F41003D191E /* DatadogCore */; }; 102B5EAD2E172F41003D191E /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAC2E172F41003D191E /* DatadogCrashReporting */; }; 102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAE2E172F41003D191E /* DatadogLogs */; }; @@ -37,8 +50,6 @@ 233E99C72D84A70900CC3A77 /* SoilCompactWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99C62D84A70900CC3A77 /* SoilCompactWidgets.swift */; }; 233E99CB2D85AAA900CC3A77 /* RainfallCompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99CA2D85AAA900CC3A77 /* RainfallCompactWidget.swift */; }; 2344A2AB2D66974300170A77 /* ManagedAttributePropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */; }; - 2344A2AF2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2344A2AD2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift */; }; - 2344A2B02D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2344A2AE2D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift */; }; 2344A2B12D68DFF800170A77 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C49D8F2C471AEA0024FBD1 /* Constants.swift */; }; 2346A7192E2FB9A300CB9239 /* SerialConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2346A7182E2FB9A300CB9239 /* SerialConnection.swift */; }; 2346A71D2E2FB9C500CB9239 /* SerialTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2346A71C2E2FB9C500CB9239 /* SerialTransport.swift */; }; @@ -349,6 +360,19 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 45E6EDE744E3446FA1084499 /* ChannelEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelEntity.swift; sourceTree = ""; }; + 4B88B4AE298C43D1B8F4516C /* ConfigModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigModels.swift; sourceTree = ""; }; + 509A1C42A695463093654617 /* DeviceMetadataEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetadataEntity.swift; sourceTree = ""; }; + 38DF7D72AA0A450399DBC592 /* MeshtasticSchema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticSchema.swift; sourceTree = ""; }; + F3204EF969484E549C139730 /* MessageEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntity.swift; sourceTree = ""; }; + DC5970BC1CAF4B66999C7248 /* MyInfoEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntity.swift; sourceTree = ""; }; + 7101240749F24E96835A040A /* NodeInfoEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntity.swift; sourceTree = ""; }; + A97241903A924144A3EEB679 /* PositionEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntity.swift; sourceTree = ""; }; + DB55D64F064C45ADBD31ABF8 /* RouteModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteModels.swift; sourceTree = ""; }; + 31483AF5F5354D3481698E32 /* TelemetryEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryEntity.swift; sourceTree = ""; }; + D8D66A4063FE4E868CD30D3C /* TraceRouteModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteModels.swift; sourceTree = ""; }; + 918722D2C1474B2D99ED01DC /* UserEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntity.swift; sourceTree = ""; }; + E98797BFD67F449B9ACCEDA7 /* WaypointEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntity.swift; sourceTree = ""; }; 01028778B8BFD81F7A039593 /* TAKConnection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKConnection.swift; sourceTree = ""; }; 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerConfig.swift; sourceTree = ""; }; 09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKCertificateManager.swift; sourceTree = ""; }; @@ -1277,6 +1301,19 @@ DDC2E18826CE24EE0042C5E4 /* Model */ = { isa = PBXGroup; children = ( + 45E6EDE744E3446FA1084499 /* ChannelEntity.swift */, + 4B88B4AE298C43D1B8F4516C /* ConfigModels.swift */, + 509A1C42A695463093654617 /* DeviceMetadataEntity.swift */, + 38DF7D72AA0A450399DBC592 /* MeshtasticSchema.swift */, + F3204EF969484E549C139730 /* MessageEntity.swift */, + DC5970BC1CAF4B66999C7248 /* MyInfoEntity.swift */, + 7101240749F24E96835A040A /* NodeInfoEntity.swift */, + A97241903A924144A3EEB679 /* PositionEntity.swift */, + DB55D64F064C45ADBD31ABF8 /* RouteModels.swift */, + 31483AF5F5354D3481698E32 /* TelemetryEntity.swift */, + D8D66A4063FE4E868CD30D3C /* TraceRouteModels.swift */, + 918722D2C1474B2D99ED01DC /* UserEntity.swift */, + E98797BFD67F449B9ACCEDA7 /* WaypointEntity.swift */, 2344A2AC2D66978000170A77 /* CoreData */, 231B3F1E2D0879BC0069A07D /* Metrics Visualization */, DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */, @@ -1680,6 +1717,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 48E682ECE71246C3BFBD6B8F /* ChannelEntity.swift in Sources */, + 898C6C4E60B640B3A285AF25 /* ConfigModels.swift in Sources */, + B96123C9177242AFA768AA8F /* DeviceMetadataEntity.swift in Sources */, + 10D80CF7AD2B4269BE0A4933 /* MeshtasticSchema.swift in Sources */, + D75A5D73360C42D6B0CB2894 /* MessageEntity.swift in Sources */, + 85D0F28AE7CE44C39B6BEBF8 /* MyInfoEntity.swift in Sources */, + 9C744FC4C34549008EADCA4E /* NodeInfoEntity.swift in Sources */, + D02589957BD040969FCB8663 /* PositionEntity.swift in Sources */, + 93FFCD83202042FB85B47CFD /* RouteModels.swift in Sources */, + 5D3AFC6F08ED4C909C830C55 /* TelemetryEntity.swift in Sources */, + 7B1684B5E1AF4137829C03B8 /* TraceRouteModels.swift in Sources */, + 70A5B362F7EC4F14A99A8C39 /* UserEntity.swift in Sources */, + E340A7CA75194A49AB8763C7 /* WaypointEntity.swift in Sources */, 230BC3972E31071E0046BF2A /* AccessoryManager+Discovery.swift in Sources */, 25F26B1F2C2F611300C9CD9D /* AppData.swift in Sources */, 25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */, @@ -1916,7 +1966,6 @@ DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, 2346A71D2E2FB9C500CB9239 /* SerialTransport.swift in Sources */, - DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */, 2344A2AF2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift in Sources */, diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift index 46d4f767..a8862494 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift @@ -9,6 +9,7 @@ import Foundation import MeshtasticProtobufs import CocoaMQTT import OSLog +import SwiftData extension AccessoryManager { @@ -76,7 +77,7 @@ extension AccessoryManager { updateDevice(key: \.num, value: Int64(myNodeInfo.myNodeNum)) if let myInfoId = await MeshPackets.shared.myInfoPacket(myInfo: myNodeInfo, peripheralId: connectedDeviceId), - let myInfo = try? context.existingObject(with: myInfoId) as? MyInfoEntity { + let myInfo = try? context.model(for: myInfoId) as? MyInfoEntity { if let bleName = myInfo.bleName { updateDevice(key: \.name, value: bleName) updateDevice(key: \.longName, value: bleName) @@ -116,7 +117,7 @@ extension AccessoryManager { // TODO: nodeInfoPacket's channel: parameter is not used // 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 { + let nodeInfo = try? context.model(for: 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 ?? "?") @@ -201,6 +202,7 @@ extension AccessoryManager { updateDevice(key: \.firmwareVersion, value: metadata.firmwareVersion) await MeshPackets.shared.deviceMetadataPacket(metadata: metadata, fromNum: deviceNum) + Logger.transport.info("āœ… [handleDeviceMetadata] deviceMetadataPacket completed for \(deviceNum.toHex(), privacy: .public)") } internal func tryClearExistingChannels() { @@ -210,15 +212,13 @@ extension AccessoryManager { } // Before we get started delete the existing channels from the myNodeInfo - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(deviceNum)) + let num = Int64(deviceNum) + let fetchMyInfoRequest = FetchDescriptor(predicate: #Predicate { $0.myNodeNum == num }) do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) if fetchedMyInfo.count == 1 { - let mutableChannels = fetchedMyInfo[0].channels?.mutableCopy() as? NSMutableOrderedSet - mutableChannels?.removeAllObjects() - fetchedMyInfo[0].channels = mutableChannels + fetchedMyInfo[0].channels.removeAll() do { try context.save() } catch { @@ -267,10 +267,11 @@ extension AccessoryManager { routerNode.storeForwardConfig?.isRouter = storeAndForwardMessage.heartbeat.secondary == 0 routerNode.storeForwardConfig?.lastHeartbeat = Date() } else { - let newConfig = StoreForwardConfigEntity(context: context) + let newConfig = StoreForwardConfigEntity() newConfig.enabled = true newConfig.isRouter = storeAndForwardMessage.heartbeat.secondary == 0 newConfig.lastHeartbeat = Date() + context.insert(newConfig) routerNode.storeForwardConfig = newConfig } @@ -296,8 +297,9 @@ extension AccessoryManager { if routerNode.storeForwardConfig != nil { routerNode.storeForwardConfig?.lastRequest = Int32(storeAndForwardMessage.history.lastRequest) } else { - let newConfig = StoreForwardConfigEntity(context: context) + let newConfig = StoreForwardConfigEntity() newConfig.lastRequest = Int32(storeAndForwardMessage.history.lastRequest) + context.insert(newConfig) routerNode.storeForwardConfig = newConfig } @@ -363,13 +365,14 @@ extension AccessoryManager { return } var hopNodes: [TraceRouteHopEntity] = [] - let connectedHop = TraceRouteHopEntity(context: context) + let connectedHop = TraceRouteHopEntity() + context.insert(connectedHop) connectedHop.time = Date() connectedHop.num = deviceNum connectedHop.name = connectedNode.user?.longName ?? "???" // If nil, set to unknown, INT8_MIN (-128) then divide by 4 connectedHop.snr = Float(routingMessage.snrBack.last ?? -128) / 4 - if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + if let mostRecent = traceRoute?.node?.positions.last, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { connectedHop.altitude = mostRecent.altitude connectedHop.latitudeI = mostRecent.latitudeI connectedHop.longitudeI = mostRecent.longitudeI @@ -383,7 +386,8 @@ extension AccessoryManager { if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { hopNode = createNodeInfo(num: Int64(node), context: context) } - let traceRouteHop = TraceRouteHopEntity(context: context) + let traceRouteHop = TraceRouteHopEntity() + context.insert(traceRouteHop) traceRouteHop.time = Date() if routingMessage.snrTowards.count >= index + 1 { traceRouteHop.snr = Float(routingMessage.snrTowards[index]) / 4 @@ -392,7 +396,7 @@ extension AccessoryManager { traceRouteHop.snr = -32 } if let hn = hopNode, hn.hasPositions { - if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + if let mostRecent = hn.positions.last, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI @@ -412,13 +416,14 @@ extension AccessoryManager { let snrLabel = (traceRouteHop.snr != -32) ? String(traceRouteHop.snr) : "unknown ".localized routeString += "\(hopName) \(mqttLabel)(\(snrLabel)dB) --> " } - let destinationHop = TraceRouteHopEntity(context: context) + let destinationHop = TraceRouteHopEntity() + context.insert(destinationHop) destinationHop.name = traceRoute?.node?.user?.longName ?? "Unknown".localized destinationHop.time = Date() // If nil, set to unknown, INT8_MIN (-128) then divide by 4 destinationHop.snr = Float(routingMessage.snrTowards.last ?? -128) / 4 destinationHop.num = traceRoute?.node?.num ?? 0 - if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + if let mostRecent = traceRoute?.node?.positions.last, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { destinationHop.altitude = mostRecent.altitude destinationHop.latitudeI = mostRecent.latitudeI destinationHop.longitudeI = mostRecent.longitudeI @@ -439,7 +444,8 @@ extension AccessoryManager { if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { hopNode = createNodeInfo(num: Int64(node), context: context) } - let traceRouteHop = TraceRouteHopEntity(context: context) + let traceRouteHop = TraceRouteHopEntity() + context.insert(traceRouteHop) traceRouteHop.time = Date() traceRouteHop.back = true if routingMessage.snrBack.count >= index + 1 { @@ -449,7 +455,7 @@ extension AccessoryManager { traceRouteHop.snr = -32 } if let hn = hopNode, hn.hasPositions { - if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + if let mostRecent = hn.positions.last, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI @@ -474,7 +480,7 @@ extension AccessoryManager { routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex())) (\(snrBackLast != -32 ? String(snrBackLast) : "unknown ".localized)dB)" traceRoute?.routeBackText = routeBackString } - traceRoute?.hops = NSOrderedSet(array: hopNodes) + traceRoute?.hops = hopNodes traceRoute?.time = Date() if let tr = traceRoute { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift index 4da8c739..5f37daa0 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+MQTT.swift @@ -6,6 +6,7 @@ // import Foundation +import SwiftData import CocoaMQTT import OSLog import MeshtasticProtobufs @@ -18,10 +19,12 @@ extension AccessoryManager { return } - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(deviceNum)) + let nodeNum = Int64(deviceNum) + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == nodeNum } + ) do { - let fetchedNodeInfo = try context.fetch(fetchNodeInfoRequest) + let fetchedNodeInfo = try context.fetch(descriptor) if fetchedNodeInfo.count == 1 { // Subscribe to Mqtt Client Proxy if enabled if fetchedNodeInfo[0].mqttConfig != nil && fetchedNodeInfo[0].mqttConfig?.enabled ?? false && fetchedNodeInfo[0].mqttConfig?.proxyToClientEnabled ?? false { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 29870d16..8958bc5b 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -8,6 +8,7 @@ import Foundation import MeshtasticProtobufs import OSLog +import SwiftData extension AccessoryManager { @@ -280,8 +281,7 @@ extension AccessoryManager { return } - let messageUsers = UserEntity.fetchRequest() - messageUsers.predicate = NSPredicate(format: "num IN %@", [fromUserNum, Int64(toUserNum)]) + let messageUsers = FetchDescriptor(predicate: #Predicate { $0.num == fromUserNum || $0.num == toUserNum }) do { let fetchedUsers = try context.fetch(messageUsers) @@ -290,7 +290,8 @@ extension AccessoryManager { Logger.data.error("🚫 Message Users Not Found, Fail") throw AccessoryError.ioFailed("🚫 Message Users Not Found, Fail") } else if fetchedUsers.count >= 1 { - let newMessage = MessageEntity(context: context) + let newMessage = MessageEntity() + context.insert(newMessage) newMessage.messageId = Int64(UInt32.random(in: UInt32(UInt8.max)..(predicate: #Predicate { $0.myNodeNum == deviceNum }) let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) if fetchedMyInfo.count != 1 { @@ -464,7 +463,7 @@ extension AccessoryManager { // We are trying to add a channel so lets get the last index myInfo = fetchedMyInfo[0] - i = Int32(myInfo.channels?.count ?? -1) + i = Int32(myInfo.channels.count) // Bail out if the index is negative or bigger than our max of 8 if i < 0 || i > 8 { @@ -475,12 +474,8 @@ extension AccessoryManager { for cs in channelSet.settings { if addChannels { - guard let mutableChannels = myInfo.channels?.mutableCopy() as? NSMutableOrderedSet else { - throw AccessoryError.appError("No channels or channel") - } - - // Bail out if there are no channels or if the same channel name already exists - if mutableChannels.first(where: { ($0 as AnyObject).name == cs.name }) is ChannelEntity { + // Bail out if the same channel name already exists + if myInfo.channels.first(where: { $0.name == cs.name }) != nil { throw AccessoryError.appError("Channel already exists") } } @@ -616,9 +611,9 @@ extension AccessoryManager { wayPointEntity.expire = nil } if waypoint.lockedTo > 0 { - wayPointEntity.locked = Int64(waypoint.lockedTo) + wayPointEntity.locked = true } else { - wayPointEntity.locked = 0 + wayPointEntity.locked = false } if wayPointEntity.created == nil { wayPointEntity.created = Date() @@ -664,14 +659,10 @@ extension AccessoryManager { let logString = String.localizedStringWithFormat("Sent a TraceRoute Packet from: %@ to: %@".localized, String(fromNodeNum), String(destNum)) try await send(toRadio, debugDescription: logString) - let traceRoute = TraceRouteEntity(context: context) - let nodes = NodeInfoEntity.fetchRequest() + let traceRoute = TraceRouteEntity() + context.insert(traceRoute) // TODO: Not sure what's going on here. We always have a fromNodeNum - // if let connectedNum = fromNodeNum { - nodes.predicate = NSPredicate(format: "num IN %@", [destNum, fromNodeNum]) - // } else { - // nodes.predicate = NSPredicate(format: "num == %@", destNum) - // } + let nodes = FetchDescriptor(predicate: #Predicate { $0.num == destNum || $0.num == fromNodeNum }) do { let fetchedNodes = try context.fetch(nodes) let receivingNode = fetchedNodes.first(where: { $0.num == destNum }) diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index caa56ed4..04dda76b 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -122,7 +122,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { // Chicken/Egg problem. Set in the App object immediately after // AppState and AccessoryManager are created var appState: AppState! - let context = PersistenceController.shared.container.viewContext + let context = PersistenceController.shared.context let mqttManager = MqttClientProxyManager.shared // Published Stuff @@ -489,6 +489,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { } private func processFromRadio(_ decodedInfo: FromRadio) async { + Logger.transport.info("šŸ“» [processFromRadio] Processing: \(String(describing: decodedInfo.payloadVariant), privacy: .public)") switch decodedInfo.payloadVariant { case .mqttClientProxyMessage(let mqttClientProxyMessage): handleMqttClientProxyMessage(mqttClientProxyMessage) diff --git a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift index 8e3bbfba..474bae81 100644 --- a/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift +++ b/Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLETransport.swift @@ -6,6 +6,7 @@ // import Foundation +import SwiftData @preconcurrency import CoreBluetooth import SwiftUI import OSLog @@ -348,15 +349,19 @@ actor BLETransport: Transport { // Get the device name if let nodeNum { - let fetchMyInfoRequest = NodeInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + let nodeNumVal = Int64(nodeNum) + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == nodeNumVal } + ) do { - let fetchedMyInfo = try PersistenceController.shared.container.viewContext.fetch(fetchMyInfoRequest) - if fetchedMyInfo.count > 0 { - if let longName = fetchedMyInfo[0].user?.longName { + let container = PersistenceController.shared.container + let bgContext = ModelContext(container) + let fetchedNodes = try bgContext.fetch(descriptor) + if let first = fetchedNodes.first { + if let longName = first.user?.longName { device.longName = longName } - if let shortName = fetchedMyInfo[0].user?.shortName { + if let shortName = first.user?.shortName { device.shortName = shortName } } diff --git a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift index 870002cc..4bf88b92 100644 --- a/Meshtastic/AppIntents/FactoryResetNodeIntent.swift +++ b/Meshtastic/AppIntents/FactoryResetNodeIntent.swift @@ -29,8 +29,9 @@ struct FactoryResetNodeIntent: AppIntent { } // Safely unwrap the connected node information + let context = await MainActor.run { PersistenceController.shared.context } if let connectedPeripheralNum = await AccessoryManager.shared.activeDeviceNum, - let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), + let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: context), let fromUser = connectedNode.user, let toUser = connectedNode.user { diff --git a/Meshtastic/AppIntents/NavigateToNodeIntent.swift b/Meshtastic/AppIntents/NavigateToNodeIntent.swift index 9559796c..e6d955b0 100644 --- a/Meshtastic/AppIntents/NavigateToNodeIntent.swift +++ b/Meshtastic/AppIntents/NavigateToNodeIntent.swift @@ -8,7 +8,7 @@ import Foundation import AppIntents import CoreLocation -import CoreData +import SwiftData import UIKit @available(iOS 16.4, *) @@ -26,12 +26,15 @@ struct NavigateToNodeIntent: ForegroundContinuableIntent { throw AppIntentErrors.AppIntentError.notConnected } - let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + let nodeNumInt64 = Int64(nodeNum) + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == nodeNumInt64 } + ) + descriptor.fetchLimit = 1 do { - guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], - fetchedNode.count == 1 else { + let fetchedNode = try await MainActor.run { try PersistenceController.shared.context.fetch(descriptor) } + guard fetchedNode.count == 1 else { throw $nodeNum.needsValueError("Could not find node") } diff --git a/Meshtastic/AppIntents/NodePositionIntent.swift b/Meshtastic/AppIntents/NodePositionIntent.swift index e942db04..f6ffbbea 100644 --- a/Meshtastic/AppIntents/NodePositionIntent.swift +++ b/Meshtastic/AppIntents/NodePositionIntent.swift @@ -8,7 +8,7 @@ import Foundation import AppIntents import CoreLocation -import CoreData +import SwiftData struct NodePositionIntent: AppIntent { @@ -22,16 +22,19 @@ struct NodePositionIntent: AppIntent { if !(await AccessoryManager.shared.isConnected) { throw AppIntentErrors.AppIntentError.notConnected } - let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + let nodeNumInt64 = Int64(nodeNum) + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == nodeNumInt64 } + ) + descriptor.fetchLimit = 1 do { - guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], fetchedNode.count == 1 else { + let fetchedNode = try await MainActor.run { try PersistenceController.shared.context.fetch(descriptor) } + guard fetchedNode.count == 1 else { throw $nodeNum.needsValueError("Could not find node") } let nodeInfo = fetchedNode[0] - if let latitude = nodeInfo.latestPosition?.coordinate.latitude, - let longitude = nodeInfo.latestPosition?.coordinate.longitude { - let nodeLocation = CLLocation(latitude: latitude, longitude: longitude) + if let coord = nodeInfo.latestPosition?.nodeCoordinate { + let nodeLocation = CLLocation(latitude: coord.latitude, longitude: coord.longitude) // Reverse geocode the CLLocation to get a CLPlacemark let geocoder = CLGeocoder() let placemarks = try await geocoder.reverseGeocodeLocation(nodeLocation) diff --git a/Meshtastic/AppIntents/RestartNodeIntent.swift b/Meshtastic/AppIntents/RestartNodeIntent.swift index 8e325480..9caa7000 100644 --- a/Meshtastic/AppIntents/RestartNodeIntent.swift +++ b/Meshtastic/AppIntents/RestartNodeIntent.swift @@ -19,8 +19,9 @@ struct RestartNodeIntent: AppIntent { throw AppIntentErrors.AppIntentError.notConnected } // Safely unwrap the connectedNode using if let + let context = await MainActor.run { PersistenceController.shared.context } if let connectedPeripheralNum = await AccessoryManager.shared.activeDeviceNum, - let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), + let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: context), let fromUser = connectedNode.user, let toUser = connectedNode.user { diff --git a/Meshtastic/AppIntents/ShutDownNodeIntent.swift b/Meshtastic/AppIntents/ShutDownNodeIntent.swift index 594c1f43..686dd73d 100644 --- a/Meshtastic/AppIntents/ShutDownNodeIntent.swift +++ b/Meshtastic/AppIntents/ShutDownNodeIntent.swift @@ -21,8 +21,9 @@ struct ShutDownNodeIntent: AppIntent { } // Safely unwrap the connectedNode using if let + let context = await MainActor.run { PersistenceController.shared.context } if let connectedPeripheralNum = await AccessoryManager.shared.activeDeviceNum, - let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext), + let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: context), let fromUser = connectedNode.user, let toUser = connectedNode.user { diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 8fce761c..3a841cbc 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -5,48 +5,50 @@ // Copyright(c) Garth Vander Houwen 11/7/22. // import Foundation -import CoreData +import SwiftData import MeshtasticProtobufs extension ChannelEntity { - var messagePredicate: NSPredicate { - return NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", self.index) - } - - var messageFetchRequest: NSFetchRequest { - let fetchRequest = MessageEntity.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] - fetchRequest.predicate = messagePredicate - return fetchRequest - } - + @MainActor var allPrivateMessages: [MessageEntity] { - let context = PersistenceController.shared.container.viewContext - let fetchRequest = messageFetchRequest - - return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() + let context = PersistenceController.shared.context + let channelIndex = self.index + var descriptor = FetchDescriptor( + predicate: #Predicate { msg in + msg.channel == channelIndex && msg.toUser == nil && msg.isEmoji == false + }, + sortBy: [SortDescriptor(\.messageTimestamp, order: .forward)] + ) + return (try? context.fetch(descriptor)) ?? [] } + @MainActor var mostRecentPrivateMessage: MessageEntity? { - // Most recent channel message (descending, limit 1) - let context = PersistenceController.shared.container.viewContext - let fetchRequest = messageFetchRequest - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: false)] - fetchRequest.fetchLimit = 1 - - return (try? context.fetch(fetchRequest))?.first + let context = PersistenceController.shared.context + let channelIndex = self.index + var descriptor = FetchDescriptor( + predicate: #Predicate { msg in + msg.channel == channelIndex && msg.toUser == nil && msg.isEmoji == false + }, + sortBy: [SortDescriptor(\.messageTimestamp, order: .reverse)] + ) + descriptor.fetchLimit = 1 + return try? context.fetch(descriptor).first } - func unreadMessages(context: NSManagedObjectContext) -> Int { - let fetchRequest = messageFetchRequest - fetchRequest.sortDescriptors = [] // sort is irrelevant. - fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) - - return (try? context.count(for: fetchRequest)) ?? 0 + @MainActor + func unreadMessages(context: ModelContext) -> Int { + let channelIndex = self.index + let descriptor = FetchDescriptor( + predicate: #Predicate { msg in + msg.channel == channelIndex && msg.toUser == nil && msg.isEmoji == false && msg.read == false + } + ) + return (try? context.fetchCount(descriptor)) ?? 0 } - // Backwards-compatible property (uses viewContext) - var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.container.viewContext) } + @MainActor + var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.context) } var protoBuf: Channel { var channel = Channel() diff --git a/Meshtastic/Extensions/CoreData/DeviceMetadataEntityExtension.swift b/Meshtastic/Extensions/CoreData/DeviceMetadataEntityExtension.swift index d505c4d9..4352ba6c 100644 --- a/Meshtastic/Extensions/CoreData/DeviceMetadataEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/DeviceMetadataEntityExtension.swift @@ -1,13 +1,10 @@ import Foundation -import CoreData +import SwiftData import MeshtasticProtobufs extension DeviceMetadataEntity { - convenience init( - context: NSManagedObjectContext, - metadata: DeviceMetadata - ) { - self.init(context: context) + convenience init(metadata: DeviceMetadata) { + self.init() self.time = Date() self.deviceStateVersion = Int32(metadata.deviceStateVersion) self.canShutdown = metadata.canShutdown diff --git a/Meshtastic/Extensions/CoreData/ExternalNotificationConfigEntityExtension.swift b/Meshtastic/Extensions/CoreData/ExternalNotificationConfigEntityExtension.swift index 5fa8589c..d7e8c8a4 100644 --- a/Meshtastic/Extensions/CoreData/ExternalNotificationConfigEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ExternalNotificationConfigEntityExtension.swift @@ -1,12 +1,9 @@ -import CoreData +import SwiftData import MeshtasticProtobufs extension ExternalNotificationConfigEntity { - convenience init( - context: NSManagedObjectContext, - config: ModuleConfig.ExternalNotificationConfig - ) { - self.init(context: context) + convenience init(config: ModuleConfig.ExternalNotificationConfig) { + self.init() self.enabled = config.enabled self.usePWM = config.usePwm self.alertBell = config.alertBell diff --git a/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift b/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift index 5b35f620..8a2043f9 100644 --- a/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift @@ -5,7 +5,7 @@ // Copyright (c) Garth Vander Houwen 11/21/23. // -import CoreData +import SwiftData import CoreLocation import MapKit import SwiftUI diff --git a/Meshtastic/Extensions/CoreData/MQTTConfigEntityExtension.swift b/Meshtastic/Extensions/CoreData/MQTTConfigEntityExtension.swift index 29b1d064..58b84f28 100644 --- a/Meshtastic/Extensions/CoreData/MQTTConfigEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MQTTConfigEntityExtension.swift @@ -1,12 +1,9 @@ -import CoreData +import SwiftData import MeshtasticProtobufs extension MQTTConfigEntity { - convenience init( - context: NSManagedObjectContext, - config: ModuleConfig.MQTTConfig - ) { - self.init(context: context) + convenience init(config: ModuleConfig.MQTTConfig) { + self.init() self.enabled = config.enabled self.proxyToClientEnabled = config.proxyToClientEnabled self.address = config.address diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index a6f232fd..f0ab1165 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -5,7 +5,7 @@ // Created by Ben on 8/22/23. // -import CoreData +import SwiftData import CoreLocation import Foundation import MapKit @@ -40,18 +40,17 @@ extension MessageEntity { return re?.canRetry ?? false } + @MainActor var tapbacks: [MessageEntity] { - let context = PersistenceController.shared.container.viewContext - let fetchRequest = MessageEntity.fetchRequest() - fetchRequest.sortDescriptors = [ - NSSortDescriptor(key: "messageTimestamp", ascending: true) - ] - fetchRequest.predicate = NSPredicate( - format: "replyID == %lld AND isEmoji == true", - self.messageId + let context = PersistenceController.shared.context + let msgId = self.messageId + let descriptor = FetchDescriptor( + predicate: #Predicate { msg in + msg.replyID == msgId && msg.isEmoji == true + }, + sortBy: [SortDescriptor(\MessageEntity.messageTimestamp, order: .forward)] ) - - return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() + return (try? context.fetch(descriptor)) ?? [] } func displayTimestamp(aboveMessage: MessageEntity?) -> Bool { @@ -61,43 +60,38 @@ extension MessageEntity { return false // First message will have no timestamp } + @MainActor func relayDisplay() -> String? { guard self.relayNode != 0 else { return nil } - let context = PersistenceController.shared.container.viewContext + let context = PersistenceController.shared.context let relaySuffix = Int64(self.relayNode & 0xFF) - let request: NSFetchRequest = UserEntity.fetchRequest() - request.predicate = NSPredicate( - format: "(num & 0xFF) == %lld", - relaySuffix - ) + let descriptor = FetchDescriptor() - do { - let users = try context.fetch(request) - - // If exactly one match is found, return its name - if users.count == 1, let name = users.first?.longName, !name.isEmpty { - return "\(name)" - } - - // If no exact match, find the node with the smallest hopsAway - if let closestNode = users.min(by: { lhs, rhs in - guard let lhsHops = lhs.userNode?.hopsAway, - let rhsHops = rhs.userNode?.hopsAway - else { - return false - } - return lhsHops < rhsHops - }), let name = closestNode.longName, !name.isEmpty { - return "\(name)" - } - - // Fallback to hex node number if no matches - return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF)) - - } catch { + guard let users = try? context.fetch(descriptor) else { return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF)) } + let matchingUsers = users.filter { ($0.num & 0xFF) == relaySuffix } + + // If exactly one match is found, return its name + if matchingUsers.count == 1, let name = matchingUsers.first?.longName, !name.isEmpty { + return "\(name)" + } + + // If no exact match, find the node with the smallest hopsAway + if let closestNode = matchingUsers.min(by: { lhs, rhs in + guard let lhsHops = lhs.userNode?.hopsAway, + let rhsHops = rhs.userNode?.hopsAway + else { + return false + } + return lhsHops < rhsHops + }), let name = closestNode.longName, !name.isEmpty { + return "\(name)" + } + + // Fallback to hex node number if no matches + return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF)) } } diff --git a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift index c9e06d88..1398efd4 100644 --- a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift @@ -6,41 +6,36 @@ // import Foundation -import CoreData +import SwiftData extension MyInfoEntity { - var messagePredicate: NSPredicate { - return NSPredicate(format: "toUser == nil AND isEmoji == false") - } - - var messageFetchRequest: NSFetchRequest { - let fetchRequest = MessageEntity.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] - fetchRequest.predicate = messagePredicate - return fetchRequest - } - + @MainActor var messageList: [MessageEntity] { - let context = PersistenceController.shared.container.viewContext - let fetchRequest = messageFetchRequest - - return (try? context.fetch(messageFetchRequest)) ?? [MessageEntity]() + let context = PersistenceController.shared.context + let descriptor = FetchDescriptor( + predicate: #Predicate { msg in + msg.toUser == nil && msg.isEmoji == false + }, + sortBy: [SortDescriptor(\MessageEntity.messageTimestamp, order: .forward)] + ) + return (try? context.fetch(descriptor)) ?? [] } - func unreadMessages(context: NSManagedObjectContext) -> Int { - // Returns the count of unread *channel* messages - let fetchRequest = messageFetchRequest - fetchRequest.sortDescriptors = [] // sort is irrelevant. - fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) - - return (try? context.count(for: fetchRequest)) ?? 0 + @MainActor + func unreadMessages(context: ModelContext) -> Int { + let descriptor = FetchDescriptor( + predicate: #Predicate { msg in + msg.toUser == nil && msg.isEmoji == false && msg.read == false + } + ) + return (try? context.fetchCount(descriptor)) ?? 0 } - // Backwards-compatible property (uses viewContext) - var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.container.viewContext) } + @MainActor + var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.context) } var hasAdmin: Bool { - let adminChannel = channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" } - return adminChannel?.count ?? 0 > 0 + let adminChannel = channels.filter { $0.name?.lowercased() == "admin" } + return adminChannel.count > 0 } } diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index bed6d970..85514d58 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -6,68 +6,75 @@ // import Foundation -import CoreData +import SwiftData extension NodeInfoEntity { var latestPosition: PositionEntity? { - return self.positions?.lastObject as? PositionEntity + return self.positions.sorted(by: { ($0.time ?? .distantPast) < ($1.time ?? .distantPast) }).last } var latestDeviceMetrics: TelemetryEntity? { - return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity + return self.telemetries.filter { $0.metricsType == 0 }.sorted(by: { ($0.time ?? .distantPast) < ($1.time ?? .distantPast) }).last } var latestEnvironmentMetrics: TelemetryEntity? { - return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).lastObject as? TelemetryEntity + return self.telemetries.filter { $0.metricsType == 1 }.sorted(by: { ($0.time ?? .distantPast) < ($1.time ?? .distantPast) }).last } var latestPowerMetrics: TelemetryEntity? { - return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 2")).lastObject as? TelemetryEntity + return self.telemetries.filter { $0.metricsType == 2 }.sorted(by: { ($0.time ?? .distantPast) < ($1.time ?? .distantPast) }).last } var hasPositions: Bool { - return self.positions?.count ?? 0 > 0 + return self.positions.count > 0 } var hasDeviceMetrics: Bool { - let deviceMetrics = telemetries?.filter { ($0 as AnyObject).metricsType == 0 } - return deviceMetrics?.count ?? 0 > 0 + let deviceMetrics = telemetries.filter { $0.metricsType == 0 } + return deviceMetrics.count > 0 } var hasEnvironmentMetrics: Bool { - let environmentMetrics = telemetries?.filter { ($0 as AnyObject).metricsType == 1 } - return environmentMetrics?.count ?? 0 > 0 + let environmentMetrics = telemetries.filter { $0.metricsType == 1 } + return environmentMetrics.count > 0 } func hasDataForLatestEnvironmentMetrics(attributes: [String]) -> Bool { + guard let latest = self.latestEnvironmentMetrics else { return false } for attribute in attributes { - guard self.latestEnvironmentMetrics?.entity.attributesByName.keys.contains(attribute) ?? false else { - return false - } - if self.latestEnvironmentMetrics?.value(forKey: attribute) != nil { - return true + let mirror = Mirror(reflecting: latest) + if let child = mirror.children.first(where: { $0.label == attribute }) { + if child.value is Optional { + let m = Mirror(reflecting: child.value) + if m.displayStyle == .optional && m.children.count > 0 { + return true + } + } else { + return true + } } } return false } + @MainActor var hasDetectionSensorMetrics: Bool { return user?.sensorMessageList.count ?? 0 > 0 } var hasPowerMetrics: Bool { - let powerMetrics = telemetries?.filter { ($0 as AnyObject).metricsType == 2 } - return powerMetrics?.count ?? 0 > 0 + let powerMetrics = telemetries.filter { $0.metricsType == 2 } + return powerMetrics.count > 0 } var hasTraceRoutes: Bool { - let routes = traceRoutes?.filter { ($0 as AnyObject).response } - return routes?.count ?? 0 > 0 + let routes = traceRoutes.filter { $0.response } + return routes.count > 0 } var hasPax: Bool { - return pax?.count ?? 0 > 0 + return pax.count > 0 } var isStoreForwardRouter: Bool { @@ -86,18 +93,18 @@ extension NodeInfoEntity { if UserDefaults.enableAdministration { return true } else { - let adminChannel = myInfo?.channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" } + let adminChannel = myInfo?.channels.filter { $0.name?.lowercased() == "admin" } return adminChannel?.count ?? 0 > 0 } } } -public func createNodeInfo(num: Int64, context: NSManagedObjectContext) -> NodeInfoEntity { +func createNodeInfo(num: Int64, context: ModelContext) -> NodeInfoEntity { - let newNode = NodeInfoEntity(context: context) + let newNode = NodeInfoEntity() newNode.id = Int64(num) newNode.num = Int64(num) - let newUser = UserEntity(context: context) + let newUser = UserEntity() newUser.num = Int64(num) let userId = num.toHex() newUser.userId = "!\(userId)" @@ -106,5 +113,7 @@ public func createNodeInfo(num: Int64, context: NSManagedObjectContext) -> NodeI newUser.shortName = last4 newUser.hwModel = "UNSET" newNode.user = newUser + context.insert(newNode) + context.insert(newUser) return newNode } diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 080693c2..4e570b53 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -5,7 +5,7 @@ // Copyright(c) Garth Vander Houwen 11/28/21. // -import CoreData +import SwiftData import CoreLocation import MapKit import MeshtasticProtobufs @@ -14,44 +14,14 @@ import SwiftUI extension PositionEntity { @MainActor - static func allPositionsFetchRequest() -> NSFetchRequest { - - let request: NSFetchRequest = PositionEntity.fetchRequest() - request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] - let positionPredicate = NSPredicate(format: "nodePosition != nil AND nodePosition.user != nil AND latest == true AND nodePosition.user.shortName != ''") - request.predicate = positionPredicate - - // Distance Predicate - if let cl = LocationsHandler.currentLocation { - - let d: Double = UserDefaults.meshMapDistance * 1.1 - let r: Double = 6371009 // Earth's mean radius in meters - - // Calculate Bounding Box - let meanLatitidue = cl.latitude * .pi / 180 - let deltaLatitude = d / r * 180 / .pi - let deltaLongitude = d / (r * cos(meanLatitidue)) * 180 / .pi - - let minLatitude: Double = cl.latitude - deltaLatitude - let maxLatitude: Double = cl.latitude + deltaLatitude - let minLongitude: Double = cl.longitude - deltaLongitude - let maxLongitude: Double = cl.longitude + deltaLongitude - - // Scale bounding box values by 1e7 and use integer attributes (longitudeI, latitudeI) - let scale: Double = 1e7 - let minLongitudeI = Int(minLongitude * scale) - let maxLongitudeI = Int(maxLongitude * scale) - let minLatitudeI = Int(minLatitude * scale) - let maxLatitudeI = Int(maxLatitude * scale) - - // Use integer comparison in the predicate - let distancePredicate = NSPredicate(format: "(%ld <= longitudeI) AND (longitudeI <= %ld) AND (%ld <= latitudeI) AND (latitudeI <= %ld)", - minLongitudeI, maxLongitudeI, minLatitudeI, maxLatitudeI) - - request.predicate = NSCompoundPredicate(type: .and, subpredicates: [positionPredicate, distancePredicate]) - } - - return request + static func allPositionsFetchDescriptor() -> FetchDescriptor { + var descriptor = FetchDescriptor( + predicate: #Predicate { pos in + pos.nodePosition != nil && pos.latest == true + }, + sortBy: [SortDescriptor(\.time, order: .reverse)] + ) + return descriptor } var latitude: Double? { @@ -127,9 +97,19 @@ extension PositionEntity { } } -extension PositionEntity: MKAnnotation { - public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? LocationsHandler.DefaultLocation } - public var fuzzedCoordinate: CLLocationCoordinate2D { fuzzedNodeCoordinate ?? LocationsHandler.DefaultLocation } - public var title: String? { nodePosition?.user?.shortName ?? "Unknown".localized } - public var subtitle: String? { time?.formatted() } +class PositionAnnotation: NSObject, MKAnnotation { + let positionEntity: PositionEntity + @objc dynamic var coordinate: CLLocationCoordinate2D + var fuzzedCoordinate: CLLocationCoordinate2D + var title: String? + var subtitle: String? + + init(position: PositionEntity) { + self.positionEntity = position + self.coordinate = position.nodeCoordinate ?? LocationsHandler.DefaultLocation + self.fuzzedCoordinate = position.fuzzedNodeCoordinate ?? LocationsHandler.DefaultLocation + self.title = position.nodePosition?.user?.shortName ?? "Unknown".localized + self.subtitle = position.time?.formatted() + super.init() + } } diff --git a/Meshtastic/Extensions/CoreData/RangeTestConfigEntityExtension.swift b/Meshtastic/Extensions/CoreData/RangeTestConfigEntityExtension.swift index 20aa1713..768ae052 100644 --- a/Meshtastic/Extensions/CoreData/RangeTestConfigEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/RangeTestConfigEntityExtension.swift @@ -1,12 +1,9 @@ -import CoreData +import SwiftData import MeshtasticProtobufs extension RangeTestConfigEntity { - convenience init( - context: NSManagedObjectContext, - config: ModuleConfig.RangeTestConfig - ) { - self.init(context: context) + convenience init(config: ModuleConfig.RangeTestConfig) { + self.init() self.sender = Int32(config.sender) self.enabled = config.enabled self.save = config.save diff --git a/Meshtastic/Extensions/CoreData/SerialConfigEntityExtension.swift b/Meshtastic/Extensions/CoreData/SerialConfigEntityExtension.swift index 49400a94..6a57440e 100644 --- a/Meshtastic/Extensions/CoreData/SerialConfigEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/SerialConfigEntityExtension.swift @@ -1,12 +1,9 @@ -import CoreData +import SwiftData import MeshtasticProtobufs extension SerialConfigEntity { - convenience init( - context: NSManagedObjectContext, - config: ModuleConfig.SerialConfig - ) { - self.init(context: context) + convenience init(config: ModuleConfig.SerialConfig) { + self.init() self.enabled = config.enabled self.echo = config.echo self.rxd = Int32(config.rxd) diff --git a/Meshtastic/Extensions/CoreData/StoreForwardConfigEntityExtension.swift b/Meshtastic/Extensions/CoreData/StoreForwardConfigEntityExtension.swift index 886a79d5..511492e1 100644 --- a/Meshtastic/Extensions/CoreData/StoreForwardConfigEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/StoreForwardConfigEntityExtension.swift @@ -1,12 +1,9 @@ -import CoreData +import SwiftData import MeshtasticProtobufs extension StoreForwardConfigEntity { - convenience init( - context: NSManagedObjectContext, - config: ModuleConfig.StoreForwardConfig - ) { - self.init(context: context) + convenience init(config: ModuleConfig.StoreForwardConfig) { + self.init() self.enabled = config.enabled self.heartbeat = config.heartbeat self.records = Int32(config.records) diff --git a/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift b/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift index 804aacf8..b58159c5 100644 --- a/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift @@ -5,7 +5,7 @@ // Copyright(c) Garth Vander Houwen 12/7/23. // -import CoreData +import SwiftData import CoreLocation import MapKit import SwiftUI diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index 3a61ff4b..4db6cd5a 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -6,64 +6,39 @@ // import Foundation -import CoreData +import SwiftData import MeshtasticProtobufs extension UserEntity { - var messagePredicate: NSPredicate { - return NSPredicate(format: "((toUser == %@) OR (fromUser == %@)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10", self, self) - } - - var messageFetchRequest: NSFetchRequest { - let fetchRequest = MessageEntity.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] - fetchRequest.predicate = messagePredicate - return fetchRequest - } - + @MainActor var messageList: [MessageEntity] { - let context = PersistenceController.shared.container.viewContext - let fetchRequest = messageFetchRequest - - return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() + let context = PersistenceController.shared.context + let messages = (self.sentMessages ?? []) + (self.receivedMessages ?? []) + return messages.filter { msg in + msg.toUser != nil && msg.fromUser != nil && !msg.isEmoji && !msg.admin && msg.portNum != 10 + }.sorted { $0.messageTimestamp < $1.messageTimestamp } } + @MainActor var mostRecentMessage: MessageEntity? { - // Most contacts will have no DMs history, so we can return early. - guard self.lastMessage != nil else { return nil; } - - // Most recent DM for this user (descending, limit 1) - let context = PersistenceController.shared.container.viewContext - let fetchRequest = messageFetchRequest - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: false)] - fetchRequest.fetchLimit = 1 - - return (try? context.fetch(fetchRequest))?.first + guard self.lastMessage != nil else { return nil } + return messageList.last } + @MainActor var sensorMessageList: [MessageEntity] { - let context = PersistenceController.shared.container.viewContext - let fetchRequest = MessageEntity.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageTimestamp", ascending: true)] - fetchRequest.predicate = NSPredicate(format: "(fromUser == %@) AND portNum = 10", self) - - return (try? context.fetch(fetchRequest)) ?? [MessageEntity]() + return (self.sentMessages ?? []).filter { $0.portNum == 10 } + .sorted { $0.messageTimestamp < $1.messageTimestamp } } - func unreadMessages(context: NSManagedObjectContext, skipLastMessageCheck: Bool = false) -> Int { - // Most contacts will have no DMs history, so we can return early. - // (For our own node, set skipLastMessageCheck=true, because we don't update lastMessage on our own connected node.) - guard self.lastMessage != nil || skipLastMessageCheck else { return 0; } - - let fetchRequest = messageFetchRequest - fetchRequest.sortDescriptors = [] // sort is irrelevant. - fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate!, NSPredicate(format: "read == false")]) - - return (try? context.count(for: fetchRequest)) ?? 0 + @MainActor + func unreadMessages(context: ModelContext, skipLastMessageCheck: Bool = false) -> Int { + guard self.lastMessage != nil || skipLastMessageCheck else { return 0 } + return messageList.filter { !$0.read }.count } - // Backwards-compatible property (uses viewContext) - var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.container.viewContext) } + @MainActor + var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.context) } /// SVG Images for Vendors who are signed project backers var hardwareImage: String? { @@ -159,26 +134,22 @@ extension UserEntity { } } -public func createUser(num: Int64, context: NSManagedObjectContext) throws -> UserEntity { +func createUser(num: Int64, context: ModelContext) throws -> UserEntity { // Validate Input guard num >= 0 else { throw CoreDataError.invalidInput(message: "User number cannot be negative.") } - var newUser: UserEntity! // Use an implicitly unwrapped optional, but ensure it's assigned - - context.performAndWait { - newUser = UserEntity(context: context) - newUser.num = num - let userId = num.toHex() - newUser.userId = userId - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" - newUser.unmessagable = false - } - + let newUser = UserEntity() + newUser.num = num + let userId = num.toHex() + newUser.userId = userId + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + newUser.unmessagable = false + context.insert(newUser) return newUser } diff --git a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift index 4f2923eb..7ecece89 100644 --- a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift @@ -4,20 +4,22 @@ // // Copyright (c) Garth Vander Houwen 1/13/23. // -import CoreData +import SwiftData import CoreLocation import MapKit import SwiftUI extension WaypointEntity { - static func allWaypointssFetchRequest() -> NSFetchRequest { - let request: NSFetchRequest = WaypointEntity.fetchRequest() - request.fetchLimit = 50 - request.returnsDistinctResults = true - request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)] - request.predicate = NSPredicate(format: "expire == nil || expire >= %@", Date() as NSDate) - return request + @MainActor + static func allWaypointsFetchDescriptor() -> FetchDescriptor { + let now = Date() + return FetchDescriptor( + predicate: #Predicate { wp in + wp.expire == nil || wp.expire! >= now + }, + sortBy: [SortDescriptor(\.name, order: .reverse)] + ) } var latitude: Double? { @@ -54,26 +56,38 @@ extension WaypointEntity { } } -extension WaypointEntity: MKAnnotation { +extension WaypointEntity { @MainActor - public var coordinate: CLLocationCoordinate2D { + var mapCoordinate: CLLocationCoordinate2D { get { waypointCoordinate ?? LocationsHandler.DefaultLocation } - set { - latitudeI = Int32(newValue.latitude * 1e7) - longitudeI = Int32(newValue.longitude * 1e7) - } } - public var title: String? { + var mapTitle: String? { name ?? "Dropped Pin" } - public var subtitle: String? { + var mapSubtitle: String? { (longDescription ?? "") + String(expire != nil ? "\nāŒ› Expires \(String(describing: expire?.formatted()))" : "") + - String(locked > 0 ? "\nšŸ”’ Locked" : "") + String(locked ? "\nšŸ”’ Locked" : "") + } +} + +class WaypointAnnotation: NSObject, MKAnnotation { + let waypointEntity: WaypointEntity + @objc dynamic var coordinate: CLLocationCoordinate2D + var title: String? + var subtitle: String? + + @MainActor + init(waypoint: WaypointEntity) { + self.waypointEntity = waypoint + self.coordinate = waypoint.mapCoordinate + self.title = waypoint.mapTitle + self.subtitle = waypoint.mapSubtitle + super.init() } } diff --git a/Meshtastic/Helpers/ContactURLHandler.swift b/Meshtastic/Helpers/ContactURLHandler.swift index 0c6150c1..6aecdd94 100644 --- a/Meshtastic/Helpers/ContactURLHandler.swift +++ b/Meshtastic/Helpers/ContactURLHandler.swift @@ -5,7 +5,7 @@ // Created by Benjamin Faershtein on 6/27/25. // import SwiftUI -import CoreData +import SwiftData import OSLog import TipKit import MeshtasticProtobufs diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index fdde3515..daa38433 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -6,7 +6,7 @@ // import Foundation -import CoreData +import SwiftData import MeshtasticProtobufs import SwiftUI import RegexBuilder @@ -55,35 +55,31 @@ func generateMessageMarkdown (message: String) -> String { return message } +@ModelActor 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 + static let shared: MeshPackets = { + let container = PersistenceController.shared.container + return MeshPackets(modelContainer: container) }() - func localConfig (config: Config, nodeNum: Int64, nodeLongName: String) async { + func localConfig (config: Config, nodeNum: Int64, nodeLongName: String) { switch config.payloadVariant { case .bluetooth: - await self.upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum) + upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum) case .device: - await self.upsertDeviceConfigPacket(config: config.device, nodeNum: nodeNum) + upsertDeviceConfigPacket(config: config.device, nodeNum: nodeNum) case .display: - await self.upsertDisplayConfigPacket(config: config.display, nodeNum: nodeNum) + upsertDisplayConfigPacket(config: config.display, nodeNum: nodeNum) case .lora: - await self.upsertLoRaConfigPacket(config: config.lora, nodeNum: nodeNum) + upsertLoRaConfigPacket(config: config.lora, nodeNum: nodeNum) case .network: - await self.upsertNetworkConfigPacket(config: config.network, nodeNum: nodeNum) + upsertNetworkConfigPacket(config: config.network, nodeNum: nodeNum) case .position: - await self.upsertPositionConfigPacket(config: config.position, nodeNum: nodeNum) + upsertPositionConfigPacket(config: config.position, nodeNum: nodeNum) case .power: - await self.upsertPowerConfigPacket(config: config.power, nodeNum: nodeNum) + upsertPowerConfigPacket(config: config.power, nodeNum: nodeNum) case .security: - await self.upsertSecurityConfigPacket(config: config.security, nodeNum: nodeNum) + upsertSecurityConfigPacket(config: config.security, nodeNum: nodeNum) default: #if DEBUG Logger.services.error("ā‰ļø Unknown Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)") @@ -91,30 +87,30 @@ actor MeshPackets { } } - func moduleConfig (config: ModuleConfig, nodeNum: Int64, nodeLongName: String) async { + func moduleConfig (config: ModuleConfig, nodeNum: Int64, nodeLongName: String) { switch config.payloadVariant { case .ambientLighting: - await self.upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum) + upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum) case .cannedMessage: - await self.upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum) + upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum) case .detectionSensor: - await self.upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum) + upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum) case .externalNotification: - await self.upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum) + upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum) case .mqtt: - await self.upsertMqttModuleConfigPacket(config: config.mqtt, nodeNum: nodeNum) + upsertMqttModuleConfigPacket(config: config.mqtt, nodeNum: nodeNum) case .paxcounter: - await self.upsertPaxCounterModuleConfigPacket(config: config.paxcounter, nodeNum: nodeNum) + upsertPaxCounterModuleConfigPacket(config: config.paxcounter, nodeNum: nodeNum) case .rangeTest: - await self.upsertRangeTestModuleConfigPacket(config: config.rangeTest, nodeNum: nodeNum) + upsertRangeTestModuleConfigPacket(config: config.rangeTest, nodeNum: nodeNum) case .serial: - await self.upsertSerialModuleConfigPacket(config: config.serial, nodeNum: nodeNum) + upsertSerialModuleConfigPacket(config: config.serial, nodeNum: nodeNum) case .telemetry: - await self.upsertTelemetryModuleConfigPacket(config: config.telemetry, nodeNum: nodeNum) + upsertTelemetryModuleConfigPacket(config: config.telemetry, nodeNum: nodeNum) case .storeForward: - await self.upsertStoreForwardModuleConfigPacket(config: config.storeForward, nodeNum: nodeNum) + upsertStoreForwardModuleConfigPacket(config: config.storeForward, nodeNum: nodeNum) case .tak: - await self.upsertTAKModuleConfigPacket(config: config.tak, nodeNum: nodeNum) + upsertTAKModuleConfigPacket(config: config.tak, nodeNum: nodeNum) default: #if DEBUG Logger.services.error("ā‰ļø Unknown Module Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)") @@ -122,76 +118,67 @@ actor MeshPackets { } } - 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(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.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)") - } + func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String) -> PersistentIdentifier? { + let logString = String.localizedStringWithFormat("MyInfo received: %@".localized, String(myInfo.myNodeNum)) + Logger.mesh.info("ā„¹ļø \(logString, privacy: .public)") + + let myNodeNum = Int64(myInfo.myNodeNum) + let fetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.myNodeNum == myNodeNum }) + + do { + let fetchedMyInfo = try modelContext.fetch(fetchDescriptor) + // Not Found Insert + if fetchedMyInfo.isEmpty { + + let myInfoEntity = MyInfoEntity() + modelContext.insert(myInfoEntity) + myInfoEntity.peripheralId = peripheralId + myInfoEntity.myNodeNum = Int64(myInfo.myNodeNum) + myInfoEntity.rebootCount = Int32(myInfo.rebootCount) + myInfoEntity.deviceId = myInfo.deviceID + do { + try modelContext.save() + Logger.data.info("šŸ’¾ Saved a new myInfo for node: \(myInfo.myNodeNum.toHex(), privacy: .public)") + return myInfoEntity.persistentModelID + } catch { + modelContext.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 modelContext.save() + Logger.data.info("šŸ’¾ Updated myInfo for node: \(myInfo.myNodeNum.toHex(), privacy: .public)") + return fetchedMyInfo[0].persistentModelID + } catch { + modelContext.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 + } catch { + Logger.data.error("šŸ’„ Fetch MyInfo Error") } + return nil } - func channelPacket (channel: Channel, fromNum: Int64) async { - let context = self.backgroundContext - await context.perform { - self.channelPacket(channel: channel, fromNum: fromNum, context: context) - } - } - - nonisolated private func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectContext) { + func channelPacket (channel: Channel, fromNum: Int64) { 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) + let fetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.myNodeNum == fromNum }) do { - let fetchedMyInfo = try context.fetch(fetchedMyInfoRequest) + let fetchedMyInfo = try modelContext.fetch(fetchDescriptor) if fetchedMyInfo.count == 1 { - let newChannel = ChannelEntity(context: context) + let newChannel = ChannelEntity() + modelContext.insert(newChannel) newChannel.id = Int32(channel.index) newChannel.index = Int32(channel.index) newChannel.uplinkEnabled = channel.settings.uplinkEnabled @@ -203,19 +190,13 @@ actor MeshPackets { 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) + if let oldIndex = fetchedMyInfo[0].channels.firstIndex(where: { $0.index == newChannel.index }) { + fetchedMyInfo[0].channels[oldIndex] = newChannel } else { - mutableChannels.add(newChannel) + fetchedMyInfo[0].channels.append(newChannel) } - fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet - context.refresh(newChannel, mergeChanges: true) do { - try context.save() + try modelContext.save() } catch { Logger.data.error("šŸ’„ Failed to save channel: \(error.localizedDescription, privacy: .public)") } @@ -224,31 +205,24 @@ actor MeshPackets { Logger.data.error("šŸ’„Trying to save a channel to a MyInfo that does not exist: \(fromNum.toHex(), privacy: .public)") } } catch { - context.rollback() + modelContext.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()) 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) { + func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data()) { 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) + let fetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.num == fromNum }) do { - let fetchedNode = try context.fetch(fetchedNodeRequest) - let newMetadata = DeviceMetadataEntity(context: context) + let fetchedNode = try modelContext.fetch(fetchDescriptor) + let newMetadata = DeviceMetadataEntity() + modelContext.insert(newMetadata) newMetadata.time = Date() newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion) newMetadata.canShutdown = metadata.canShutdown @@ -265,48 +239,46 @@ actor MeshPackets { newMetadata.firmwareVersion = String(version) if fetchedNode.count > 0 { fetchedNode[0].metadata = newMetadata + if sessionPasskey?.count != 0 { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } } else { - if fromNum > 0 { - let newNode = createNodeInfo(num: Int64(fromNum), context: context) + let newNode = createNodeInfo(num: Int64(fromNum), context: modelContext) newNode.metadata = newMetadata } } - if sessionPasskey?.count != 0 { - fetchedNode[0].sessionPasskey = sessionPasskey - fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) - } do { - try context.save() + try modelContext.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() + modelContext.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, 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) + func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, deferSave: Bool = false) -> PersistentIdentifier? { + 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 nodeNum = Int64(nodeInfo.num) + let fetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.num == nodeNum }) + + do { + let fetchedNode = try modelContext.fetch(fetchDescriptor) + // Not Found Insert + if fetchedNode.isEmpty && nodeInfo.num > 0 { + + let newNode = NodeInfoEntity() + modelContext.insert(newNode) newNode.id = Int64(nodeInfo.num) newNode.num = Int64(nodeInfo.num) newNode.channel = Int32(nodeInfo.channel) @@ -315,14 +287,13 @@ actor MeshPackets { newNode.hopsAway = Int32(nodeInfo.hopsAway) if nodeInfo.hasDeviceMetrics { - let telemetry = TelemetryEntity(context: context) + let telemetry = TelemetryEntity() + modelContext.insert(telemetry) telemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) telemetry.voltage = nodeInfo.deviceMetrics.voltage telemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization telemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx - var newTelemetries = [TelemetryEntity]() - newTelemetries.append(telemetry) - newNode.telemetries? = NSOrderedSet(array: newTelemetries) + newNode.telemetries.append(telemetry) } if nodeInfo.lastHeard > 0 { newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) @@ -334,7 +305,8 @@ actor MeshPackets { newNode.snr = nodeInfo.snr if nodeInfo.hasUser { - let newUser = UserEntity(context: context) + let newUser = UserEntity() + modelContext.insert(newUser) newUser.userId = nodeInfo.num.toHex() newUser.num = Int64(nodeInfo.num) newUser.longName = nodeInfo.user.longName @@ -367,7 +339,7 @@ actor MeshPackets { newNode.user = newUser } else if nodeInfo.num > Constants.minimumNodeNum { do { - let newUser = try createUser(num: Int64(nodeInfo.num), context: context) + let newUser = try createUser(num: Int64(nodeInfo.num), context: modelContext) 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)") @@ -377,7 +349,8 @@ actor MeshPackets { } if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { - let position = PositionEntity(context: context) + let position = PositionEntity() + modelContext.insert(position) position.latest = true position.seqNo = Int32(nodeInfo.position.seqNumber) position.latitudeI = nodeInfo.position.latitudeI @@ -387,28 +360,26 @@ actor MeshPackets { 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) + newNode.positions.append(position) } // Look for a MyInfo - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) + let myInfoNodeNum = Int64(nodeInfo.num) + let fetchMyInfoDescriptor = FetchDescriptor(predicate: #Predicate { $0.myNodeNum == myInfoNodeNum }) do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + let fetchedMyInfo = try modelContext.fetch(fetchMyInfoDescriptor) if fetchedMyInfo.count > 0 { newNode.myInfo = fetchedMyInfo[0] } do { if !deferSave { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ Saved a new Node Info For: \(String(nodeInfo.num), privacy: .public)") } - return newNode.objectID + return newNode.persistentModelID } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError, privacy: .public)") } @@ -428,7 +399,9 @@ actor MeshPackets { if nodeInfo.hasUser { if fetchedNode[0].user == nil { - fetchedNode[0].user = UserEntity(context: context) + let newUserEntity = UserEntity() + modelContext.insert(newUserEntity) + fetchedNode[0].user = newUserEntity } // Set the public key for a user if it is empty, don't update if fetchedNode[0].user?.publicKey == nil && !nodeInfo.user.publicKey.isEmpty { @@ -477,7 +450,7 @@ actor MeshPackets { } else { if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum { do { - let newUser = try createUser(num: Int64(nodeInfo.num), context: context) + let newUser = try createUser(num: Int64(nodeInfo.num), context: modelContext) 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)") @@ -489,53 +462,48 @@ actor MeshPackets { if nodeInfo.hasDeviceMetrics { - let newTelemetry = TelemetryEntity(context: context) + let newTelemetry = TelemetryEntity() + modelContext.insert(newTelemetry) 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 + fetchedNode[0].telemetries.append(newTelemetry) } 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) + let position = PositionEntity() + modelContext.insert(position) 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 + fetchedNode[0].positions.append(position) } } // Look for a MyInfo - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) + let myInfoNodeNum2 = Int64(nodeInfo.num) + let fetchMyInfoDescriptor2 = FetchDescriptor(predicate: #Predicate { $0.myNodeNum == myInfoNodeNum2 }) do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + let fetchedMyInfo = try modelContext.fetch(fetchMyInfoDescriptor2) if fetchedMyInfo.count > 0 { fetchedNode[0].myInfo = fetchedMyInfo[0] } do { if !deferSave { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [NodeInfo] saved for \(nodeInfo.num.toHex(), privacy: .public)") } - return fetchedNode[0].objectID + return fetchedNode[0].persistentModelID } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ Error Saving Core Data NodeInfoEntity: \(nsError, privacy: .public)") } @@ -543,257 +511,225 @@ actor MeshPackets { Logger.data.error("šŸ’„ Fetch MyInfo Error") } } - } catch { - Logger.data.error("šŸ’„ Fetch NodeInfoEntity Error") + } catch { + Logger.data.error("šŸ’„ Fetch NodeInfoEntity Error") + } + return nil + } + + func adminAppPacket (packet: MeshPacket) { + 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 packetFrom = Int64(packet.from) + let fetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.num == packetFrom }) + + do { + let fetchedNode = try modelContext.fetch(fetchDescriptor) + 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 modelContext.save() + Logger.data.info("šŸ’¾ Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex() ?? "Unknown".localized, privacy: .public)") + } catch { + modelContext.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)) + } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) { + deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey) + } 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) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { + upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { + self.upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { + self.upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { + self.upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { + self.upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { + self.upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { + self.upsertSecurityConfigPacket(config: config.security, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey) + } + } 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)) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) { + self.upsertCannedMessagesModuleConfigPacket(config: moduleConfig.cannedMessage, nodeNum: Int64(packet.from)) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(moduleConfig.detectionSensor) { + self.upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from)) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(moduleConfig.externalNotification) { + self.upsertExternalNotificationModuleConfigPacket(config: moduleConfig.externalNotification, nodeNum: Int64(packet.from)) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(moduleConfig.mqtt) { + self.upsertMqttModuleConfigPacket(config: moduleConfig.mqtt, nodeNum: Int64(packet.from)) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(moduleConfig.rangeTest) { + self.upsertRangeTestModuleConfigPacket(config: moduleConfig.rangeTest, nodeNum: Int64(packet.from)) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(moduleConfig.serial) { + self.upsertSerialModuleConfigPacket(config: moduleConfig.serial, nodeNum: Int64(packet.from)) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.storeForward(moduleConfig.storeForward) { + self.upsertStoreForwardModuleConfigPacket(config: moduleConfig.storeForward, nodeNum: Int64(packet.from)) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(moduleConfig.telemetry) { + self.upsertTelemetryModuleConfigPacket(config: moduleConfig.telemetry, nodeNum: Int64(packet.from)) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.tak(moduleConfig.tak) { + self.upsertTAKModuleConfigPacket(config: moduleConfig.tak, nodeNum: Int64(packet.from)) + } + } 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)) + } + } else { + Logger.mesh.error("šŸ•øļø MESH PACKET received Admin App UNHANDLED \((try? packet.decoded.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } - return nil + // 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) } } - func adminAppPacket (packet: MeshPacket) async { - let context = self.backgroundContext - await context.perform { - if let adminMessage = try? AdminMessage(serializedBytes: packet.decoded.payload) { + private func adminResponseAck (packet: MeshPacket) { + let requestID = Int64(packet.decoded.requestID) + let fetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.messageId == requestID }) + do { + let fetchedMessage = try modelContext.fetch(fetchDescriptor) + 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 + + do { + try modelContext.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) { + let logString = String.localizedStringWithFormat("PAX Counter message received from: %@".localized, String(packet.from)) + Logger.mesh.info("šŸ§‘ā€šŸ¤ā€šŸ§‘ \(logString, privacy: .public)") + + let packetFrom = Int64(packet.from) + let fetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.num == packetFrom }) + + do { + let fetchedNode = try modelContext.fetch(fetchDescriptor) + + if let paxMessage = try? Paxcount(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) { - 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 moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.tak(moduleConfig.tak) { - self.upsertTAKModuleConfigPacket(config: moduleConfig.tak, 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) + let newPax = PaxCounterEntity() + modelContext.insert(newPax) + 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 { + fetchedNode[0].pax.append(newPax) + do { + try modelContext.save() + } catch { + Logger.data.error("Failed to save pax: \(error.localizedDescription, privacy: .public)") } } else { - Logger.mesh.error("šŸ•øļø MESH PACKET received Admin App UNHANDLED \((try? packet.decoded.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + Logger.data.info("Node Info Not Found") } - // 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) } + } catch { + } } - nonisolated private func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) { - let fetchedAdminMessageRequest = MessageEntity.fetchRequest() - fetchedAdminMessageRequest.predicate = NSPredicate(format: "messageId == %lld", packet.decoded.requestID) + func routingPacket (packet: MeshPacket, connectedNodeNum: Int64) { + 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 requestID = Int64(packet.decoded.requestID) + let fetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.messageId == requestID }) + do { - let fetchedMessage = try context.fetch(fetchedAdminMessageRequest) + let fetchedMessage = try modelContext.fetch(fetchDescriptor) 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 + 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 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)") - } + if packet.rxTime > 0 { + fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) } else { - Logger.data.info("Node Info Not Found") + fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970) } - } - } 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 + + } else { 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) + try modelContext.save() + Logger.data.info("šŸ’¾ ACK Saved for Message: \(packet.decoded.requestID, privacy: .public)") + } catch { + modelContext.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) { + 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() + modelContext.insert(telemetry) + let packetFrom = Int64(packet.from) + let fetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.num == packetFrom }) + do { + let fetchedNode = try modelContext.fetch(fetchDescriptor) 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) { @@ -860,18 +796,14 @@ actor MeshPackets { 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) + fetchedNode[0].telemetries.append(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() + try modelContext.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 @@ -930,14 +862,13 @@ actor MeshPackets { #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)") + } catch { + modelContext.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)") } } @@ -949,9 +880,7 @@ actor MeshPackets { storeForward: Bool = false, appState: AppState? ) async { - let context = self.backgroundContext - await context.perform { - var messageText = String(bytes: packet.decoded.payload, encoding: .utf8) + var messageText = String(bytes: packet.decoded.payload, encoding: .utf8) let rangeRef = Reference(Int.self) let rangeTestRegex = Regex { "seq " @@ -978,11 +907,13 @@ actor MeshPackets { 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]) + let toNum = Int64(packet.to) + let fromNum = Int64(packet.from) + let fetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.num == toNum || $0.num == fromNum }) do { - let fetchedUsers = try context.fetch(messageUsers) - let newMessage = MessageEntity(context: context) + let fetchedUsers = try modelContext.fetch(fetchDescriptor) + let newMessage = MessageEntity() + modelContext.insert(newMessage) newMessage.messageId = Int64(packet.id) if packet.rxTime > 0 { newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) @@ -1015,7 +946,7 @@ actor MeshPackets { newMessage.toUser = nil } else { do { - let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.to), context: modelContext) 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)") @@ -1053,15 +984,15 @@ actor MeshPackets { } else { /// Make a new from user if they are unknown do { - let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: modelContext) // Reuse an existing NodeInfoEntity if present to avoid creating duplicates - let fetchExistingNodeRequest = NodeInfoEntity.fetchRequest() - fetchExistingNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - let existingNodes = try context.fetch(fetchExistingNodeRequest) + let existingNodeFetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.num == fromNum }) + let existingNodes = try modelContext.fetch(existingNodeFetchDescriptor) if let existingNode = existingNodes.first { existingNode.user = newUser } else { - let newNode = NodeInfoEntity(context: context) + let newNode = NodeInfoEntity() + modelContext.insert(newNode) newNode.id = Int64(newUser.num) newNode.num = Int64(newUser.num) newNode.user = newUser @@ -1085,11 +1016,11 @@ actor MeshPackets { } var messageSaved = false do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ Saved a new message for \(newMessage.messageId, privacy: .public)") messageSaved = true } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("Failed to save new MessageEntity \(nsError, privacy: .public)") } @@ -1101,7 +1032,7 @@ actor MeshPackets { 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 + let unreadCount = await newMessage.toUser?.unreadMessages(context: modelContext, skipLastMessageCheck: true) ?? 0 // skipLastMessageCheck=true because we don't update lastMessage on our own connected node Task { @MainActor in appState?.unreadDirectMessages = unreadCount } @@ -1130,17 +1061,14 @@ actor MeshPackets { } } } else if newMessage.fromUser != nil && newMessage.toUser == nil { - let fetchMyInfoRequest = MyInfoEntity.fetchRequest() - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) + let myInfoFetchDescriptor = FetchDescriptor(predicate: #Predicate { $0.myNodeNum == connectedNode }) do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + let fetchedMyInfo = try modelContext.fetch(myInfoFetchDescriptor) if !fetchedMyInfo.isEmpty { + let ctx = modelContext 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) - } + appState?.unreadChannelMessages = fetchedMyInfo[0].unreadMessages(context: ctx) + for channel in fetchedMyInfo[0].channels { if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications && newMessage.isEmoji == false { // Create an iOS Notification for the received channel message let manager = LocalNotificationManager() @@ -1169,126 +1097,123 @@ actor MeshPackets { } } } - } 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) 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 { - 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)) + func waypointPacket (packet: MeshPacket) { + 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 waypointId = Int64(waypointMessage.id) + let fetchWaypointDescriptor = FetchDescriptor(predicate: #Predicate { $0.id == waypointId }) - 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)) + let fetchedWaypoint = try modelContext.fetch(fetchWaypointDescriptor) + // Fetch the node info to get the short name + var nodeShortName: String = "?" + let packetFrom = Int64(packet.from) + let fetchNodeDescriptor = FetchDescriptor(predicate: #Predicate { $0.num == packetFrom }) + do { + let fetchedNode = try modelContext.fetch(fetchNodeDescriptor) + 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() + modelContext.insert(waypoint) + 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 = waypointMessage.lockedTo != 0 + waypoint.createdBy = Int64(packet.from) + if waypointMessage.expire >= 1 { + waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) + } else { + waypoint.expire = nil + } + waypoint.created = Date() 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()) + try modelContext.save() + 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 { - Logger.data.error("Failed to fetch NodeInfoEntity for node \(packet.from.toHex(), privacy: .public): \(error)") + modelContext.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") } - 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) - waypoint.createdBy = Int64(packet.from) - 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)") - - 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() + } else { + // Update existing waypoint + let existingWaypoint = fetchedWaypoint[0] + if !existingWaypoint.locked { + let currentTime = Int64(Date().timeIntervalSince1970) + if waypointMessage.expire > 0 && waypointMessage.expire <= currentTime { + modelContext.delete(existingWaypoint) + do { + try modelContext.save() + Logger.data.info("šŸ’¾ Deleted a waypoint") + } catch { + modelContext.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)") } - } 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 = waypointMessage.lockedTo != 0 + existingWaypoint.lastUpdatedBy = Int64(packet.from) + if waypointMessage.expire >= 1 { + existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire))) } 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) - existingWaypoint.lastUpdatedBy = Int64(packet.from) - 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)") - } + existingWaypoint.expire = nil + } + existingWaypoint.lastUpdated = Date() + do { + try modelContext.save() + Logger.data.info("šŸ’¾ Updated Node Waypoint App Packet For: \(existingWaypoint.id, privacy: .public)") + } catch { + modelContext.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/Helpers/TAK/TAKMeshtasticBridge.swift b/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift index 8985accf..fc119f13 100644 --- a/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift +++ b/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift @@ -8,7 +8,7 @@ import Foundation import MeshtasticProtobufs import OSLog -import CoreData +import SwiftData /// Bridges CoT messages between TAK clients and the Meshtastic mesh network /// Handles bidirectional conversion and message routing @@ -18,8 +18,8 @@ final class TAKMeshtasticBridge { weak var accessoryManager: AccessoryManager? weak var takServerManager: TAKServerManager? - /// Core Data context for node lookups - var context: NSManagedObjectContext? + /// SwiftData context for node lookups + var context: ModelContext? /// Lookup table mapping callsigns to device UIDs /// Populated when receiving PLI packets from other TAK users @@ -519,7 +519,7 @@ final class TAKMeshtasticBridge { guard let takServerManager, takServerManager.isRunning else { return } // Get context - try the bridge's context first, then fall back to PersistenceController - let context = self.context ?? PersistenceController.shared.container.viewContext + let context = self.context ?? PersistenceController.shared.context let twoHoursAgo = Date().addingTimeInterval(-7200) @@ -530,14 +530,13 @@ final class TAKMeshtasticBridge { // Fetch all nodes - be more lenient, include any node that's been heard from // We'll check positions when creating CoT messages - let fetchRequest: NSFetchRequest = NodeInfoEntity.fetchRequest() - fetchRequest.predicate = NSPredicate( - format: "user != nil" + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.user != nil }, + sortBy: [SortDescriptor(\NodeInfoEntity.lastHeard, order: .reverse)] ) - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "lastHeard", ascending: false)] do { - let nodes = try context.fetch(fetchRequest) + let nodes = try context.fetch(descriptor) Logger.tak.info("Found \(nodes.count) total nodes with user info, connected node: \(connectedNodeNum)") var broadcastCount = 0 @@ -594,15 +593,17 @@ final class TAKMeshtasticBridge { private func lookupNodeInfo(nodeNum: UInt32) -> NodeInfoEntity? { // Use PersistenceController's viewContext directly to ensure we can find nodes - let context = PersistenceController.shared.container.viewContext + let context = PersistenceController.shared.context // Use the same format as MeshPackets - num is Int64 - let fetchRequest: NSFetchRequest = NodeInfoEntity.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - fetchRequest.fetchLimit = 1 + let nodeNumInt64 = Int64(nodeNum) + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == nodeNumInt64 } + ) + descriptor.fetchLimit = 1 do { - return try context.fetch(fetchRequest).first + return try context.fetch(descriptor).first } catch { Logger.tak.warning("Failed to lookup node info for \(nodeNum): \(error.localizedDescription)") return nil diff --git a/Meshtastic/Helpers/TAK/TAKServerManager.swift b/Meshtastic/Helpers/TAK/TAKServerManager.swift index b619af98..5b2bd553 100644 --- a/Meshtastic/Helpers/TAK/TAKServerManager.swift +++ b/Meshtastic/Helpers/TAK/TAKServerManager.swift @@ -10,7 +10,7 @@ import Network import OSLog import Combine import SwiftUI -import CoreData +import SwiftData import MeshtasticProtobufs enum TAKServerError: LocalizedError { @@ -140,17 +140,16 @@ final class TAKServerManager: ObservableObject { /// Check the primary channel for validity /// Returns true if the primary channel is valid for TAK server operation func checkPrimaryChannelValidity() { - let context = PersistenceController.shared.container.viewContext - let fetchRequest = MyInfoEntity.fetchRequest() + let context = PersistenceController.shared.context + let descriptor = FetchDescriptor() var issues: [PrimaryChannelIssue] = [] var isValid = true do { - let myInfos = try context.fetch(fetchRequest) + let myInfos = try context.fetch(descriptor) guard let myInfo = myInfos.first, - let channels = myInfo.channels?.array as? [ChannelEntity], - let primaryChannel = channels.first(where: { $0.index == 0 || $0.role == 1 }) else { + let primaryChannel = myInfo.channels.first(where: { $0.index == 0 || $0.role == 1 }) else { issues.append(PrimaryChannelIssue( title: "No Primary Channel", description: "No primary channel found on device", @@ -584,7 +583,7 @@ final class TAKServerManager: ObservableObject { Logger.tak.info("Auto-fixing primary channel for TAK compatibility") - let context = PersistenceController.shared.container.viewContext + let context = PersistenceController.shared.context guard let connectedNodeNum = accessoryManager.activeDeviceNum else { Logger.tak.error("Cannot fix channel: No active device number") @@ -597,13 +596,12 @@ final class TAKServerManager: ObservableObject { return false } - let fetchRequest = MyInfoEntity.fetchRequest() + let descriptor = FetchDescriptor() do { - let myInfos = try context.fetch(fetchRequest) + let myInfos = try context.fetch(descriptor) guard let myInfo = myInfos.first, - let channels = myInfo.channels?.array as? [ChannelEntity], - let primaryChannel = channels.first(where: { $0.index == 0 || $0.role == 1 }) else { + let primaryChannel = myInfo.channels.first(where: { $0.index == 0 || $0.role == 1 }) else { Logger.tak.error("Cannot fix channel: No primary channel found") return false } @@ -619,12 +617,9 @@ final class TAKServerManager: ObservableObject { primaryChannel.role = 1 primaryChannel.index = 0 - if let mutableChannels = myInfo.channels?.mutableCopy() as? NSMutableOrderedSet { - if mutableChannels.contains(primaryChannel) { - mutableChannels.remove(primaryChannel) - mutableChannels.insert(primaryChannel, at: 0) - myInfo.channels = mutableChannels.copy() as? NSOrderedSet - } + if let idx = myInfo.channels.firstIndex(of: primaryChannel), idx != 0 { + myInfo.channels.remove(at: idx) + myInfo.channels.insert(primaryChannel, at: 0) } try context.save() diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 9d9f6789..f04a846a 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -1,7 +1,7 @@ // Copyright (C) 2022 Garth Vander Houwen import SwiftUI -import CoreData +import SwiftData import OSLog import TipKit import MeshtasticProtobufs @@ -202,12 +202,12 @@ struct MeshtasticAppleApp: App { SessionReplay.stopRecording() accessoryManager.appDidEnterBackground() do { - try persistenceController.container.viewContext.save() - Logger.services.info("šŸ’¾ [App] Saved CoreData ViewContext when the app went to the background.") + try persistenceController.container.mainContext.save() + Logger.services.info("šŸ’¾ [App] Saved SwiftData context when the app went to the background.") } catch { - Logger.services.error("šŸ’„ [App] Failed to save viewContext when the app goes to the background.") + Logger.services.error("šŸ’„ [App] Failed to save context when the app goes to the background.") } case .inactive: Logger.services.info("šŸŽ¬ [App] Scene is inactive") @@ -220,7 +220,7 @@ struct MeshtasticAppleApp: App { Logger.services.error("šŸŽ [App] Apple must have changed something") } } - .environment(\.managedObjectContext, persistenceController.container.viewContext) + .modelContainer(persistenceController.container) .environmentObject(appState) .environmentObject(accessoryManager) } diff --git a/Meshtastic/Model/ChannelEntity.swift b/Meshtastic/Model/ChannelEntity.swift new file mode 100644 index 00000000..e3c5b7ce --- /dev/null +++ b/Meshtastic/Model/ChannelEntity.swift @@ -0,0 +1,26 @@ +// +// ChannelEntity.swift +// Meshtastic +// +// SwiftData model for channels. +// + +import Foundation +import SwiftData + +@Model +final class ChannelEntity { + var downlinkEnabled: Bool = false + var id: Int32 = 0 + var index: Int32 = 0 + var mute: Bool = false + var name: String? + var positionPrecision: Int32 = 32 + var psk: Data? + var role: Int32 = 0 + var uplinkEnabled: Bool = false + + var myInfoChannel: MyInfoEntity? + + init() {} +} diff --git a/Meshtastic/Model/ConfigModels.swift b/Meshtastic/Model/ConfigModels.swift new file mode 100644 index 00000000..c888d0bf --- /dev/null +++ b/Meshtastic/Model/ConfigModels.swift @@ -0,0 +1,320 @@ +// +// ConfigModels.swift +// Meshtastic +// +// SwiftData models for all device and module configuration entities. +// + +import Foundation +import SwiftData + +@Model +final class AmbientLightingConfigEntity { + var blue: Int32 = 0 + var current: Int32 = 0 + var green: Int32 = 0 + var ledState: Bool = false + var red: Int32 = 0 + var ambientLightingConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class BluetoothConfigEntity { + var deviceLoggingEnabled: Bool = false + var enabled: Bool = false + var fixedPin: Int32 = 123456 + var mode: Int32 = 0 + var bluetoothConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class CannedMessageConfigEntity { + var enabled: Bool = false + var inputbrokerEventCcw: Int32 = 0 + var inputbrokerEventCw: Int32 = 0 + var inputbrokerEventPress: Int32 = 0 + var inputbrokerPinA: Int32 = 0 + var inputbrokerPinB: Int32 = 0 + var inputbrokerPinPress: Int32 = 0 + var messages: String? + var rotary1Enabled: Bool = false + var sendBell: Bool = false + var updown1Enabled: Bool = false + var cannedMessagesConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class DetectionSensorConfigEntity { + var enabled: Bool = false + var minimumBroadcastSecs: Int32 = 0 + var monitorPin: Int32 = 0 + var name: String? + var sendBell: Bool = false + var stateBroadcastSecs: Int32 = 0 + var triggerType: Int32 = 0 + var usePullup: Bool = false + var detectionSensorConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class DeviceConfigEntity { + var buttonGpio: Int32 = 0 + var buzzerGpio: Int32 = 0 + var debugLogEnabled: Bool = false + var disableTripleClick: Bool = false + var doubleTapAsButtonPress: Bool = false + var isManaged: Bool = false + var ledHeartbeatEnabled: Bool = true + var nodeInfoBroadcastSecs: Int32 = 0 + var rebroadcastMode: Int32 = 0 + var role: Int32 = 0 + var serialEnabled: Bool = false + var tripleClickAsAdHocPing: Bool = true + var tzdef: String? + var deviceConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class DisplayConfigEntity { + var compassNorthTop: Bool = false + var displayMode: Int32 = 0 + var flipScreen: Bool = false + var headingBold: Bool = true + var oledType: Int32 = 0 + var screenCarouselInterval: Int32 = 0 + var screenOnSeconds: Int32 = 0 + var units: Int32 = 0 + var use12HClock: Bool = false + var wakeOnTapOrMotion: Bool = false + var displayConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class ExternalNotificationConfigEntity { + var active: Bool = false + var alertBell: Bool = false + var alertBellBuzzer: Bool = false + var alertBellVibra: Bool = false + var alertMessage: Bool = false + var alertMessageBuzzer: Bool = false + var alertMessageVibra: Bool = false + var enabled: Bool = false + var nagTimeout: Int32 = 0 + var output: Int32 = 0 + var outputBuzzer: Int32 = 0 + var outputMilliseconds: Int32 = 0 + var outputVibra: Int32 = 0 + var useI2SAsBuzzer: Bool = false + var usePWM: Bool = true + var externalNotificationConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class LoRaConfigEntity { + var bandwidth: Int32 = 0 + var channelNum: Int32 = 0 + var codingRate: Int32 = 0 + var frequencyOffset: Float = 0 + var hopLimit: Int32 = 0 + var ignoreMqtt: Bool = false + var modemPreset: Int32 = 0 + var okToMqtt: Bool = false + var overrideDutyCycle: Bool = false + var overrideFrequency: Float = 0.0 + var regionCode: Int32 = 0 + var spreadFactor: Int32 = 0 + var sx126xRxBoostedGain: Bool = false + var txEnabled: Bool = true + var txPower: Int32 = 0 + var usePreset: Bool = true + var loRaConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class MQTTConfigEntity { + var address: String? + var enabled: Bool = false + var encryptionEnabled: Bool = false + var jsonEnabled: Bool = false + var mapPositionPrecision: Int32 = 13 + var mapPublishIntervalSecs: Int32 = 0 + var mapReportingEnabled: Bool = false + var mapReportingShouldReportLocation: Bool = false + var password: String? + var proxyToClientEnabled: Bool = false + var root: String? = "msh" + var tlsEnabled: Bool = false + var username: String? + var mqttConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class NetworkConfigEntity { + var dns: Int32 = 0 + var enabledProtocols: Int32 = 0 + var ethEnabled: Bool = false + var gateway: Int32 = 0 + var ip: Int32 = 0 + var ntpServer: String? + var subnet: Int32 = 0 + var wifiEnabled: Bool = false + var wifiMode: Int32 = 0 + var wifiPsk: String? + var wifiSsid: String? + var networkConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class PaxCounterConfigEntity { + var bleThreshold: Int32 = 0 + var enabled: Bool = false + var updateInterval: Int32 = 0 + var wifiThreshold: Int32 = -80 + var paxCounterConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class PositionConfigEntity { + var broadcastSmartMinimumDistance: Int32 = 0 + var broadcastSmartMinimumIntervalSecs: Int32 = 0 + var deviceGpsEnabled: Bool = false + var fixedPosition: Bool = false + var gpsAttemptTime: Int32 = 0 + var gpsEnGpio: Int32 = 0 + var gpsMode: Int32 = 0 + var gpsUpdateInterval: Int32 = 0 + var positionBroadcastSeconds: Int32 = 0 + var positionFlags: Int32 = 0 + var rxGpio: Int32 = 0 + var smartPositionEnabled: Bool = false + var txGpio: Int32 = 0 + var positionConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class PowerConfigEntity { + var adcMultiplierOverride: Float = 0 + var deviceBatteryInaAddress: Int32 = 0 + var isPowerSaving: Bool = false + var lsSecs: Int32 = 0 + var minWakeSecs: Int32 = 0 + var onBatteryShutdownAfterSecs: Int32 = 0 + var waitBluetoothSecs: Int32 = 0 + var powerConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class RangeTestConfigEntity { + var enabled: Bool = false + var save: Bool = false + var sender: Int32 = 0 + var rangeTestConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class RTTTLConfigEntity { + var ringtone: String? + var rtttlConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class SecurityConfigEntity { + var adminChannelEnabled: Bool = false + var adminKey: Data? + var adminKey2: Data? + var adminKey3: Data? + var bluetoothLoggingEnabled: Bool = false + var debugLogApiEnabled: Bool = false + var isManaged: Bool = false + var privateKey: Data? + var publicKey: Data? + var serialEnabled: Bool = false + var securityConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class SerialConfigEntity { + var baudRate: Int32 = 0 + var echo: Bool = false + var enabled: Bool = false + var mode: Int32 = 0 + var overrideConsoleSerialPort: Bool = false + var rxd: Int32 = 0 + var timeout: Int32 = 0 + var txd: Int32 = 0 + var serialConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class StoreForwardConfigEntity { + var enabled: Bool = false + var heartbeat: Bool = false + var historyReturnMax: Int32 = 0 + var historyReturnWindow: Int32 = 0 + var isRouter: Bool = false + var lastHeartbeat: Date? + var lastRequest: Int32 = 0 + var records: Int32 = 0 + var storeForwardConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class TAKConfigEntity { + var role: Int32 = 0 + var team: Int32 = 0 + var takConfigNode: NodeInfoEntity? + + init() {} +} + +@Model +final class TelemetryConfigEntity { + var deviceTelemetryEnabled: Bool = false + var deviceUpdateInterval: Int32 = 0 + var environmentDisplayFahrenheit: Bool = false + var environmentMeasurementEnabled: Bool = false + var environmentScreenEnabled: Bool = false + var environmentUpdateInterval: Int32 = 0 + var powerMeasurementEnabled: Bool = false + var powerScreenEnabled: Bool = false + var powerUpdateInterval: Int32 = 0 + var telemetryConfigNode: NodeInfoEntity? + + init() {} +} diff --git a/Meshtastic/Model/DeviceMetadataEntity.swift b/Meshtastic/Model/DeviceMetadataEntity.swift new file mode 100644 index 00000000..2112af1c --- /dev/null +++ b/Meshtastic/Model/DeviceMetadataEntity.swift @@ -0,0 +1,28 @@ +// +// DeviceMetadataEntity.swift +// Meshtastic +// +// SwiftData model for device metadata. +// + +import Foundation +import SwiftData + +@Model +final class DeviceMetadataEntity { + var canShutdown: Bool = false + var deviceStateVersion: Int32 = 0 + var excludedModules: Int32 = 0 + var firmwareVersion: String? + var hasBluetooth: Bool = false + var hasEthernet: Bool = false + var hasWifi: Bool = false + var hwModel: String? + var positionFlags: Int32 = 0 + var role: Int32 = 0 + var time: Date? + + var metadataNode: NodeInfoEntity? + + init() {} +} diff --git a/Meshtastic/Model/MeshtasticSchema.swift b/Meshtastic/Model/MeshtasticSchema.swift new file mode 100644 index 00000000..f480145e --- /dev/null +++ b/Meshtastic/Model/MeshtasticSchema.swift @@ -0,0 +1,53 @@ +// +// MeshtasticSchema.swift +// Meshtastic +// +// SwiftData schema definition and migration plan for migrating from Core Data. +// + +import Foundation +import SwiftData + +/// All model types in the Meshtastic schema +enum MeshtasticSchema { + static var allModels: [any PersistentModel.Type] { + [ + // Core entities + NodeInfoEntity.self, + UserEntity.self, + MyInfoEntity.self, + MessageEntity.self, + ChannelEntity.self, + PositionEntity.self, + WaypointEntity.self, + DeviceMetadataEntity.self, + TelemetryEntity.self, + PaxCounterEntity.self, + TraceRouteEntity.self, + TraceRouteHopEntity.self, + RouteEntity.self, + LocationEntity.self, + // Config entities + AmbientLightingConfigEntity.self, + BluetoothConfigEntity.self, + CannedMessageConfigEntity.self, + DetectionSensorConfigEntity.self, + DeviceConfigEntity.self, + DisplayConfigEntity.self, + ExternalNotificationConfigEntity.self, + LoRaConfigEntity.self, + MQTTConfigEntity.self, + NetworkConfigEntity.self, + PaxCounterConfigEntity.self, + PositionConfigEntity.self, + PowerConfigEntity.self, + RangeTestConfigEntity.self, + RTTTLConfigEntity.self, + SecurityConfigEntity.self, + SerialConfigEntity.self, + StoreForwardConfigEntity.self, + TAKConfigEntity.self, + TelemetryConfigEntity.self, + ] + } +} diff --git a/Meshtastic/Model/MessageEntity.swift b/Meshtastic/Model/MessageEntity.swift new file mode 100644 index 00000000..5451b07c --- /dev/null +++ b/Meshtastic/Model/MessageEntity.swift @@ -0,0 +1,43 @@ +// +// MessageEntity.swift +// Meshtastic +// +// SwiftData model for messages. +// + +import Foundation +import SwiftData + +@Model +final class MessageEntity { + var ackError: Int32 = 0 + var ackSNR: Float = 0.0 + var ackTimestamp: Int32 = 0 + var admin: Bool = false + var adminDescription: String? + var channel: Int32 = 0 + var isEmoji: Bool = false + var messageId: Int64 = 0 + var messagePayload: String? = "" + var messagePayloadMarkdown: String? + var messagePayloadTranslated: String? + var messagePayloadTranslatedMarkdown: String? + var messageTimestamp: Int32 = 0 + var pkiEncrypted: Bool = false + var portNum: Int32 = 0 + var publicKey: Data? + var read: Bool = false + var realACK: Bool = false + var receivedACK: Bool = false + var relayNode: Int64 = 0 + var relays: Int16 = 0 + var replyID: Int64 = 0 + var rssi: Int32 = 0 + var showTranslatedMessage: Bool = false + var snr: Float = 0.0 + + var fromUser: UserEntity? + var toUser: UserEntity? + + init() {} +} diff --git a/Meshtastic/Model/Metrics Visualization/MetricTableColumn.swift b/Meshtastic/Model/Metrics Visualization/MetricTableColumn.swift index 5a3c53de..33aa0a3f 100644 --- a/Meshtastic/Model/Metrics Visualization/MetricTableColumn.swift +++ b/Meshtastic/Model/Metrics Visualization/MetricTableColumn.swift @@ -48,7 +48,7 @@ class MetricsTableColumn: ObservableObject { visible: Bool = true, @ViewBuilder tableBody: @escaping (MetricsTableColumn, Value) -> TableContent? ) { - // This works because TelemetryEntity is an NSManagedObject and derrived from NSObject + // This works because TelemetryEntity is an @Model and conforms to PersistentModel self.id = id self.name = name self.abbreviatedName = abbreviatedName diff --git a/Meshtastic/Model/Metrics Visualization/MetricsChartSeries.swift b/Meshtastic/Model/Metrics Visualization/MetricsChartSeries.swift index 2a11bd7e..de72f169 100644 --- a/Meshtastic/Model/Metrics Visualization/MetricsChartSeries.swift +++ b/Meshtastic/Model/Metrics Visualization/MetricsChartSeries.swift @@ -59,7 +59,7 @@ class MetricsChartSeries: ObservableObject { @ChartContentBuilder chartBody: @escaping (MetricsChartSeries, ClosedRange?, Date, Value) -> ChartBody? ) { - // This works because TelemetryEntity is an NSManagedObject and derrived from NSObject + // This works because TelemetryEntity is an @Model and conforms to PersistentModel self.id = id self.name = name self.abbreviatedName = abbreviatedName diff --git a/Meshtastic/Model/MyInfoEntity.swift b/Meshtastic/Model/MyInfoEntity.swift new file mode 100644 index 00000000..f38b5af8 --- /dev/null +++ b/Meshtastic/Model/MyInfoEntity.swift @@ -0,0 +1,27 @@ +// +// MyInfoEntity.swift +// Meshtastic +// +// SwiftData model for connected device info. +// + +import Foundation +import SwiftData + +@Model +final class MyInfoEntity { + var bleName: String? + var deviceId: Data? + var minAppVersion: Int32 = 0 + var myNodeNum: Int64 = 0 + var peripheralId: String? + var rebootCount: Int32 = 0 + var registered: Bool = false + + @Relationship(deleteRule: .cascade, inverse: \ChannelEntity.myInfoChannel) + var channels: [ChannelEntity] = [] + + var myInfoNode: NodeInfoEntity? + + init() {} +} diff --git a/Meshtastic/Model/NodeInfoEntity.swift b/Meshtastic/Model/NodeInfoEntity.swift new file mode 100644 index 00000000..5e40800f --- /dev/null +++ b/Meshtastic/Model/NodeInfoEntity.swift @@ -0,0 +1,112 @@ +// +// NodeInfoEntity.swift +// Meshtastic +// +// SwiftData model for the central node information entity. +// + +import Foundation +import SwiftData + +@Model +final class NodeInfoEntity { + var bleName: String? + var channel: Int32 = 0 + var favorite: Bool = false + var firstHeard: Date? + var hopsAway: Int32 = 0 + var id: Int64 = 0 + var ignored: Bool = false + var lastHeard: Date? + var num: Int64 = 0 + var peripheralId: String? + var rssi: Int32 = 0 + var sessionExpiration: Date? + var sessionPasskey: Data? + var snr: Float = 0.0 + var viaMqtt: Bool = false + + // Config relationships (to-one, cascade) + @Relationship(deleteRule: .cascade, inverse: \AmbientLightingConfigEntity.ambientLightingConfigNode) + var ambientLightingConfig: AmbientLightingConfigEntity? + + @Relationship(deleteRule: .cascade, inverse: \BluetoothConfigEntity.bluetoothConfigNode) + var bluetoothConfig: BluetoothConfigEntity? + + @Relationship(deleteRule: .cascade, inverse: \CannedMessageConfigEntity.cannedMessagesConfigNode) + var cannedMessageConfig: CannedMessageConfigEntity? + + @Relationship(deleteRule: .cascade, inverse: \DetectionSensorConfigEntity.detectionSensorConfigNode) + var detectionSensorConfig: DetectionSensorConfigEntity? + + @Relationship(deleteRule: .cascade, inverse: \DeviceConfigEntity.deviceConfigNode) + var deviceConfig: DeviceConfigEntity? + + @Relationship(deleteRule: .cascade, inverse: \DisplayConfigEntity.displayConfigNode) + var displayConfig: DisplayConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \ExternalNotificationConfigEntity.externalNotificationConfigNode) + var externalNotificationConfig: ExternalNotificationConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \LoRaConfigEntity.loRaConfigNode) + var loRaConfig: LoRaConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \DeviceMetadataEntity.metadataNode) + var metadata: DeviceMetadataEntity? + + @Relationship(deleteRule: .nullify, inverse: \MQTTConfigEntity.mqttConfigNode) + var mqttConfig: MQTTConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \MyInfoEntity.myInfoNode) + var myInfo: MyInfoEntity? + + @Relationship(deleteRule: .nullify, inverse: \NetworkConfigEntity.networkConfigNode) + var networkConfig: NetworkConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \PaxCounterEntity.paxNode) + var pax: [PaxCounterEntity] = [] + + @Relationship(deleteRule: .nullify, inverse: \PaxCounterConfigEntity.paxCounterConfigNode) + var paxCounterConfig: PaxCounterConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \PositionConfigEntity.positionConfigNode) + var positionConfig: PositionConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \PositionEntity.nodePosition) + var positions: [PositionEntity] = [] + + @Relationship(deleteRule: .nullify, inverse: \PowerConfigEntity.powerConfigNode) + var powerConfig: PowerConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \RangeTestConfigEntity.rangeTestConfigNode) + var rangeTestConfig: RangeTestConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \RTTTLConfigEntity.rtttlConfigNode) + var rtttlConfig: RTTTLConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \SecurityConfigEntity.securityConfigNode) + var securityConfig: SecurityConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \SerialConfigEntity.serialConfigNode) + var serialConfig: SerialConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \StoreForwardConfigEntity.storeForwardConfigNode) + var storeForwardConfig: StoreForwardConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \TAKConfigEntity.takConfigNode) + var takConfig: TAKConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \TelemetryEntity.nodeTelemetry) + var telemetries: [TelemetryEntity] = [] + + @Relationship(deleteRule: .nullify, inverse: \TelemetryConfigEntity.telemetryConfigNode) + var telemetryConfig: TelemetryConfigEntity? + + @Relationship(deleteRule: .nullify, inverse: \TraceRouteEntity.node) + var traceRoutes: [TraceRouteEntity] = [] + + @Relationship(deleteRule: .nullify, inverse: \UserEntity.userNode) + var user: UserEntity? + + init() {} +} diff --git a/Meshtastic/Model/PositionEntity.swift b/Meshtastic/Model/PositionEntity.swift new file mode 100644 index 00000000..9578a00e --- /dev/null +++ b/Meshtastic/Model/PositionEntity.swift @@ -0,0 +1,29 @@ +// +// PositionEntity.swift +// Meshtastic +// +// SwiftData model for node positions. +// + +import Foundation +import SwiftData + +@Model +final class PositionEntity { + var altitude: Int32 = 0 + var heading: Int32 = 0 + var latest: Bool = false + var latitudeI: Int32 = 0 + var longitudeI: Int32 = 0 + var precisionBits: Int32 = 32 + var rssi: Int32 = 0 + var satsInView: Int32 = 0 + var seqNo: Int32 = 0 + var snr: Float = 0.0 + var speed: Int32 = 0 + var time: Date? + + var nodePosition: NodeInfoEntity? + + init() {} +} diff --git a/Meshtastic/Model/RouteModels.swift b/Meshtastic/Model/RouteModels.swift new file mode 100644 index 00000000..29520629 --- /dev/null +++ b/Meshtastic/Model/RouteModels.swift @@ -0,0 +1,53 @@ +// +// RouteModels.swift +// Meshtastic +// +// SwiftData models for routes and locations. +// + +import Foundation +import SwiftData + +@Model +final class RouteEntity { + var color: Int64 = 0 + var date: Date? + var distance: Double = 0 + var elevationGain: Double = 0 + var enabled: Bool = false + var endDate: Date? + var id: Int32 = 0 + var name: String? + var notes: String? + + @Relationship(deleteRule: .cascade, inverse: \LocationEntity.routeLocation) + var locations: [LocationEntity] = [] + + init() {} +} + +@Model +final class LocationEntity { + var altitude: Int32 = 0 + var heading: Int32 = 0 + var id: Int32 = 0 + var latitudeI: Int32 = 0 + var longitudeI: Int32 = 0 + var speed: Int32 = 0 + + var routeLocation: RouteEntity? + + init() {} +} + +@Model +final class PaxCounterEntity { + var ble: Int32 = 0 + var time: Date? + var uptime: Int32 = 0 + var wifi: Int32 = 0 + + var paxNode: NodeInfoEntity? + + init() {} +} diff --git a/Meshtastic/Model/TelemetryEntity.swift b/Meshtastic/Model/TelemetryEntity.swift new file mode 100644 index 00000000..318b7d65 --- /dev/null +++ b/Meshtastic/Model/TelemetryEntity.swift @@ -0,0 +1,74 @@ +// +// TelemetryEntity.swift +// Meshtastic +// +// SwiftData model for telemetry data. +// Replaces the manual Core Data TelemetryEntity+CoreDataClass/Properties files. +// + +import Foundation +import SwiftData + +@Model +final class TelemetryEntity { + // Non-optional scalars + var metricsType: Int32 = 0 + var numOnlineNodes: Int32 = 0 + var numPacketsRx: Int32 = 0 + var numPacketsRxBad: Int32 = 0 + var numPacketsTx: Int32 = 0 + var numRxDupe: Int32 = 0 + var numTotalNodes: Int32 = 0 + var numTxRelay: Int32 = 0 + var numTxRelayCanceled: Int32 = 0 + var time: Date? + + // Optional scalars (previously used @ManagedAttribute wrapper) + var airUtilTx: Float? + var barometricPressure: Float? + var batteryLevel: Int32? + var channelUtilization: Float? + var current: Float? + var distance: Float? + var gasResistance: Float? + var iaq: Int32? + var irLux: Float? + var lux: Float? + var powerCh1Current: Float? + var powerCh1Voltage: Float? + var powerCh2Current: Float? + var powerCh2Voltage: Float? + var powerCh3Current: Float? + var powerCh3Voltage: Float? + var radiation: Float? + var rainfall1H: Float? + var rainfall24H: Float? + var relativeHumidity: Float? + var rssi: Int32? + var snr: Float? + var soilMoisture: UInt32? + var soilTemperature: Float? + var temperature: Float? + var uptimeSeconds: Int32? + var uvLux: Float? + var voltage: Float? + var weight: Float? + var whiteLux: Float? + var windDirection: Int32? + var windGust: Float? + var windLull: Float? + var windSpeed: Float? + + // Relationship + var nodeTelemetry: NodeInfoEntity? + + // Computed property + var dewPoint: Float? { + guard let temp = self.temperature, let rh = self.relativeHumidity else { + return nil + } + return Float(calculateDewPoint(temp: temp, relativeHumidity: rh, convertToLocale: false)) + } + + init() {} +} diff --git a/Meshtastic/Model/TraceRouteModels.swift b/Meshtastic/Model/TraceRouteModels.swift new file mode 100644 index 00000000..34d7a6fb --- /dev/null +++ b/Meshtastic/Model/TraceRouteModels.swift @@ -0,0 +1,46 @@ +// +// TraceRouteModels.swift +// Meshtastic +// +// SwiftData models for trace routes and hops. +// + +import Foundation +import SwiftData + +@Model +final class TraceRouteEntity { + var hasPositions: Bool = false + var hopsBack: Int32 = 0 + var hopsTowards: Int32 = 0 + var id: Int64 = 0 + var response: Bool = false + var routeBackText: String? + var routeText: String? + var sent: Bool = false + var snr: Float = 0.0 + var time: Date? + + @Relationship(deleteRule: .cascade, inverse: \TraceRouteHopEntity.traceRoute) + var hops: [TraceRouteHopEntity] = [] + + var node: NodeInfoEntity? + + init() {} +} + +@Model +final class TraceRouteHopEntity { + var altitude: Int32 = 0 + var back: Bool = false + var latitudeI: Int32 = 0 + var longitudeI: Int32 = 0 + var name: String? + var num: Int64 = 0 + var snr: Float = 0.0 + var time: Date? + + var traceRoute: TraceRouteEntity? + + init() {} +} diff --git a/Meshtastic/Model/UserEntity.swift b/Meshtastic/Model/UserEntity.swift new file mode 100644 index 00000000..ae9b5f10 --- /dev/null +++ b/Meshtastic/Model/UserEntity.swift @@ -0,0 +1,40 @@ +// +// UserEntity.swift +// Meshtastic +// +// SwiftData model for user information. +// + +import Foundation +import SwiftData + +@Model +final class UserEntity { + var hwDisplayName: String? + var hwModel: String? + var hwModelId: Int32 = 0 + var isLicensed: Bool = false + var keyMatch: Bool = true + var lastMessage: Date? + var longName: String? + var mute: Bool = false + var newPublicKey: Data? + var num: Int64 = 0 + var numString: String? + var pkiEncrypted: Bool = false + var publicKey: Data? + var role: Int32 = 0 + var shortName: String? + var unmessagable: Bool = false + var userId: String? + + @Relationship(inverse: \MessageEntity.fromUser) + var sentMessages: [MessageEntity] = [] + + @Relationship(inverse: \MessageEntity.toUser) + var receivedMessages: [MessageEntity] = [] + + var userNode: NodeInfoEntity? + + init() {} +} diff --git a/Meshtastic/Model/WaypointEntity.swift b/Meshtastic/Model/WaypointEntity.swift new file mode 100644 index 00000000..59f720db --- /dev/null +++ b/Meshtastic/Model/WaypointEntity.swift @@ -0,0 +1,27 @@ +// +// WaypointEntity.swift +// Meshtastic +// +// SwiftData model for waypoints. +// + +import Foundation +import SwiftData + +@Model +final class WaypointEntity { + var created: Date? + var createdBy: Int64 = 0 + var expire: Date? + var icon: Int64 = 0 + var id: Int64 = 0 + var lastUpdated: Date? + var lastUpdatedBy: Int64 = 0 + var latitudeI: Int32 = 0 + var locked: Bool = false + var longDescription: String? + var longitudeI: Int32 = 0 + var name: String? + + init() {} +} diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 25c7e65b..65469b96 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -5,103 +5,75 @@ // Copyright(c) Garth Vander Houwen 11/28/21. // -import CoreData +import SwiftData import OSLog +@MainActor class PersistenceController { static let shared = PersistenceController() static var preview: PersistenceController = { - let result = PersistenceController(inMemory: false) - let viewContext = result.container.viewContext + let result = PersistenceController(inMemory: true) + let context = result.container.mainContext for _ in 0..<10 { - let newItem = NodeInfoEntity(context: viewContext) + let newItem = NodeInfoEntity() newItem.lastHeard = Date() - } - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + context.insert(newItem) } return result }() - let container: NSPersistentContainer + let container: ModelContainer + + var context: ModelContext { + container.mainContext + } init(inMemory: Bool = false) { + let schema = Schema(MeshtasticSchema.allModels) - container = NSPersistentContainer(name: "Meshtastic") + let config = ModelConfiguration( + "Meshtastic", + schema: schema, + isStoredInMemoryOnly: inMemory, + allowsSave: true + ) - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - - container.loadPersistentStores(completionHandler: { (_, error) in - - // Merge policy that favors in memory data over data in the db - self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - self.container.viewContext.automaticallyMergesChangesFromParent = true - self.container.viewContext.shouldDeleteInaccessibleFaults = true - - if let error = error as NSError? { - - Logger.data.error("CoreData Error: \(error.localizedDescription, privacy: .public). Now attempting to truncate CoreData database. All app data will be lost.") - self.clearDatabase() + do { + container = try ModelContainer(for: schema, configurations: config) + container.mainContext.autosaveEnabled = true + Logger.data.info("šŸ’¾ SwiftData store initialized successfully") + } catch { + Logger.data.error("SwiftData Error: \(error.localizedDescription, privacy: .public). Attempting to recreate database.") + // Attempt recovery by creating in-memory store + let fallbackConfig = ModelConfiguration( + "Meshtastic", + schema: schema, + isStoredInMemoryOnly: true + ) + do { + container = try ModelContainer(for: schema, configurations: fallbackConfig) + Logger.data.error("SwiftData database recreated in-memory. All app data has been lost.") + } catch { + fatalError("Failed to create ModelContainer: \(error.localizedDescription)") } - }) + } } - public func clearDatabase() { - guard let url = self.container.persistentStoreDescriptions.first?.url else { return } - - let persistentStoreCoordinator = self.container.persistentStoreCoordinator - do { - try persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil) - Logger.data.error("CoreData database truncated. All app data has been erased.") - - do { - try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) - } catch let error { - Logger.data.error("Failed to re-create CoreData database: \(error.localizedDescription, privacy: .public)") - } - - } catch let error { - Logger.data.error("Failed to destroy CoreData database, delete the app and re-install to clear data. Attempted to clear persistent store: \(error.localizedDescription, privacy: .public)") + @MainActor + public func clearDatabase(includeRoutes: Bool = true) { + do { + for modelType in MeshtasticSchema.allModels { + if !includeRoutes && (modelType == RouteEntity.self || modelType == LocationEntity.self) { + continue + } + try container.mainContext.delete(model: modelType) + } + try container.mainContext.save() + Logger.data.error("SwiftData database truncated. All app data has been erased.") + } catch { + Logger.data.error("Failed to clear SwiftData database: \(error.localizedDescription, privacy: .public)") } } } - -extension NSManagedObjectContext { - - /// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date. - /// - /// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute. - /// - Throws: An error if anything went wrong executing the batch deletion. - public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws { - batchDeleteRequest.resultType = .resultTypeObjectIDs - - let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult - let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []] - - NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self]) - } -} - -// Created by Tom Harrington on 5/12/20. -// Copyright Ā© 2020 Atomic Bird LLC. All rights reserved. -// Gist from https://atomicbird.com/blog/core-data-back-up-store/ -// -extension NSPersistentContainer { - enum CopyPersistentStoreErrors: Error { - case invalidDestination(String) - case destinationError(String) - case destinationNotRemoved(String) - case copyStoreError(String) - case invalidSource(String) - } - -} diff --git a/Meshtastic/Persistence/QueryCoreData.swift b/Meshtastic/Persistence/QueryCoreData.swift index dffe425b..972452b2 100644 --- a/Meshtastic/Persistence/QueryCoreData.swift +++ b/Meshtastic/Persistence/QueryCoreData.swift @@ -5,88 +5,62 @@ // Created(c) Garth Vander Houwen 1/16/23. // -import CoreData +import Foundation +import SwiftData -public func getNodeInfo(id: Int64, context: NSManagedObjectContext) -> NodeInfoEntity? { - - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(id)) - fetchNodeInfoRequest.fetchLimit = 1 - - do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - if fetchedNode.count == 1 { - return fetchedNode[0] - } - } catch { - return nil - } - return nil +func getNodeInfo(id: Int64, context: ModelContext) -> NodeInfoEntity? { + let num = id + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == num } + ) + descriptor.fetchLimit = 1 + return try? context.fetch(descriptor).first } -public func getStoreAndForwardMessageIds(seconds: Int, context: NSManagedObjectContext) -> [UInt32] { - +func getStoreAndForwardMessageIds(seconds: Int, context: ModelContext) -> [UInt32] { let time = seconds * -1 - let fetchMessagesRequest = MessageEntity.fetchRequest() let timeRange = Calendar.current.date(byAdding: .minute, value: time, to: Date()) let milleseconds = Int32(timeRange?.timeIntervalSince1970 ?? 0) - fetchMessagesRequest.predicate = NSPredicate(format: "messageTimestamp >= %d", milleseconds) - - do { - let fetchedMessages = try context.fetch(fetchMessagesRequest) - if fetchedMessages.count == 1 { - return fetchedMessages.map { UInt32($0.messageId) } - } - } catch { - return [] - } - return [] + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.messageTimestamp >= milleseconds } + ) + let fetchedMessages = (try? context.fetch(descriptor)) ?? [] + return fetchedMessages.map { UInt32($0.messageId) } } -public func getTraceRoute(id: Int64, context: NSManagedObjectContext) -> TraceRouteEntity? { - - let fetchTraceRouteRequest = TraceRouteEntity.fetchRequest() - fetchTraceRouteRequest.predicate = NSPredicate(format: "id == %lld", Int64(id)) - - do { - let fetchedTraceRoute = try context.fetch(fetchTraceRouteRequest) - if fetchedTraceRoute.count == 1 { - return fetchedTraceRoute[0] - } - } catch { - return nil - } - return nil +func getTraceRoute(id: Int64, context: ModelContext) -> TraceRouteEntity? { + let traceId = id + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.id == traceId } + ) + descriptor.fetchLimit = 1 + return try? context.fetch(descriptor).first } -public func getUser(id: Int64, context: NSManagedObjectContext) -> UserEntity { - - let fetchUserRequest = UserEntity.fetchRequest() - fetchUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(id)) - - do { - let fetchedUser = try context.fetch(fetchUserRequest) - if fetchedUser.count == 1 { - return fetchedUser[0] - } - } catch { - return UserEntity(context: context) +func getUser(id: Int64, context: ModelContext) -> UserEntity { + let userNum = id + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == userNum } + ) + if let existing = try? context.fetch(descriptor).first { + return existing } - return UserEntity(context: context) + let newUser = UserEntity() + newUser.num = id + context.insert(newUser) + return newUser } -public func getWaypoint(id: Int64, context: NSManagedObjectContext) -> WaypointEntity { - - let fetchWaypointRequest = WaypointEntity.fetchRequest() - fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(id)) - - do { - let fetchedWaypoint = try context.fetch(fetchWaypointRequest) - if fetchedWaypoint.count == 1 { - return fetchedWaypoint[0] - } - } catch { - return WaypointEntity(context: context) +func getWaypoint(id: Int64, context: ModelContext) -> WaypointEntity { + let waypointId = id + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.id == waypointId } + ) + if let existing = try? context.fetch(descriptor).first { + return existing } - return WaypointEntity(context: context) + let newWaypoint = WaypointEntity() + newWaypoint.id = id + context.insert(newWaypoint) + return newWaypoint } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index a73651fc..5f52af6c 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -4,19 +4,12 @@ // // Copyright(c) Garth Vander Houwen 10/3/22. -import CoreData +import SwiftData import MeshtasticProtobufs import OSLog extension MeshPackets { - public func clearStaleNodes(nodeExpireDays: Int) async -> Bool { - let context = self.backgroundContext - return await context.perform { - return self.clearStaleNodes(nodeExpireDays: nodeExpireDays, context: context) - } - } - - nonisolated public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) -> Bool { + public func clearStaleNodes(nodeExpireDays: Int) -> Bool { var nodeExpireTime: TimeInterval { return TimeInterval(-nodeExpireDays * 86400) } @@ -25,260 +18,191 @@ extension MeshPackets { } 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 - + let expireDate = Date(timeIntervalSinceNow: nodeExpireTime) + let pkiExpireDate = Date(timeIntervalSinceNow: nodePKIExpireTime) + let descriptor = FetchDescriptor( + predicate: #Predicate { node in + node.favorite == false && node.ignored == false && node.lastHeard != nil + } + ) 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 + let candidates = try modelContext.fetch(descriptor) + let staleNodes = candidates.filter { node in + guard let lastHeard = node.lastHeard else { return false } + if node.user?.pkiEncrypted == true { + return lastHeard < pkiExpireDate + } else { + return lastHeard < expireDate } - } else { - Logger.data.error("šŸ’„ [NodeInfoEntity] bad delete results") } + let deletedNodes = staleNodes.count + for node in staleNodes { + modelContext.delete(node) + } + try modelContext.save() + Logger.data.info("šŸ’¾ [NodeInfoEntity] Cleared \(deletedNodes) stale nodes") + return deletedNodes > 0 } catch { - context.rollback() Logger.data.error("šŸ’„ [NodeInfoEntity] Error deleting stale nodes") } return false } - 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)) - + func clearPax(destNum: Int64) -> Bool { + let num = destNum + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == num } + ) + descriptor.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - let newPax = [PaxCounterLog]() - fetchedNode[0].pax? = NSOrderedSet(array: newPax) - do { - try context.save() + if let node = try modelContext.fetch(descriptor).first { + node.pax = [] + try modelContext.save() return true - - } catch { - context.rollback() - return false } } catch { Logger.data.error("šŸ’„ [NodeInfoEntity] fetch data error") - return false } + return false } - 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)) - + public func clearPositions(destNum: Int64) -> Bool { + let num = destNum + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == num } + ) + descriptor.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - let newPostions = [PositionEntity]() - fetchedNode[0].positions? = NSOrderedSet(array: newPostions) - do { - try context.save() + if let node = try modelContext.fetch(descriptor).first { + node.positions = [] + try modelContext.save() return true - - } catch { - context.rollback() - return false } } catch { Logger.data.error("šŸ’„ [NodeInfoEntity] fetch data error") - return false } + return false } - 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)) - + public func clearTelemetry(destNum: Int64, metricsType: Int32) -> Bool { + let num = destNum + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == num } + ) + descriptor.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) - let emptyTelemetry = [TelemetryEntity]() - fetchedNode[0].telemetries? = NSOrderedSet(array: emptyTelemetry) - do { - try context.save() + if let node = try modelContext.fetch(descriptor).first { + node.telemetries = node.telemetries.filter { $0.metricsType != metricsType } + try modelContext.save() return true - - } catch { - context.rollback() - return false } } catch { Logger.data.error("šŸ’„ [NodeInfoEntity] fetch data error") - return false } + return false } - 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) + public func deleteChannelMessages(channel: ChannelEntity) { + let channelIndex = channel.index + let descriptor = FetchDescriptor( + predicate: #Predicate { msg in + msg.channel == channelIndex && msg.toUser == nil && msg.isEmoji == false } - } - } - - nonisolated public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) { + ) do { - // Copied logic from ChannelEntity.allPrivateMessages, which is always on the MainActor - // But this code may not be on the MainActor. - let fetchRequest = MessageEntity.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", channel.index) - let objects = (try? context.fetch(fetchRequest)) ?? [MessageEntity]() - + let objects = try modelContext.fetch(descriptor) for object in objects { - context.delete(object) + modelContext.delete(object) } - - try context.save() - } catch let error as NSError { + try modelContext.save() + } catch { Logger.data.error("\(error.localizedDescription, 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) - } + public func deleteUserMessages(user: UserEntity) { + let messages = (user.sentMessages ?? []) + (user.receivedMessages ?? []) + let filtered = messages.filter { msg in + msg.toUser != nil && msg.fromUser != nil && !msg.isEmoji && !msg.admin && msg.portNum != 10 + } + for object in filtered { + modelContext.delete(object) } - } - - nonisolated 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 { + try modelContext.save() + } catch { 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) - } + public func clearDatabase(includeRoutes: Bool) { + let allModels: [any PersistentModel.Type] = MeshtasticSchema.allModels + for modelType in allModels { + let typeName = String(describing: modelType) + if !includeRoutes && (typeName.contains("Route") || typeName.contains("Location")) { + continue } do { - try context.executeAndMergeChanges(using: deleteRequest) + try modelContext.delete(model: modelType) } 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) + do { + try modelContext.save() + } catch { + Logger.data.error("Failed to save after clearing database: \(error.localizedDescription, privacy: .public)") } } - nonisolated func updateAnyPacketFrom (packet: MeshPacket, activeDeviceNum: Int64, context: NSManagedObjectContext) { + func updateAnyPacketFrom (packet: MeshPacket, activeDeviceNum: Int64) { // 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 + guard packet.from != activeDeviceNum else { return } - let fetchNodeInfoAppRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + let num = Int64(packet.from) + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == num } + ) + descriptor.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) - if fetchedNode.count >= 1 { - fetchedNode[0].id = Int64(packet.from) - fetchedNode[0].num = Int64(packet.from) + if let node = try modelContext.fetch(descriptor).first { + node.id = Int64(packet.from) + node.num = Int64(packet.from) if packet.rxTime > 0 { - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + node.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() + node.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 + node.snr = packet.rxSnr + node.rssi = packet.rxRssi + node.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)") + node.hopsAway = Int32(packet.hopStart - packet.hopLimit) + Logger.data.info("šŸ’¾ [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) hopsAway=\(node.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)") + try modelContext.save() + Logger.data.info("šŸ’¾ [updateAnyPacketFrom] Updating node \(node.num.toHex(), privacy: .public) snr=\(node.snr), rssi=\(node.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)") + Logger.data.error("šŸ’„ [updateAnyPacketFrom] Error Saving node \(node.num.toHex(), privacy: .public) from packet \(packet.id.toHex(), privacy: .public) \(nsError, privacy: .public)") } } } catch { @@ -286,29 +210,24 @@ extension MeshPackets { } } - 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) { + func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false) { 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)) + let fetchNum = Int64(packet.from) + var fetchNodeInfoAppRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoAppRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoAppRequest) if fetchedNode.count == 0 { // Not Found Insert - let newNode = NodeInfoEntity(context: context) + let newNode = NodeInfoEntity() + modelContext.insert(newNode) newNode.id = Int64(packet.from) newNode.num = Int64(packet.from) newNode.favorite = favorite @@ -338,7 +257,7 @@ extension MeshPackets { if newUserMessage.id.isEmpty { if packet.from > Constants.minimumNodeNum { do { - let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: modelContext) 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)") @@ -348,7 +267,8 @@ extension MeshPackets { } } else { - let newUser = UserEntity(context: context) + let newUser = UserEntity() + modelContext.insert(newUser) newUser.userId = newNode.num.toHex() newUser.num = Int64(packet.from) newUser.longName = newUserMessage.longName @@ -401,7 +321,7 @@ extension MeshPackets { } else { if packet.from > Constants.minimumNodeNum { do { - let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: modelContext) if !packet.publicKey.isEmpty { newNode.user?.pkiEncrypted = true newNode.user?.publicKey = packet.publicKey @@ -417,24 +337,24 @@ extension MeshPackets { // 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) + let newUser = try createUser(num: Int64(packet.from), context: modelContext) 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() + modelContext.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() + modelContext.rollback() return } } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [NodeInfo] Saved a NodeInfo for node number: \(packet.from.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [NodeInfoEntity] Error Inserting New Core Data: \(nsError, privacy: .public)") } @@ -450,14 +370,15 @@ extension MeshPackets { fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway) fetchedNode[0].favorite = nodeInfoMessage.isFavorite if nodeInfoMessage.hasDeviceMetrics { - let telemetry = TelemetryEntity(context: context) + let telemetry = TelemetryEntity() + modelContext.insert(telemetry) 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) + fetchedNode[0].telemetries = newTelemetries } if nodeInfoMessage.hasUser { fetchedNode[0].user?.userId = nodeInfoMessage.num.toHex() @@ -495,7 +416,7 @@ extension MeshPackets { } if fetchedNode[0].user == nil { do { - let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context) + let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: modelContext) 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)") @@ -504,10 +425,10 @@ extension MeshPackets { } } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [NodeInfoEntity] Updated from Node Info App Packet For: \(fetchedNode[0].num.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [NodeInfoEntity] Error Saving from NODEINFO_APP \(nsError, privacy: .public)") } @@ -517,41 +438,34 @@ extension MeshPackets { } } - 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) { + func upsertPositionPacket (packet: MeshPacket) { 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)) - + let fetchNum = Int64(packet.from) + var fetchNodePositionRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodePositionRequest.fetchLimit = 1 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) + let fetchedNode = try modelContext.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) + let posNum = Int64(packet.from) + let fetchCurrentLatestPositionsRequest = FetchDescriptor(predicate: #Predicate { $0.nodePosition?.num == posNum && $0.latest == true }) + let fetchedPositions = try modelContext.fetch(fetchCurrentLatestPositionsRequest) if fetchedPositions.count > 0 { for position in fetchedPositions { position.latest = false } } - let position = PositionEntity(context: context) + let position = PositionEntity() + modelContext.insert(position) position.latest = true position.snr = packet.rxSnr position.rssi = packet.rxRssi @@ -572,28 +486,28 @@ extension MeshPackets { } else { position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) } - guard let mutablePositions = fetchedNode[0].positions?.mutableCopy() as? NSMutableOrderedSet else { - return - } + var mutablePositions = fetchedNode[0].positions /// 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) + if let mostRecentCoord = mutablePositions.last?.nodeCoordinate, + let positionCoord = position.nodeCoordinate, + mostRecentCoord.distance(from: positionCoord) < 9.0 { + mutablePositions.removeLast() } } else if mutablePositions.count > 0 { /// Don't store any history for reduced accuracy positions, we will just show a circle - mutablePositions.removeAllObjects() + mutablePositions.removeAll() } - mutablePositions.add(position) + mutablePositions.append(position) fetchedNode[0].channel = Int32(packet.channel) - fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet + fetchedNode[0].positions = mutablePositions do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [Position] Saved from Position App Packet For: \(fetchedNode[0].num.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ Error Saving NodeInfoEntity from POSITION_APP \(nsError, privacy: .public)") } @@ -607,27 +521,21 @@ extension MeshPackets { } } - 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) { + func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Device Config if !fetchedNode.isEmpty { if fetchedNode[0].bluetoothConfig == nil { - let newBluetoothConfig = BluetoothConfigEntity(context: context) + let newBluetoothConfig = BluetoothConfigEntity() + modelContext.insert(newBluetoothConfig) newBluetoothConfig.enabled = config.enabled newBluetoothConfig.mode = Int32(config.mode.rawValue) newBluetoothConfig.fixedPin = Int32(config.fixedPin) @@ -642,10 +550,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [BluetoothConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [BluetoothConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -658,26 +566,20 @@ extension MeshPackets { } } - 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) { + func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Device Config if !fetchedNode.isEmpty { if fetchedNode[0].deviceConfig == nil { - let newDeviceConfig = DeviceConfigEntity(context: context) + let newDeviceConfig = DeviceConfigEntity() + modelContext.insert(newDeviceConfig) newDeviceConfig.role = Int32(config.role.rawValue) newDeviceConfig.buttonGpio = Int32(config.buttonGpio) newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio) @@ -706,10 +608,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [DeviceConfigEntity] Updated Device Config for node number: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [DeviceConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -720,29 +622,23 @@ extension MeshPackets { } } - 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) { + func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Device Config if !fetchedNode.isEmpty { if fetchedNode[0].displayConfig == nil { - let newDisplayConfig = DisplayConfigEntity(context: context) + let newDisplayConfig = DisplayConfigEntity() + modelContext.insert(newDisplayConfig) newDisplayConfig.screenOnSeconds = Int32(truncatingIfNeeded: config.screenOnSecs) newDisplayConfig.screenCarouselInterval = Int32(truncatingIfNeeded: config.autoScreenCarouselSecs) newDisplayConfig.compassNorthTop = config.compassNorthTop @@ -770,11 +666,11 @@ extension MeshPackets { } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [DisplayConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [DisplayConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -788,27 +684,22 @@ extension MeshPackets { } } - 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) { + func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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) + let fetchNum = nodeNum + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.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) + let newLoRaConfig = LoRaConfigEntity() + modelContext.insert(newLoRaConfig) newLoRaConfig.regionCode = Int32(config.region.rawValue) newLoRaConfig.usePreset = config.usePreset newLoRaConfig.modemPreset = Int32(config.modemPreset.rawValue) @@ -850,10 +741,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [LoRaConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [LoRaConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -866,27 +757,21 @@ extension MeshPackets { } } - 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) { + func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save WiFi Config if !fetchedNode.isEmpty { if fetchedNode[0].networkConfig == nil { - let newNetworkConfig = NetworkConfigEntity(context: context) + let newNetworkConfig = NetworkConfigEntity() + modelContext.insert(newNetworkConfig) newNetworkConfig.wifiEnabled = config.wifiEnabled newNetworkConfig.wifiSsid = config.wifiSsid newNetworkConfig.wifiPsk = config.wifiPsk @@ -905,11 +790,11 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [NetworkConfigEntity] Updated Network Config for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [NetworkConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -922,27 +807,21 @@ extension MeshPackets { } } - 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) { + func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save LoRa Config if !fetchedNode.isEmpty { if fetchedNode[0].positionConfig == nil { - let newPositionConfig = PositionConfigEntity(context: context) + let newPositionConfig = PositionConfigEntity() + modelContext.insert(newPositionConfig) newPositionConfig.smartPositionEnabled = config.positionBroadcastSmartEnabled newPositionConfig.deviceGpsEnabled = config.gpsEnabled newPositionConfig.gpsMode = Int32(truncatingIfNeeded: config.gpsMode.rawValue) @@ -977,10 +856,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [PositionConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [PositionConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -993,26 +872,20 @@ extension MeshPackets { } } - 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) { + func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Power Config if !fetchedNode.isEmpty { if fetchedNode[0].powerConfig == nil { - let newPowerConfig = PowerConfigEntity(context: context) + let newPowerConfig = PowerConfigEntity() + modelContext.insert(newPowerConfig) newPowerConfig.adcMultiplierOverride = config.adcMultiplierOverride newPowerConfig.deviceBatteryInaAddress = Int32(config.deviceBatteryInaAddress) newPowerConfig.isPowerSaving = config.isPowerSaving @@ -1035,10 +908,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [PowerConfigEntity] Updated Power Config for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [PowerConfigEntity] Error Updating Core Data PowerConfigEntity: \(nsError, privacy: .public)") } @@ -1051,27 +924,21 @@ extension MeshPackets { } } - 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) { + func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Security Config if !fetchedNode.isEmpty { if fetchedNode[0].securityConfig == nil { - let newSecurityConfig = SecurityConfigEntity(context: context) + let newSecurityConfig = SecurityConfigEntity() + modelContext.insert(newSecurityConfig) newSecurityConfig.publicKey = config.publicKey newSecurityConfig.privateKey = config.privateKey if config.adminKey.count > 0 { @@ -1104,11 +971,11 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [SecurityConfigEntity] Updated Security Config for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [SecurityConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -1121,28 +988,22 @@ extension MeshPackets { } } - 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) { + func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Ambient Lighting Config if !fetchedNode.isEmpty { if fetchedNode[0].cannedMessageConfig == nil { - let newAmbientLightingConfig = AmbientLightingConfigEntity(context: context) + let newAmbientLightingConfig = AmbientLightingConfigEntity() + modelContext.insert(newAmbientLightingConfig) newAmbientLightingConfig.ledState = config.ledState newAmbientLightingConfig.current = Int32(config.current) newAmbientLightingConfig.red = Int32(config.red) @@ -1152,7 +1013,9 @@ extension MeshPackets { } else { if fetchedNode[0].ambientLightingConfig == nil { - fetchedNode[0].ambientLightingConfig = AmbientLightingConfigEntity(context: context) + let newAmbientLighting = AmbientLightingConfigEntity() + modelContext.insert(newAmbientLighting) + fetchedNode[0].ambientLightingConfig = newAmbientLighting } fetchedNode[0].ambientLightingConfig?.ledState = config.ledState fetchedNode[0].ambientLightingConfig?.current = Int32(config.current) @@ -1165,10 +1028,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [AmbientLightingConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [AmbientLightingConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -1181,28 +1044,22 @@ extension MeshPackets { } } - 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) { + func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Canned Message Config if !fetchedNode.isEmpty { if fetchedNode[0].cannedMessageConfig == nil { - let newCannedMessageConfig = CannedMessageConfigEntity(context: context) + let newCannedMessageConfig = CannedMessageConfigEntity() + modelContext.insert(newCannedMessageConfig) newCannedMessageConfig.enabled = config.enabled newCannedMessageConfig.sendBell = config.sendBell newCannedMessageConfig.rotary1Enabled = config.rotary1Enabled @@ -1231,10 +1088,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [CannedMessageConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [CannedMessageConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -1247,27 +1104,21 @@ extension MeshPackets { } } - 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) - } - } - - nonisolated func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Detection Sensor Config if !fetchedNode.isEmpty { if fetchedNode[0].detectionSensorConfig == nil { - let newConfig = DetectionSensorConfigEntity(context: context) + let newConfig = DetectionSensorConfigEntity() + modelContext.insert(newConfig) newConfig.enabled = config.enabled newConfig.sendBell = config.sendBell newConfig.name = config.name @@ -1292,11 +1143,11 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [DetectionSensorConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [DetectionSensorConfigEntity] Error Updating Core Data : \(nsError, privacy: .public)") } @@ -1311,28 +1162,22 @@ extension MeshPackets { } } - 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) - } - } - - nonisolated func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save External Notificaitone Config if !fetchedNode.isEmpty { if fetchedNode[0].externalNotificationConfig == nil { - let newExternalNotificationConfig = ExternalNotificationConfigEntity(context: context) + let newExternalNotificationConfig = ExternalNotificationConfigEntity() + modelContext.insert(newExternalNotificationConfig) newExternalNotificationConfig.enabled = config.enabled newExternalNotificationConfig.usePWM = config.usePwm newExternalNotificationConfig.alertBell = config.alertBell @@ -1371,10 +1216,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [ExternalNotificationConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [ExternalNotificationConfigEntity] Error Updating Core Data : \(nsError, privacy: .public)") } @@ -1387,27 +1232,21 @@ extension MeshPackets { } } - 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) { + func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save PAX Counter Config if !fetchedNode.isEmpty { if fetchedNode[0].paxCounterConfig == nil { - let newPaxCounterConfig = PaxCounterConfigEntity(context: context) + let newPaxCounterConfig = PaxCounterConfigEntity() + modelContext.insert(newPaxCounterConfig) newPaxCounterConfig.enabled = config.enabled newPaxCounterConfig.updateInterval = Int32(config.paxcounterUpdateInterval) fetchedNode[0].paxCounterConfig = newPaxCounterConfig @@ -1420,10 +1259,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [PaxCounterConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [PaxCounterConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -1436,27 +1275,21 @@ extension MeshPackets { } } - 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) { + func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save RTTTL Config if !fetchedNode.isEmpty { if fetchedNode[0].rtttlConfig == nil { - let newRtttlConfig = RTTTLConfigEntity(context: context) + let newRtttlConfig = RTTTLConfigEntity() + modelContext.insert(newRtttlConfig) newRtttlConfig.ringtone = ringtone fetchedNode[0].rtttlConfig = newRtttlConfig } else { @@ -1467,10 +1300,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [RtttlConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [RtttlConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -1483,27 +1316,21 @@ extension MeshPackets { } } - 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) { + func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save MQTT Config if !fetchedNode.isEmpty { if fetchedNode[0].mqttConfig == nil { - let newMQTTConfig = MQTTConfigEntity(context: context) + let newMQTTConfig = MQTTConfigEntity() + modelContext.insert(newMQTTConfig) newMQTTConfig.enabled = config.enabled newMQTTConfig.proxyToClientEnabled = config.proxyToClientEnabled newMQTTConfig.address = config.address @@ -1537,10 +1364,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [MQTTConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [MQTTConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -1553,27 +1380,21 @@ extension MeshPackets { } } - 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) { + func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Device Config if !fetchedNode.isEmpty { if fetchedNode[0].rangeTestConfig == nil { - let newRangeTestConfig = RangeTestConfigEntity(context: context) + let newRangeTestConfig = RangeTestConfigEntity() + modelContext.insert(newRangeTestConfig) newRangeTestConfig.sender = Int32(config.sender) newRangeTestConfig.enabled = config.enabled newRangeTestConfig.save = config.save @@ -1588,10 +1409,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [RangeTestConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -1604,27 +1425,21 @@ extension MeshPackets { } } - 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) { + func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Device Config if !fetchedNode.isEmpty { if fetchedNode[0].serialConfig == nil { - let newSerialConfig = SerialConfigEntity(context: context) + let newSerialConfig = SerialConfigEntity() + modelContext.insert(newSerialConfig) newSerialConfig.enabled = config.enabled newSerialConfig.echo = config.echo newSerialConfig.rxd = Int32(config.rxd) @@ -1647,11 +1462,11 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [SerialConfigEntity]Updated Serial Module Config for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [SerialConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") @@ -1666,27 +1481,21 @@ extension MeshPackets { } } - 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) { + func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) - + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Store & Forward Sensor Config if !fetchedNode.isEmpty { if fetchedNode[0].storeForwardConfig == nil { - let newConfig = StoreForwardConfigEntity(context: context) + let newConfig = StoreForwardConfigEntity() + modelContext.insert(newConfig) newConfig.enabled = config.enabled newConfig.heartbeat = config.heartbeat newConfig.records = Int32(config.records) @@ -1706,10 +1515,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [StoreForwardConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [StoreForwardConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -1722,26 +1531,21 @@ extension MeshPackets { } } - 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) { + func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { 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)) + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) // Found a node, save Telemetry Config if !fetchedNode.isEmpty { if fetchedNode[0].telemetryConfig == nil { - let newTelemetryConfig = TelemetryConfigEntity(context: context) + let newTelemetryConfig = TelemetryConfigEntity() + modelContext.insert(newTelemetryConfig) newTelemetryConfig.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval) newTelemetryConfig.deviceTelemetryEnabled = config.deviceTelemetryEnabled newTelemetryConfig.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval) @@ -1768,11 +1572,11 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [TelemetryConfigEntity] Updated Telemetry Module Config for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [TelemetryConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } @@ -1787,25 +1591,20 @@ extension MeshPackets { } } - func upsertTAKModuleConfigPacket(config: ModuleConfig.TAKConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { - let context = self.backgroundContext - await context.perform { - self.upsertTAKModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) - } - } - - nonisolated func upsertTAKModuleConfigPacket(config: ModuleConfig.TAKConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + func upsertTAKModuleConfigPacket(config: ModuleConfig.TAKConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) { let logString = String.localizedStringWithFormat("TAK module config received: %@".localized, String(nodeNum)) Logger.data.info("šŸŽÆ \(logString, privacy: .public)") - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + let fetchNum = Int64(nodeNum) + var fetchNodeInfoRequest = FetchDescriptor(predicate: #Predicate { $0.num == fetchNum }) + fetchNodeInfoRequest.fetchLimit = 1 do { - let fetchedNode = try context.fetch(fetchNodeInfoRequest) + let fetchedNode = try modelContext.fetch(fetchNodeInfoRequest) if !fetchedNode.isEmpty { if fetchedNode[0].takConfig == nil { - let newTAKConfig = TAKConfigEntity(context: context) + let newTAKConfig = TAKConfigEntity() + modelContext.insert(newTAKConfig) newTAKConfig.team = Int32(config.team.rawValue) newTAKConfig.role = Int32(config.role.rawValue) fetchedNode[0].takConfig = newTAKConfig @@ -1818,10 +1617,10 @@ extension MeshPackets { fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) } do { - try context.save() + try modelContext.save() Logger.data.info("šŸ’¾ [TAKConfigEntity] Updated TAK Module Config for node: \(nodeNum.toHex(), privacy: .public)") } catch { - context.rollback() + modelContext.rollback() let nsError = error as NSError Logger.data.error("šŸ’„ [TAKConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") } diff --git a/Meshtastic/Router/Router.swift b/Meshtastic/Router/Router.swift index 61a599c2..09fc4723 100644 --- a/Meshtastic/Router/Router.swift +++ b/Meshtastic/Router/Router.swift @@ -1,5 +1,5 @@ import Combine -import CoreData +import SwiftData import OSLog import SwiftUI @@ -44,27 +44,27 @@ class Router: ObservableObject { // MARK: Node Object ID Cache - /// In-memory cache mapping node numbers to their Core Data `NSManagedObjectID` for O(1) lookups. + /// In-memory cache mapping node numbers to their SwiftData `PersistentIdentifier` for O(1) lookups. /// Thread-safe by virtue of Router's @MainActor isolation — all access is on the main thread. - private var nodeObjectIDCache: [Int64: NSManagedObjectID] = [:] + private var nodeObjectIDCache: [Int64: PersistentIdentifier] = [:] /// Updates the node cache from a set of fetched nodes. Call this when the node list changes. func updateNodeIndex(from nodes: C) where C.Element: NodeInfoEntity { nodeObjectIDCache = Dictionary( - nodes.map { ($0.num, $0.objectID) }, + nodes.map { ($0.num, $0.persistentModelID) }, uniquingKeysWith: { _, new in new } ) } - /// Looks up a node using the in-memory cache for O(1) performance, falling back to a Core Data fetch. - func cachedNodeInfo(id: Int64, context: NSManagedObjectContext) -> NodeInfoEntity? { - if let objectID = nodeObjectIDCache[id] { - return try? context.existingObject(with: objectID) as? NodeInfoEntity + /// Looks up a node using the in-memory cache for O(1) performance, falling back to a SwiftData fetch. + func cachedNodeInfo(id: Int64, context: ModelContext) -> NodeInfoEntity? { + if let persistentID = nodeObjectIDCache[id] { + return context.model(for: persistentID) as? NodeInfoEntity } // Cache miss — fall back to standard fetch let node = getNodeInfo(id: id, context: context) if let node { - nodeObjectIDCache[id] = node.objectID + nodeObjectIDCache[id] = node.persistentModelID } return node } diff --git a/Meshtastic/Views/Connect/Connect.swift b/Meshtastic/Views/Connect/Connect.swift index 84e90c6e..ebe1d048 100644 --- a/Meshtastic/Views/Connect/Connect.swift +++ b/Meshtastic/Views/Connect/Connect.swift @@ -7,7 +7,7 @@ import SwiftUI import MapKit -import CoreData +import SwiftData import CoreLocation import CoreBluetooth import OSLog @@ -18,7 +18,7 @@ import ActivityKit struct Connect: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.colorScheme) private var colorScheme @State var router: Router @@ -351,8 +351,10 @@ struct Connect: View { if let deviceNum = accessoryManager.activeDeviceNum, UserDefaults.preferredPeripheralId.count > 0 && state == .subscribed { - let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() - fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", deviceNum) + var fetchNodeInfoRequest = FetchDescriptor( + predicate: #Predicate { $0.num == deviceNum } + ) + fetchNodeInfoRequest.fetchLimit = 1 do { node = try context.fetch(fetchNodeInfoRequest).first @@ -373,8 +375,8 @@ struct Connect: View { liveActivityStarted = true // 15 Minutes Local Stats Interval let timerSeconds = 900 - let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 4")) - let mostRecent = localStats?.lastObject as? TelemetryEntity + let localStats = node?.telemetries.filter { $0.metricsType == 4 } + let mostRecent = localStats?.last let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName?.addingVariationSelectors ?? "unknown") @@ -445,7 +447,7 @@ struct TransportIcon: View { struct ManualConnectionMenu: View { @EnvironmentObject var accessoryManager: AccessoryManager - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context private struct IterableTransport: Identifiable { let id: UUID @@ -518,7 +520,7 @@ struct ManualConnectionMenu: View { if accessoryManager.allowDisconnect { try await accessoryManager.disconnect() } - await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false) + await PersistenceController.shared.clearDatabase(includeRoutes: false) clearNotifications() try await selectedTransport?.transport.manuallyConnect(toDevice: device) @@ -532,7 +534,7 @@ struct ManualConnectionMenu: View { } struct DeviceConnectRow: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @State var presentingSwitchPreferredPeripheral = false let device: Device @@ -599,7 +601,7 @@ struct DeviceConnectRow: View { if accessoryManager.allowDisconnect { try await accessoryManager.disconnect() } - await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false) + await PersistenceController.shared.clearDatabase(includeRoutes: false) clearNotifications() try await accessoryManager.connect(to: device) diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index dfd24f04..0c1e981a 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -10,14 +10,14 @@ import Charts struct BatteryGauge: View { - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity private let minValue = 0.0 private let maxValue = 100.00 var body: some View { - let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) - let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + let deviceMetrics = node.telemetries.filter { $0.metricsType == 0 } + let mostRecent = deviceMetrics.last // For VoiceOver purposes, detect when device is plugged in (battery > 100%) let isPluggedIn = (mostRecent?.batteryLevel ?? 0) > 100 // Use a capped battery level for UI display diff --git a/Meshtastic/Views/Helpers/ChannelLock.swift b/Meshtastic/Views/Helpers/ChannelLock.swift index facc07cb..e3587a2f 100644 --- a/Meshtastic/Views/Helpers/ChannelLock.swift +++ b/Meshtastic/Views/Helpers/ChannelLock.swift @@ -8,7 +8,7 @@ import SwiftUI struct ChannelLock: View { - @ObservedObject var channel: ChannelEntity + @Bindable var channel: ChannelEntity var body: some View { /// Unencrypted - using no key at all or a known 1 byte key if channel.psk?.hexDescription.count ?? 0 < 3 { @@ -33,14 +33,16 @@ struct ChannelLock: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let encryptedChannel = ChannelEntity(context: context) + let encryptedChannel = ChannelEntity() encryptedChannel.psk = Data([0x01, 0x02, 0x03, 0x04]) - let unencryptedChannel = ChannelEntity(context: context) + let unencryptedChannel = ChannelEntity() unencryptedChannel.psk = Data() - return HStack(spacing: 16) { + HStack(spacing: 16) { ChannelLock(channel: encryptedChannel) ChannelLock(channel: unencryptedChannel) } } +*/ diff --git a/Meshtastic/Views/Helpers/CircleText.swift b/Meshtastic/Views/Helpers/CircleText.swift index f51eeb73..877e291e 100644 --- a/Meshtastic/Views/Helpers/CircleText.swift +++ b/Meshtastic/Views/Helpers/CircleText.swift @@ -4,7 +4,7 @@ A view draws a circle in the background of the shortName text */ import SwiftUI -import CoreData +import SwiftData struct CircleText: View { var text: String diff --git a/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift b/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift index 2173c5c5..3937f754 100644 --- a/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift +++ b/Meshtastic/Views/Helpers/Messages/MessageTemplate.swift @@ -38,13 +38,12 @@ struct MessageTemplate: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test User" user.shortName = "TU" - let message = MessageEntity(context: context) + let message = MessageEntity() message.messagePayload = "Hello, World!" message.messageTimestamp = Int32(Date().timeIntervalSince1970) message.replyID = 0 - return MessageTemplate(user: user, message: message) + MessageTemplate(user: user, message: message) } diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index bf3c2752..01f0fba9 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -6,12 +6,12 @@ // import SwiftUI -import CoreData +import SwiftData import OSLog struct ChannelList: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Binding var node: NodeInfoEntity? @Binding var channelSelection: ChannelEntity? @@ -22,11 +22,8 @@ struct ChannelList: View { var restrictedChannels = ["gpio", "mqtt", "serial", "admin"] - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)], - predicate: nil, - animation: .default - ) private var channels: FetchedResults + @Query(sort: \ChannelEntity.index) + private var channels: [ChannelEntity] @ViewBuilder private func makeChannelRow( @@ -108,72 +105,74 @@ struct ChannelList: View { } } } + @ViewBuilder + private func makeChannelListItem( + node: NodeInfoEntity, + myInfo: MyInfoEntity, + channel: ChannelEntity + ) -> some View { + let hasMessages = channel.mostRecentPrivateMessage != nil + makeChannelRow(myInfo: myInfo, channel: channel) + .alignmentGuide(.listRowSeparatorLeading) { + $0[.leading] + } + .frame(height: 62) + .contextMenu { + if hasMessages { + Button(role: .destructive) { + isPresentingDeleteChannelMessagesConfirm = true + channelToDeleteMessages = channel + } label: { + Label("Delete Messages", systemImage: "trash") + } + } + Button { + channel.mute.toggle() + Task { + do { + _ = try await accessoryManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!) + Task { @MainActor in + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("šŸ’„ Save Channel Mute Error") + } + } + } catch { + Logger.mesh.error("Unable to save channel") + } + } + } label: { + Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash") + } + } + .confirmationDialog( + "This conversation will be deleted.", + isPresented: $isPresentingDeleteChannelMessagesConfirm, + titleVisibility: .visible + ) { + Button(role: .destructive) { + Task { + await MeshPackets.shared.deleteChannelMessages(channel: channelToDeleteMessages!) + await MainActor.run { + channelToDeleteMessages = nil + } + } + } label: { + Text("Delete") + } + } + } + var body: some View { VStack { // Display Contacts for the rest of the non admin channels if let node, let myInfo = node.myInfo { List(selection: $channelSelection) { ForEach(channels) { (channel: ChannelEntity) in - let hasMessages = channel.mostRecentPrivateMessage != nil if !restrictedChannels.contains(channel.name?.lowercased() ?? "") { - makeChannelRow(myInfo: myInfo, channel: channel) - .alignmentGuide(.listRowSeparatorLeading) { - $0[.leading] - } - .frame(height: 62) - .contextMenu { - if hasMessages { - Button(role: .destructive) { - isPresentingDeleteChannelMessagesConfirm = true - channelToDeleteMessages = channel - } label: { - Label("Delete Messages", systemImage: "trash") - } - } - Button { - channel.mute.toggle() - do { - Task { - do { - _ = try await accessoryManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!) - Task { @MainActor in - do { - context.refresh(channel, mergeChanges: true) - try context.save() - } catch { - context.rollback() - Logger.data.error("šŸ’„ Save Channel Mute Error") - } - } - } catch { - Logger.mesh.error("Unable to save channel") - } - } - } - } label: { - Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash") - } - } - .confirmationDialog( - "This conversation will be deleted.", - isPresented: $isPresentingDeleteChannelMessagesConfirm, - titleVisibility: .visible - ) { - Button(role: .destructive) { - Task { - await MeshPackets.shared.deleteChannelMessages(channel: channelToDeleteMessages!) - await MainActor.run { - context.refresh(channel, mergeChanges: true) - context.refresh(myInfo, mergeChanges: true) - - // Reset state - channelToDeleteMessages = nil - } - } - } label: { - Text("Delete") - } - } + makeChannelListItem(node: node, myInfo: myInfo, channel: channel) } } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index a1c70b89..b467bce1 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -5,7 +5,7 @@ // Created by Garth Vander Houwen on 12/24/21. // -import CoreData +import SwiftData import MeshtasticProtobufs import OSLog import SwiftUI @@ -13,31 +13,28 @@ import SwiftUI struct ChannelMessageList: View { @EnvironmentObject var appState: AppState @Environment(\.scenePhase) var scenePhase - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @FocusState var messageFieldFocused: Bool - @ObservedObject var myInfo: MyInfoEntity - @ObservedObject var channel: ChannelEntity + @Bindable var myInfo: MyInfoEntity + @Bindable var channel: ChannelEntity @State private var replyMessageId: Int64 = 0 @State private var redrawTapbacksTrigger = UUID() @AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1 @State private var messageToHighlight: Int64 = 0 - @FetchRequest private var allPrivateMessages: FetchedResults + @Query private var allPrivateMessages: [MessageEntity] init(myInfo: MyInfoEntity, channel: ChannelEntity) { self.myInfo = myInfo self.channel = channel - // Configure fetch request here - let request: NSFetchRequest = MessageEntity.fetchRequest() - request.sortDescriptors = [ - NSSortDescriptor(keyPath: \MessageEntity.messageTimestamp, ascending: true) - ] - request.predicate = NSPredicate( - format: "channel == %ld AND toUser == nil AND isEmoji == false", - channel.index + let channelIndex = channel.index + _allPrivateMessages = Query( + filter: #Predicate { + $0.channel == channelIndex && $0.toUser == nil && $0.isEmoji == false + }, + sort: \MessageEntity.messageTimestamp ) - _allPrivateMessages = FetchRequest(fetchRequest: request) } func handleInteractionComplete() { @@ -53,7 +50,6 @@ struct ChannelMessageList: View { try context.save() Logger.data.info("šŸ“– [App] All unread messages marked as read.") appState.unreadChannelMessages = myInfo.unreadMessages - context.refresh(myInfo, mergeChanges: true) } catch { Logger.data.error("Failed to read messages: \(error.localizedDescription, privacy: .public)") } diff --git a/Meshtastic/Views/Messages/ChannelMessageRow.swift b/Meshtastic/Views/Messages/ChannelMessageRow.swift index 8a02a80f..a79d3dbe 100644 --- a/Meshtastic/Views/Messages/ChannelMessageRow.swift +++ b/Meshtastic/Views/Messages/ChannelMessageRow.swift @@ -1,4 +1,4 @@ -import CoreData +import SwiftData import MeshtasticProtobufs import SwiftUI @@ -6,9 +6,9 @@ struct ChannelMessageRow: View { @EnvironmentObject var appState: AppState // Core Data object observed for changes (like Tapbacks being received) - @ObservedObject var message: MessageEntity + @Bindable var message: MessageEntity - let allMessages: FetchedResults // The full list for reply lookup + let allMessages: [MessageEntity] // The full list for reply lookup let previousMessage: MessageEntity? let preferredPeripheralNum: Int let channel: ChannelEntity @@ -24,7 +24,7 @@ struct ChannelMessageRow: View { } init(message: MessageEntity, - allMessages: FetchedResults, + allMessages: [MessageEntity], previousMessage: MessageEntity?, preferredPeripheralNum: Int, channel: ChannelEntity, @@ -34,7 +34,7 @@ struct ChannelMessageRow: View { scrollView: ScrollViewProxy, onInteractionComplete: @escaping () -> Void) { // Initialize ObservedObject with the concrete instance - self._message = ObservedObject(initialValue: message) + self.message = message self.allMessages = allMessages self.previousMessage = previousMessage self.preferredPeripheralNum = preferredPeripheralNum diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index 42f54da2..d0c30054 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -1,9 +1,9 @@ import SwiftUI -import CoreData +import SwiftData import OSLog struct MessageContextMenuItems: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager let message: MessageEntity @@ -148,7 +148,7 @@ struct MessageContextMenuItems: View { } private extension MessageDestination { - var managedObject: NSManagedObject { + var persistentModel: any PersistentModel { switch self { case let .user(user): return user case let .channel(channel): return channel diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 6343b91a..635bb103 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -20,7 +20,7 @@ struct MessageText: View { ) static let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss:a") static let timeFormatString = (localeTimeFormat ?? "j:mm:ss:a") - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager let message: MessageEntity @@ -284,10 +284,10 @@ struct MessageText: View { ) await MainActor.run { switch tapBackDestination { - case let .channel(channel): - context.refresh(channel, mergeChanges: true) - case let .user(user): - context.refresh(user, mergeChanges: true) + case .channel: + break + case .user: + break } } } catch { diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index 2f4e2950..56cd7a45 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -6,13 +6,13 @@ // import SwiftUI -import CoreData +import SwiftData import OSLog import TipKit struct Messages: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @Environment(\.colorScheme) private var colorScheme @ObservedObject var router: Router @Binding var unreadChannelMessages: Int @@ -116,10 +116,7 @@ struct Messages: View { switch state { case .channels(channelId: let channelId, messageId: _): if let channelId { - channelSelection = node?.myInfo?.channels?.first(where: { channel in - guard let channel = channel as? ChannelEntity else { return false } - return channel.id == channelId - }) as? ChannelEntity + channelSelection = node?.myInfo?.channels.first { $0.id == channelId } } else { channelSelection = nil userSelection = nil diff --git a/Meshtastic/Views/Messages/RetryButton.swift b/Meshtastic/Views/Messages/RetryButton.swift index ab48072b..494b4f35 100644 --- a/Meshtastic/Views/Messages/RetryButton.swift +++ b/Meshtastic/Views/Messages/RetryButton.swift @@ -2,7 +2,7 @@ import SwiftUI import OSLog struct RetryButton: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager let message: MessageEntity @@ -48,7 +48,6 @@ struct RetryButton: View { // to messages is via a weak fetched property which is not updated by // `bleManager.sendMessage` unlike the user entity. Task { @MainActor in - context.refresh(channel, mergeChanges: true) } } } catch { diff --git a/Meshtastic/Views/Messages/TapbackResponses.swift b/Meshtastic/Views/Messages/TapbackResponses.swift index b46e65f3..bd5c5e2f 100644 --- a/Meshtastic/Views/Messages/TapbackResponses.swift +++ b/Meshtastic/Views/Messages/TapbackResponses.swift @@ -2,7 +2,7 @@ import SwiftUI import OSLog struct TapbackResponses: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context let message: MessageEntity let onRead: () -> Void diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index ba07a9bf..28b7d034 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -6,13 +6,13 @@ // import SwiftUI -import CoreData +import SwiftData import OSLog import TipKit struct UserList: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @State private var editingFilters = false @State private var showingHelp = false @@ -69,30 +69,28 @@ struct UserList: View { fileprivate struct FilteredUserList: View { @EnvironmentObject var accessoryManager: AccessoryManager - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context - @FetchRequest private var users: FetchedResults + @Query(sort: [SortDescriptor(\UserEntity.lastMessage, order: .reverse), + SortDescriptor(\UserEntity.longName)]) + private var allUsers: [UserEntity] @Binding var userSelection: UserEntity? @Binding var node: NodeInfoEntity? @State private var isPresentingDeleteUserMessagesConfirm: Bool = false @State private var userToDeleteMessages: UserEntity? + private var filters: NodeFilterParameters init(withFilters: NodeFilterParameters, node: Binding, userSelection: Binding) { - let request: NSFetchRequest = UserEntity.fetchRequest() - request.sortDescriptors = [ - NSSortDescriptor(key: "lastMessage", ascending: false), - NSSortDescriptor(key: "userNode.favorite", ascending: false), - NSSortDescriptor(key: "pkiEncrypted", ascending: false), - NSSortDescriptor(key: "userNode.lastHeard", ascending: false), - NSSortDescriptor(key: "longName", ascending: true) - ] - request.predicate = withFilters.buildPredicate() - self._users = FetchRequest(fetchRequest: request) + self.filters = withFilters self._node = node self._userSelection = userSelection } + private var users: [UserEntity] { + allUsers.filter { filters.matches(user: $0) } + } + var body: some View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY") @@ -188,7 +186,6 @@ fileprivate struct FilteredUserList: View { Logger.data.info("Unfavorited a node") } } - context.refresh(user, mergeChanges: true) do { try context.save() } catch { @@ -226,7 +223,6 @@ fileprivate struct FilteredUserList: View { Button(role: .destructive) { Task { await MeshPackets.shared.deleteUserMessages(user: userToDeleteMessages!) - context.refresh(node!.user!, mergeChanges: true) } } label: { Text("Delete") @@ -239,6 +235,84 @@ fileprivate struct FilteredUserList: View { } } fileprivate extension NodeFilterParameters { + func matches(user: UserEntity) -> Bool { + // Search text + if !searchText.isEmpty { + let text = searchText.lowercased() + let matchesSearch = [user.userId, user.numString, user.hwModel, user.hwDisplayName, user.longName, user.shortName] + .compactMap { $0?.lowercased() } + .contains { $0.contains(text) } + if !matchesSearch { return false } + } + // Mqtt and lora + if !(viaLora && viaMqtt) { + if viaLora { + if user.userNode?.viaMqtt == true { return false } + } else { + if user.userNode?.viaMqtt != true { return false } + } + } + // Roles + if roleFilter && !deviceRoles.isEmpty { + let userRole = Int(user.role) + if !deviceRoles.contains(userRole) { return false } + } + // Hops Away + if hopsAway == 0 { + if user.userNode?.hopsAway != 0 { return false } + } else if hopsAway > -1 { + let nodeHops = user.userNode?.hopsAway ?? 0 + if nodeHops <= 0 || nodeHops > Int32(hopsAway) { return false } + } + // Online + if isOnline { + let twoHoursAgo = Calendar.current.date(byAdding: .minute, value: -120, to: Date()) ?? Date.distantPast + if let lastHeard = user.userNode?.lastHeard, lastHeard < twoHoursAgo { return false } + if user.userNode?.lastHeard == nil { return false } + } + // Encrypted + if isPkiEncrypted { + if !user.pkiEncrypted { return false } + } + // Favorites + if isFavorite { + if user.userNode?.favorite != true { return false } + } + // Distance + if distanceFilter { + if let poi = LocationsHandler.currentLocation, + poi.latitude != LocationsHandler.DefaultLocation.latitude, + poi.longitude != LocationsHandler.DefaultLocation.longitude { + let d = maxDistance * 1.1 + let r: Double = 6371009 + let meanLat = poi.latitude * .pi / 180 + let deltaLat = d / r * 180 / .pi + let deltaLon = d / (r * cos(meanLat)) * 180 / .pi + let minLat = poi.latitude - deltaLat + let maxLat = poi.latitude + deltaLat + let minLon = poi.longitude - deltaLon + let maxLon = poi.longitude + deltaLon + let hasNearbyPosition = (user.userNode?.positions ?? []).contains { pos in + guard pos.latest else { return false } + let lon = Double(pos.longitudeI) / 1e7 + let lat = Double(pos.latitudeI) / 1e7 + return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat + } + if !hasNearbyPosition { return false } + } + } + // Unmessagable filter + if user.unmessagable { + let hasMessages = !(user.receivedMessages ?? []).isEmpty || !(user.sentMessages ?? []).isEmpty + if !hasMessages { return false } + } + // Ignored + if user.userNode?.ignored == true { return false } + // Connected node + if user.numString == String(UserDefaults.preferredPeripheralNum) { return false } + return true + } + func buildPredicate() -> NSPredicate? { var predicates: [NSPredicate] = [] // Search text predicates diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index dc417565..90c48620 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CoreData +import SwiftData import OSLog import MeshtasticProtobufs // Added to ensure RoutingError is accessible if needed @@ -14,21 +14,19 @@ struct UserMessageList: View { @EnvironmentObject var appState: AppState @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.scenePhase) var scenePhase - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @FocusState var messageFieldFocused: Bool - @ObservedObject var user: UserEntity + @Bindable var user: UserEntity @State private var replyMessageId: Int64 = 0 @State private var messageToHighlight: Int64 = 0 @State private var redrawTapbacksTrigger = UUID() @AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1 - @FetchRequest private var allPrivateMessages: FetchedResults - - init(user: UserEntity) { - self.user = user - - // Configure fetch request here - let request: NSFetchRequest = user.messageFetchRequest - _allPrivateMessages = FetchRequest(fetchRequest: request) + private var allPrivateMessages: [MessageEntity] { + let sent = user.sentMessages ?? [] + let received = user.receivedMessages ?? [] + return (sent + received) + .filter { !$0.isEmoji } + .sorted { $0.messageTimestamp < $1.messageTimestamp } } func handleInteractionComplete() { @@ -49,8 +47,6 @@ struct UserMessageList: View { let connectedUser = connectedNode.user { appState.unreadDirectMessages = connectedUser.unreadMessages(context: context, skipLastMessageCheck: true) // skipLastMessageCheck=true because we don't update lastMessage on our own connected node } - - context.refresh(user, mergeChanges: true) } catch { Logger.data.error("Failed to read direct messages: \(error.localizedDescription, privacy: .public)") } diff --git a/Meshtastic/Views/Messages/UserMessageRow.swift b/Meshtastic/Views/Messages/UserMessageRow.swift index 9f5f50eb..6d96285d 100644 --- a/Meshtastic/Views/Messages/UserMessageRow.swift +++ b/Meshtastic/Views/Messages/UserMessageRow.swift @@ -5,14 +5,14 @@ //Ā  Copyright(c) Garth Vander Houwen 10/1/2025 // -import CoreData +import SwiftData import MeshtasticProtobufs import SwiftUI struct UserMessageRow: View { @EnvironmentObject var appState: AppState - @ObservedObject var message: MessageEntity + @Bindable var message: MessageEntity let allMessages: [MessageEntity] let previousMessage: MessageEntity? let preferredPeripheralNum: Int @@ -38,7 +38,7 @@ struct UserMessageRow: View { scrollView: ScrollViewProxy, onInteractionComplete: @escaping () -> Void) { // Initialize ObservedObject with the concrete instance - self._message = ObservedObject(initialValue: message) + self.message = message self.allMessages = allMessages self.previousMessage = previousMessage self.preferredPeripheralNum = preferredPeripheralNum diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index ae57cab0..8157580e 100644 --- a/Meshtastic/Views/Nodes/DetectionSensorLog.swift +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -6,20 +6,21 @@ // import SwiftUI +import SwiftData import Charts import MeshtasticProtobufs import OSLog struct DetectionSensorLog: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @State private var isPresentingClearLogConfirm: Bool = false @State var isExporting = false @State var exportString = "" - @ObservedObject var node: NodeInfoEntity - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "messageTimestamp", ascending: false)], - predicate: NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue)), animation: .none) - private var detections: FetchedResults + @Bindable var node: NodeInfoEntity + @Query(filter: #Predicate { $0.portNum == 10 }, + sort: \MessageEntity.messageTimestamp, order: .reverse) + private var detections: [MessageEntity] var body: some View { let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date()) @@ -142,15 +143,17 @@ struct DetectionSensorLog: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" node.user = user - return DetectionSensorLog(node: node) + DetectionSensorLog(node: node) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } +*/ diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 09bca3e7..0bdea154 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -10,7 +10,7 @@ import OSLog struct DeviceMetricsLog: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @@ -21,7 +21,7 @@ struct DeviceMetricsLog: View { @State private var batteryChartColor: Color = .blue @State private var airtimeChartColor: Color = .yellow @State private var channelUtilizationChartColor: Color = .green - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity @State private var sortOrder = [KeyPathComparator(\TelemetryEntity.time, order: .reverse)] @State private var selection: TelemetryEntity.ID? @State private var chartSelection: Date? @@ -30,7 +30,7 @@ struct DeviceMetricsLog: View { VStack { if node.hasDeviceMetrics { let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) - let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).reversed() as? [TelemetryEntity] ?? [] + let deviceMetrics = node.telemetries.filter { $0.metricsType == 0 }.reversed() let chartData = deviceMetrics .filter { $0.time != nil && $0.time! >= oneWeekAgo! } .sorted { $0.time! < $1.time! } @@ -255,15 +255,17 @@ struct DeviceMetricsLog: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" node.user = user - return DeviceMetricsLog(node: node) + DeviceMetricsLog(node: node) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } +*/ diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index ca19ebee..c9a4002f 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -7,33 +7,27 @@ import SwiftUI import Charts import OSLog -import CoreData +import SwiftData struct EnvironmentMetricsLog: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @State private var isPresentingClearLogConfirm: Bool = false @State var isExporting = false @State var exportString = "" - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity @StateObject var columnList = MetricsColumnList.environmentDefaultColumns @StateObject var seriesList = MetricsSeriesList.environmentDefaultChartSeries @State var isEditingColumnConfiguration = false - @FetchRequest private var chartData: FetchedResults - - init(node: NodeInfoEntity) { - self.node = node - - // Build fetch request: - let request: NSFetchRequest = TelemetryEntity.fetchRequest() + private var chartData: [TelemetryEntity] { let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date.distantPast - request.predicate = NSPredicate(format: "nodeTelemetry == %@ AND metricsType == 1 AND time >= %@", node, oneWeekAgo as NSDate) - request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] - _chartData = FetchRequest(fetchRequest: request) + return (node.telemetries ?? []) + .filter { $0.metricsType == 1 && ($0.time ?? Date.distantPast) >= oneWeekAgo } + .sorted { ($0.time ?? .distantPast) > ($1.time ?? .distantPast) } } var body: some View { @@ -187,15 +181,17 @@ struct EnvironmentMetricsLog: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" node.user = user - return EnvironmentMetricsLog(node: node) + EnvironmentMetricsLog(node: node) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } +*/ diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift index 020855e7..d43d54be 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift @@ -41,12 +41,14 @@ struct ClientHistoryButton: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let connectedNode = NodeInfoEntity(context: context) + let connectedNode = NodeInfoEntity() connectedNode.num = 987654321 - return ClientHistoryButton(connectedNode: connectedNode, node: node) + ClientHistoryButton(connectedNode: connectedNode, node: node) .environmentObject(AccessoryManager.shared) } +*/ diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift index a7b096b3..a5d291c9 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift @@ -1,10 +1,10 @@ -import CoreData +import SwiftData import OSLog import SwiftUI struct DeleteNodeButton: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager var connectedNode: NodeInfoEntity @@ -65,13 +65,15 @@ struct DeleteNodeButton: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let connectedNode = NodeInfoEntity(context: context) + let connectedNode = NodeInfoEntity() connectedNode.num = 987654321 - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - return DeleteNodeButton(connectedNode: connectedNode, node: node) + DeleteNodeButton(connectedNode: connectedNode, node: node) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } +*/ diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift index c71e6b87..18f37e5f 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift @@ -1,4 +1,4 @@ -import CoreData +import SwiftData import SwiftUI struct ExchangePositionsButton: View { @@ -62,12 +62,14 @@ struct ExchangePositionsButton: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let connectedNode = NodeInfoEntity(context: context) + let connectedNode = NodeInfoEntity() connectedNode.num = 987654321 - return ExchangePositionsButton(node: node, connectedNode: connectedNode) + ExchangePositionsButton(node: node, connectedNode: connectedNode) .environmentObject(AccessoryManager.shared) } +*/ diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift index 595eec4f..e9de11eb 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangeUserInfoButton.swift @@ -1,4 +1,4 @@ -import CoreData +import SwiftData import SwiftUI import OSLog @@ -60,12 +60,14 @@ struct ExchangeUserInfoButton: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let connectedNode = NodeInfoEntity(context: context) + let connectedNode = NodeInfoEntity() connectedNode.num = 987654321 - return ExchangeUserInfoButton(node: node, connectedNode: connectedNode) + ExchangeUserInfoButton(node: node, connectedNode: connectedNode) .environmentObject(AccessoryManager.shared) } +*/ diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift index d3a54864..1dcb9e90 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift @@ -1,13 +1,13 @@ -import CoreData +import SwiftData import OSLog import SwiftUI struct FavoriteNodeButton: View { @EnvironmentObject var accessoryManager: AccessoryManager - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity @State var isShowingClientBaseConfirmation = false var body: some View { @@ -80,15 +80,17 @@ struct FavoriteNodeButton: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" node.user = user - return FavoriteNodeButton(node: node) + FavoriteNodeButton(node: node) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } +*/ diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift index 51a8801b..a3d0bf12 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/IgnoreNodeButton.swift @@ -1,12 +1,12 @@ -import CoreData +import SwiftData import OSLog import SwiftUI struct IgnoreNodeButton: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager - @ObservedObject + @Bindable var node: NodeInfoEntity var body: some View { @@ -52,11 +52,13 @@ struct IgnoreNodeButton: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - return IgnoreNodeButton(node: node) + IgnoreNodeButton(node: node) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } +*/ diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift index ec130098..1dcb811b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/NavigateToButton.swift @@ -7,7 +7,7 @@ import SwiftUI import CoreLocation -import CoreData +import SwiftData import OSLog struct NavigateToButton: View { @@ -21,11 +21,13 @@ struct NavigateToButton: View { } Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum, privacy: .public)") - let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") - fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum)) + var descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == userNum } + ) + descriptor.fetchLimit = 1 do { - let fetchedNodes = try PersistenceController.shared.container.viewContext.fetch(fetchRequest) + let fetchedNodes = try PersistenceController.shared.context.fetch(descriptor) guard let nodeInfo = fetchedNodes.first else { Logger.services.error("NavigateToAction: Node with userNum \(userNum, privacy: .public) not found in Core Data") return @@ -55,14 +57,16 @@ struct NavigateToButton: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" user.num = 123456789 node.user = user - return NavigateToButton(node: node) + NavigateToButton(node: node) } +*/ diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift index 179fb1af..bcc6381d 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift @@ -1,20 +1,19 @@ -import CoreData +import SwiftData import OSLog import SwiftUI struct NodeAlertsButton: View { - var context: NSManagedObjectContext + var context: ModelContext - @ObservedObject + @Bindable var node: NodeInfoEntity - @ObservedObject + @Bindable var user: UserEntity var body: some View { Button { user.mute = !user.mute - context.refresh(node, mergeChanges: true) do { try context.save() } catch { @@ -32,13 +31,15 @@ struct NodeAlertsButton: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" node.user = user - return NodeAlertsButton(context: context, node: node, user: user) + NodeAlertsButton(context: context, node: node, user: user) } +*/ diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift index 62e696d4..91eba72e 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift @@ -44,14 +44,16 @@ struct TraceRouteButton: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" node.user = user - return TraceRouteButton(node: node) + TraceRouteButton(node: node) .environmentObject(AccessoryManager.shared) } +*/ diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 39e42baa..540ef0ca 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -6,6 +6,7 @@ // import SwiftUI +import SwiftData import MapKit import CoreLocation import OSLog @@ -40,15 +41,16 @@ struct MeshMapContent: MapContent { @AppStorage("mapOverlaysEnabled") private var showMapOverlays = false @Binding var enabledOverlayConfigs: Set - @FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn) - var positions: FetchedResults - - @FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none) - var waypoints: FetchedResults - - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], - predicate: NSPredicate(format: "enabled == true", ""), animation: .none) - private var routes: FetchedResults + @Query(filter: #Predicate { $0.nodePosition != nil && $0.latest == true }, + sort: \PositionEntity.time, order: .reverse) + var positions: [PositionEntity] + + @Query(sort: \WaypointEntity.name, order: .reverse) + var waypoints: [WaypointEntity] + + @Query(filter: #Predicate { $0.enabled == true }, + sort: \RouteEntity.name) + private var routes: [RouteEntity] @MapContentBuilder var positionAnnotations: some MapContent { @@ -57,10 +59,10 @@ struct MeshMapContent: MapContent { if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) { let coordinateForNodePin: CLLocationCoordinate2D = if position.isPreciseLocation { // Precise location: place node pin at actual location. - position.coordinate + position.nodeCoordinate ?? LocationsHandler.DefaultLocation } else { // Imprecise location: fuzz slightly so overlapping nodes are visible and clickable at highest zoom levels. - position.fuzzedCoordinate + position.fuzzedNodeCoordinate ?? LocationsHandler.DefaultLocation } if 12...15 ~= position.precisionBits || position.precisionBits == 32 { @@ -131,7 +133,8 @@ struct MeshMapContent: MapContent { @MapContentBuilder var routeAnnotations: some MapContent { ForEach(routes) { route in - if let routeLocations = route.locations, let locations = Array(routeLocations) as? [LocationEntity] { + if !route.locations.isEmpty { + let locations = route.locations let routeCoords = locations.compactMap {(loc) -> CLLocationCoordinate2D in return loc.locationCoordinate ?? LocationsHandler.DefaultLocation } @@ -167,7 +170,7 @@ struct MeshMapContent: MapContent { var waypointAnnotations: some MapContent { if waypoints.count > 0, showWaypoints, let waypoints = Array(waypoints) as? [WaypointEntity] { ForEach(waypoints, id: \.self) { waypoint in - Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { + Annotation(waypoint.name ?? "?", coordinate: waypoint.mapCoordinate) { LazyVStack { ZStack { CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "šŸ“"), color: Color.orange, circleSize: 40) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift index 02635dd8..95cbf3ef 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift @@ -6,11 +6,11 @@ // import SwiftUI import MapKit -import CoreData +import SwiftData struct NodeMapContent: MapContent { - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity /// Map State User Defaults @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false @@ -22,7 +22,7 @@ struct NodeMapContent: MapContent { @MapContentBuilder var nodeMap: some MapContent { - let positionArray = node.positions?.array as? [PositionEntity] ?? [] + let positionArray = node.positions /// Node Color from node.num let nodeColor = UIColor(hex: UInt32(node.num)) @@ -43,7 +43,7 @@ struct NodeMapContent: MapContent { let pp = PositionPrecision(rawValue: Int(position.precisionBits)) let radius: CLLocationDistance = pp?.precisionMeters ?? 0 if radius > 0.0 { - MapCircle(center: position.coordinate, radius: radius) + MapCircle(center: position.nodeCoordinate ?? LocationsHandler.DefaultLocation, radius: radius) .foregroundStyle(Color(nodeColor).opacity(0.25)) .stroke(.white, lineWidth: 2) } @@ -51,7 +51,7 @@ struct NodeMapContent: MapContent { /// Lastest Position Pin if position.latest { /// Node Annotations - Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { + Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.nodeCoordinate ?? LocationsHandler.DefaultLocation) { LazyVStack { ZStack { if pf.contains(.Heading) { @@ -100,7 +100,7 @@ struct NodeMapContent: MapContent { // Having showNodeHistory enabled can be quite slow if there are thousands of history points. if position.latest == false && node.favorite { let headingDegrees = Angle.degrees(Double(position.heading)) - Annotation("", coordinate: position.coordinate) { + Annotation("", coordinate: position.nodeCoordinate ?? LocationsHandler.DefaultLocation) { if pf.contains(.Heading) { Image(uiImage: prerenderedHistoryPointArrowImage) .renderingMode(.original) @@ -154,7 +154,7 @@ struct NodeMapContent: MapContent { @MapContentBuilder var body: some MapContent { - if node.positions?.count ?? 0 > 0 { + if node.positions.count > 0 { nodeMap } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapDataFiles.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapDataFiles.swift index c39f3ef6..dff3e143 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapDataFiles.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapDataFiles.swift @@ -3,7 +3,7 @@ import UniformTypeIdentifiers import OSLog struct MapDataFiles: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @ObservedObject private var mapDataManager = MapDataManager.shared diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 09a08e34..d9a99592 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -6,6 +6,7 @@ // import SwiftUI +import SwiftData import CoreLocation import MapKit @@ -30,10 +31,10 @@ private struct NodeMapContentEquatableWrapper: View, Equatable { } struct NodeMapSwiftUI: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager /// Parameters - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity @State var showUserLocation: Bool = false @State var positions: [PositionEntity] = [] /// Map State User Defaults @@ -58,11 +59,8 @@ struct NodeMapSwiftUI: View { @State private var mapRegion = MKCoordinateRegion.init() - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], - predicate: NSPredicate( - format: "expire == nil || expire >= %@", Date() as NSDate - ), animation: .none) - private var waypoints: FetchedResults + @Query(sort: \WaypointEntity.name, order: .reverse) + private var waypoints: [WaypointEntity] var body: some View { if node.hasPositions { @@ -78,7 +76,7 @@ struct NodeMapSwiftUI: View { configuredMap } } - .navigationBarTitle(String((node.user?.shortName ?? "Unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline) + .navigationBarTitle(String((node.user?.shortName ?? "Unknown".localized) + (" \(node.positions.count) points")), displayMode: .inline) .navigationBarItems(trailing: ZStack { ConnectedDevice( @@ -123,8 +121,8 @@ struct NodeMapSwiftUI: View { } private var mapContentSignature: NodeMapContentSignature { - let positionCount = node.positions?.count ?? 0 - let lastPositionTime = (node.positions?.lastObject as? PositionEntity)?.time + let positionCount = node.positions.count + let lastPositionTime = node.positions.last?.time return NodeMapContentSignature(nodeNum: node.num, positionCount: positionCount, lastPositionTime: lastPositionTime, showNodeHistory: showNodeHistory, showRouteLines: showRouteLines, showConvexHull: showConvexHull, favorite: node.favorite) } @@ -208,7 +206,7 @@ struct NodeMapSwiftUI: View { .glassButtonStyle() } - if node.positions?.count ?? 0 > 1 { + if node.positions.count > 1 { Button(action: { if isLookingAround { isLookingAround = false @@ -241,15 +239,15 @@ struct NodeMapSwiftUI: View { private func handleNodeChange() { isLookingAround = false isShowingAltitude = false - let newMostRecent = node.positions?.lastObject as? PositionEntity - if node.positions?.count ?? 0 > 1 { + let newMostRecent = node.positions.last + if node.positions.count > 1 { position = .automatic - } else if let mrCoord = newMostRecent?.coordinate { + } else if let mrCoord = newMostRecent?.nodeCoordinate { position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0)) } - if let newMostRecent { + if let newMostRecent, let coord = newMostRecent.nodeCoordinate { Task { - scene = try? await fetchScene(for: newMostRecent.coordinate) + scene = try? await fetchScene(for: coord) } } } @@ -257,13 +255,13 @@ struct NodeMapSwiftUI: View { private func handleAppear() { UIApplication.shared.isIdleTimerDisabled = true updateMapStyle(for: selectedMapLayer) - let mostRecent = node.positions?.lastObject as? PositionEntity - if node.positions?.count ?? 0 > 1 { + let mostRecent = node.positions.last + if node.positions.count > 1 { position = .automatic - } else if let mrCoord = mostRecent?.coordinate { + } else if let mrCoord = mostRecent?.nodeCoordinate { position = .camera(MapCamera(centerCoordinate: mrCoord, distance: distance, heading: 0, pitch: 0)) } - if scene == nil, let mrCoord = mostRecent?.coordinate { + if scene == nil, let mrCoord = mostRecent?.nodeCoordinate { Task { scene = try? await fetchScene(for: mrCoord) } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift index 5bde5a14..aacf10d3 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift @@ -16,16 +16,12 @@ struct PositionAltitude { struct PositionAltitudeChart: View { @Environment(\.dismiss) private var dismiss - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity @State private var lineWidth = 2.0 var data: [PositionAltitude] { let fiveYearsAgo = Calendar.current.date(byAdding: .year, value: -5, to: Date()) - guard let nodePositions = node.positions, - let positions = Array(nodePositions) as? [PositionEntity] - else { - return [] - } + let positions = node.positions let filteredPositions = positions.filter({$0.time != nil && ($0.time ?? fiveYearsAgo!) > fiveYearsAgo!}) return filteredPositions.map { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index bc40ddf3..c444fa82 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -11,7 +11,7 @@ import MapKit struct PositionPopover: View { @ObservedObject var locationsHandler = LocationsHandler.shared - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var appState: AppState private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Environment(\.dismiss) private var dismiss @@ -81,7 +81,7 @@ struct PositionPopover: View { .padding(.bottom, 5) /// Coordinate Label { - Text("\(String(format: "%.6f", position.coordinate.latitude)), \(String(format: "%.6f", position.coordinate.longitude))") + Text("\(String(format: "%.6f", position.nodeCoordinate?.latitude ?? 0)), \(String(format: "%.6f", position.nodeCoordinate?.longitude ?? 0))") .textSelection(.enabled) .foregroundColor(.primary) .font(idiom == .phone ? .callout : .body) @@ -169,7 +169,8 @@ struct PositionPopover: View { if let lastLocation = locationsHandler.locationsArray.last { /// Distance if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { - let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)) + let posCoord = position.nodeCoordinate ?? LocationsHandler.DefaultLocation + let metersAway = posCoord.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)) Label { Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") .foregroundColor(.primary) @@ -263,7 +264,7 @@ struct PositionPopover: View { .presentationBackgroundInteraction(.enabled(upThrough: .large)) .navigationDestination(isPresented: $navigateToCompass) { CompassView( - waypointLocation: position.coordinate, + waypointLocation: position.nodeCoordinate ?? LocationsHandler.DefaultLocation, waypointLongName: position.nodePosition?.user?.longName ?? "Unknown node", waypointShortName: position.nodePosition?.user?.shortName ?? "???", color: (position.nodePosition?.user?.num != nil && position.nodePosition?.user?.num != 0) ? Color(UIColor(hex: UInt32(position.nodePosition!.user!.num))) : .orange diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index 53367b7a..09556580 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -10,12 +10,12 @@ import MapKit import MeshtasticProtobufs import OSLog import SwiftUI -import CoreData +import SwiftData struct WaypointForm: View { @EnvironmentObject var accessoryManager: AccessoryManager - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @Environment(\.dismiss) private var dismiss @State var waypoint: WaypointEntity let distanceFormatter = MKDistanceFormatter() @@ -43,20 +43,20 @@ struct WaypointForm: View { Divider() Form { if let cl = LocationsHandler.currentLocation { - let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude )) + let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.mapCoordinate.latitude, longitude: waypoint.mapCoordinate.longitude )) Section(header: Text("Coordinate") ) { HStack { Text("Location:") .foregroundColor(.secondary) - Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))") + Text("\(String(format: "%.5f", waypoint.mapCoordinate.latitude) + "," + String(format: "%.5f", waypoint.mapCoordinate.longitude))") .textSelection(.enabled) .foregroundColor(.secondary) .font(.caption) } Button { - waypoint.coordinate.longitude = cl.longitude - waypoint.coordinate.latitude = cl.latitude + waypoint.longitudeI = Int32(cl.longitude * 1e7) + waypoint.latitudeI = Int32(cl.latitude * 1e7) } label: { HStack { Text("Use my Location") @@ -65,7 +65,7 @@ struct WaypointForm: View { } .accessibilityLabel("Set to current location") HStack { - if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 { + if waypoint.mapCoordinate.latitude != 0 && waypoint.mapCoordinate.longitude != 0 { DistanceText(meters: distance) .foregroundColor(Color.gray) } @@ -288,7 +288,7 @@ struct WaypointForm: View { Text(waypoint.name ?? "?") .font(.largeTitle) Spacer() - if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) { + if waypoint.locked { Image(systemName: "lock.fill") .font(.largeTitle) } else { @@ -359,7 +359,7 @@ struct WaypointForm: View { Label { Text("Coordinates:") .foregroundColor(.primary) - Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))") + Text("\(String(format: "%.6f", waypoint.mapCoordinate.latitude)), \(String(format: "%.6f", waypoint.mapCoordinate.longitude))") .textSelection(.enabled) .foregroundColor(.secondary) .font(.caption2) @@ -369,7 +369,7 @@ struct WaypointForm: View { .padding(.bottom) // Drop Maps Pin Button(action: { - if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") { + if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.mapCoordinate.latitude),\(waypoint.mapCoordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") { UIApplication.shared.open(url) } }) { @@ -410,7 +410,7 @@ struct WaypointForm: View { /// Distance if let cl = LocationsHandler.currentLocation { if cl.distance(from: cl) > 0.0 { - let metersAway = waypoint.coordinate.distance(from: cl) + let metersAway = waypoint.mapCoordinate.distance(from: cl) Label { Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") .foregroundColor(.primary) @@ -479,9 +479,8 @@ struct WaypointForm: View { } else { expires = false } - if waypoint.locked > 0 { + if waypoint.locked { locked = true - lockedTo = waypoint.locked } } else { name = "" @@ -490,8 +489,8 @@ struct WaypointForm: View { expires = false expire = Date.now.addingTimeInterval(60 * 480) icon = "šŸ“" - latitude = waypoint.coordinate.latitude - longitude = waypoint.coordinate.longitude + latitude = waypoint.mapCoordinate.latitude + longitude = waypoint.mapCoordinate.longitude } } .presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85))) @@ -501,12 +500,14 @@ struct WaypointForm: View { private func fetchNodeInfo() async { // --- Fetch createdBy node --- if waypoint.createdBy != 0 { - let createdByFetch: NSFetchRequest = NodeInfoEntity.fetchRequest() - createdByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.createdBy)) - createdByFetch.fetchLimit = 1 + let createdByNum = Int64(waypoint.createdBy) + var createdByDescriptor = FetchDescriptor( + predicate: #Predicate { $0.num == createdByNum } + ) + createdByDescriptor.fetchLimit = 1 do { - let nodes = try context.fetch(createdByFetch) + let nodes = try context.fetch(createdByDescriptor) createdByNode = nodes.first } catch { Logger.services.warning("Error fetching createdBy node: \(error.localizedDescription)") @@ -516,12 +517,14 @@ struct WaypointForm: View { // --- Fetch lastUpdatedBy node (only if different from createdBy) --- if waypoint.lastUpdatedBy != 0, waypoint.lastUpdatedBy != waypoint.createdBy { - let updatedByFetch: NSFetchRequest = NodeInfoEntity.fetchRequest() - updatedByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.lastUpdatedBy)) - updatedByFetch.fetchLimit = 1 + let updatedByNum = Int64(waypoint.lastUpdatedBy) + var updatedByDescriptor = FetchDescriptor( + predicate: #Predicate { $0.num == updatedByNum } + ) + updatedByDescriptor.fetchLimit = 1 do { - let nodes = try context.fetch(updatedByFetch) + let nodes = try context.fetch(updatedByDescriptor) lastUpdatedByNode = nodes.first } catch { Logger.services.warning("Error fetching lastUpdatedBy node: \(error.localizedDescription)") diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index fcfad87d..98e1c8a5 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -19,13 +19,13 @@ struct NodeDetail: View { var modemPreset: ModemPresets = ModemPresets( rawValue: UserDefaults.modemPreset ) ?? ModemPresets.longFast - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false @State private var dateFormatRelative: Bool = true var connectedNode: NodeInfoEntity? - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity @State private var environmentSectionHeight: CGFloat = 0 @State var showingCompassSheet = false @@ -69,7 +69,7 @@ struct NodeDetail: View { } .accessibilityElement(children: .combine) } - if node.telemetries?.count ?? 0 > 0 { + if node.telemetries.count > 0 { Spacer() BatteryGauge(node: node) } @@ -134,9 +134,7 @@ struct NodeDetail: View { } Spacer() Button(action: { - context.perform { - UIPasteboard.general.string = publicKey - } + UIPasteboard.general.string = publicKey }) { HStack { Image(systemName: "key.horizontal.fill") @@ -185,7 +183,7 @@ struct NodeDetail: View { } .accessibilityElement(children: .combine) } - if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds { + if let dm = node.telemetries.filter({ $0.metricsType == 0 }).last, let uptimeSeconds = dm.uptimeSeconds { HStack { Label { Text("\("Uptime".localized)") @@ -401,7 +399,7 @@ struct NodeDetail: View { .symbolRenderingMode(.multicolor) } } - .disabled(node.traceRoutes?.count ?? 0 == 0) + .disabled(node.traceRoutes.count == 0) NavigationLink { PowerMetricsLog(node: node) } label: { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift index 920f4335..47de4898 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift @@ -11,7 +11,7 @@ import MapKit struct NodeInfoItem: View { - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity var body: some View { @@ -48,10 +48,10 @@ struct NodeInfoItem: View { .font(.caption2) } } - let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) - if deviceMetrics?.count ?? 0 >= 1 { + let deviceMetrics = node.telemetries.filter { $0.metricsType == 0 } + if deviceMetrics.count >= 1 { Divider() - let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + let mostRecent = deviceMetrics.last VStack(alignment: .center) { BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) if mostRecent?.voltage ?? 0 > 0 { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 3c20a9e2..6846664f 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -11,7 +11,7 @@ import MapKit struct NodeInfoItem: View { - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity @State private var currentDevice: DeviceHardware? var body: some View { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 23a97fd0..78680567 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -88,7 +88,7 @@ struct NodeListItem: View { return desc } - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity var isDirectlyConnected: Bool var connectedNode: Int64 var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast @@ -109,7 +109,7 @@ struct NodeListItem: View { } var locationData: (PositionEntity, CLLocation)? { - guard let lastPostion = node.positions?.lastObject as? PositionEntity else { + guard let lastPostion = node.positions.last else { return nil } guard let currentLocation = LocationsHandler.shared.locationsArray.last else { @@ -172,7 +172,7 @@ struct NodeListItem: View { text: "Store & Forward".localized) } - if node.positions?.count ?? 0 > 0 && connectedNode != node.num { + if node.positions.count > 0 && connectedNode != node.num { HStack { if let (lastPostion, myCoord) = locationData { let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude) @@ -298,9 +298,8 @@ struct IconAndText: View { IconAndText(systemName: "antenna.radiowaves.left.and.right.circle.fill", text: "foo") IconAndText(systemName: "antenna.radiowaves.left.and.right.circle", text: "bar") NodeListItem(node: { - let context = PersistenceController.preview.container.viewContext - let nodeInfo = NodeInfoEntity(context: context) - let user = UserEntity(context: context) + let nodeInfo = NodeInfoEntity() + let user = UserEntity() user.longName = "Test User" user.shortName = "TU" nodeInfo.user = user diff --git a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift index cf30b441..2f8b80f4 100644 --- a/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift +++ b/Meshtastic/Views/Nodes/Helpers/ShareContactQRDialog.swift @@ -8,7 +8,7 @@ import CoreImage.CIFilterBuiltins #if canImport(UIKit) import UIKit #endif -import CoreData +import SwiftData import MeshtasticProtobufs import OSLog diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 5ace2510..738d6337 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CoreData +import SwiftData import CoreLocation import Foundation import OSLog @@ -14,7 +14,7 @@ import MapKit struct MeshMap: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @ObservedObject @@ -101,7 +101,7 @@ struct MeshMap: View { centerMapAt(coordinate: coordinate) newWaypointCoord = coordinate - editingWaypoint = WaypointEntity(context: context) + editingWaypoint = WaypointEntity() editingWaypoint!.name = "Waypoint Pin" editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480) editingWaypoint!.latitudeI = Int32((newWaypointCoord?.latitude ?? 0) * 1e7) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 67dbdb81..a5a81edb 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -7,14 +7,14 @@ import SwiftUI import CoreLocation import OSLog -import CoreData +import SwiftData import Foundation struct NodeList: View { /// Debounce delay for node selection changes (100ms) private static let nodeSelectionDebounceNs: UInt64 = 100_000_000 - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @StateObject var router: Router @State private var selectedNode: NodeInfoEntity? @@ -27,6 +27,7 @@ struct NodeList: View { @State private var shareContactNode: NodeInfoEntity? @StateObject var filters = NodeFilterParameters() @State var isEditingFilters = false + @State private var filteredNodeCount: Int = 0 @SceneStorage("selectedDetailView") var selectedDetailView: String? var connectedNode: NodeInfoEntity? { @@ -45,7 +46,8 @@ struct NodeList: View { connectedNode: connectedNode, isPresentingDeleteNodeAlert: $isPresentingDeleteNodeAlert, deleteNodeId: $deleteNodeId, - shareContactNode: $shareContactNode + shareContactNode: $shareContactNode, + filteredNodeCount: $filteredNodeCount ) .sheet(isPresented: $isEditingFilters) { NodeListFilter( @@ -72,7 +74,7 @@ struct NodeList: View { .searchable(text: $filters.searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Find a node") .autocorrectionDisabled(true) .scrollDismissesKeyboard(.immediately) - .navigationTitle(String.localizedStringWithFormat("Nodes (%@)".localized, String(getNodeCount()))) + .navigationTitle(String.localizedStringWithFormat("Nodes (%@)".localized, String(filteredNodeCount))) .listStyle(.plain) .alert("Position Exchange Requested", isPresented: $isPresentingPositionSentAlert) { Button("OK") { }.keyboardShortcut(.defaultAction) @@ -152,12 +154,6 @@ struct NodeList: View { } } - // Helper to get the count of nodes for the navigation title - private func getNodeCount() -> Int { - let request: NSFetchRequest = NodeInfoEntity.fetchRequest() - request.predicate = filters.buildPredicate() - return (try? context.count(for: request)) ?? 0 - } } // @@ -166,8 +162,9 @@ struct NodeList: View { // fileprivate struct FilteredNodeList: View { @EnvironmentObject var accessoryManager: AccessoryManager - @FetchRequest private var nodes: FetchedResults - @Environment(\.managedObjectContext) var context + @Query(sort: \NodeInfoEntity.lastHeard, order: .reverse) + private var allNodes: [NodeInfoEntity] + @Environment(\.modelContext) private var context var router: Router @Binding var selectedNode: NodeInfoEntity? @@ -175,6 +172,8 @@ fileprivate struct FilteredNodeList: View { @Binding var isPresentingDeleteNodeAlert: Bool @Binding var deleteNodeId: Int64 @Binding var shareContactNode: NodeInfoEntity? + @Binding var filteredNodeCount: Int + private var filters: NodeFilterParameters // The initializer for the FetchRequest init( @@ -184,26 +183,21 @@ fileprivate struct FilteredNodeList: View { connectedNode: NodeInfoEntity?, isPresentingDeleteNodeAlert: Binding, deleteNodeId: Binding, - shareContactNode: Binding + shareContactNode: Binding, + filteredNodeCount: Binding ) { self.router = router - let request: NSFetchRequest = NodeInfoEntity.fetchRequest() - request.sortDescriptors = [ - NSSortDescriptor(key: "ignored", ascending: true), - NSSortDescriptor(key: "favorite", ascending: false), - NSSortDescriptor(key: "lastHeard", ascending: false), - NSSortDescriptor(key: "user.longName", ascending: true) - ] - request.predicate = withFilters.buildPredicate() - request.fetchBatchSize = 50 - request.relationshipKeyPathsForPrefetching = ["user"] - self._nodes = FetchRequest(fetchRequest: request) - + self.filters = withFilters self._selectedNode = selectedNode self.connectedNode = connectedNode self._isPresentingDeleteNodeAlert = isPresentingDeleteNodeAlert self._deleteNodeId = deleteNodeId self._shareContactNode = shareContactNode + self._filteredNodeCount = filteredNodeCount + } + + private var nodes: [NodeInfoEntity] { + allNodes.filter { filters.matches(node: $0) } } // The body of the view @@ -243,9 +237,11 @@ fileprivate struct FilteredNodeList: View { } .onAppear { router.updateNodeIndex(from: nodes) + filteredNodeCount = nodes.count } - .onChange(of: nodes.count) { _, _ in + .onChange(of: nodes.count) { _, newCount in router.updateNodeIndex(from: nodes) + filteredNodeCount = newCount } } @@ -330,6 +326,73 @@ fileprivate struct FilteredNodeList: View { // fileprivate extension NodeFilterParameters { + func matches(node: NodeInfoEntity) -> Bool { + // Search text + if !searchText.isEmpty { + let text = searchText.lowercased() + let fields = [node.user?.userId, node.user?.numString, node.user?.hwModel, + node.user?.hwDisplayName, node.user?.longName, node.user?.shortName] + let matchesSearch = fields.compactMap { $0?.lowercased() }.contains { $0.contains(text) } + if !matchesSearch { return false } + } + // Favorite + if isFavorite && !node.favorite { return false } + // Via Lora/MQTT + if viaLora && !viaMqtt && node.viaMqtt { return false } + if !viaLora && viaMqtt && !node.viaMqtt { return false } + // Roles + if roleFilter && !deviceRoles.isEmpty { + let userRole = Int(node.user?.role ?? 0) + if !deviceRoles.contains(userRole) { return false } + } + // Hops Away + if hopsAway == 0 && node.hopsAway != 0 { return false } + if hopsAway > 0 && (node.hopsAway <= 0 || node.hopsAway > Int32(hopsAway)) { return false } + // Online + if isOnline { + let twoHoursAgo = Calendar.current.date(byAdding: .minute, value: -120, to: Date()) ?? Date.distantPast + if let lastHeard = node.lastHeard, lastHeard < twoHoursAgo { return false } + if node.lastHeard == nil { return false } + } + // Encrypted + if isPkiEncrypted && node.user?.pkiEncrypted != true { return false } + // Ignored + if isIgnored { + if !node.ignored { return false } + } else { + if node.ignored { return false } + } + // Environment + if isEnvironment { + let hasEnvTelemetry = (node.telemetries ?? []).contains { $0.metricsType == 1 } + if !hasEnvTelemetry { return false } + } + // Distance + if distanceFilter { + if let poi = LocationsHandler.currentLocation, + poi.latitude != LocationsHandler.DefaultLocation.latitude, + poi.longitude != LocationsHandler.DefaultLocation.longitude { + let d = maxDistance * 1.1 + let r: Double = 6371009 + let meanLat = poi.latitude * .pi / 180 + let deltaLat = d / r * 180 / .pi + let deltaLon = d / (r * cos(meanLat)) * 180 / .pi + let minLat = poi.latitude - deltaLat + let maxLat = poi.latitude + deltaLat + let minLon = poi.longitude - deltaLon + let maxLon = poi.longitude + deltaLon + let hasNearbyPosition = (node.positions ?? []).contains { pos in + guard pos.latest else { return false } + let lon = Double(pos.longitudeI) / 1e7 + let lat = Double(pos.latitudeI) / 1e7 + return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat + } + if !hasNearbyPosition { return false } + } + } + return true + } + func buildPredicate() -> NSPredicate? { var predicates: [NSPredicate] = [] diff --git a/Meshtastic/Views/Nodes/PaxCounterLog.swift b/Meshtastic/Views/Nodes/PaxCounterLog.swift index c4a93e59..77eff8d5 100644 --- a/Meshtastic/Views/Nodes/PaxCounterLog.swift +++ b/Meshtastic/Views/Nodes/PaxCounterLog.swift @@ -11,7 +11,7 @@ import OSLog struct PaxCounterLog: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @State private var isPresentingClearLogConfirm: Bool = false @@ -21,66 +21,70 @@ struct PaxCounterLog: View { @State private var bleChartColor: Color = .blue @State private var wifiChartColor: Color = .orange @State private var paxChartColor: Color = .green - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity + + @ViewBuilder + private func paxChart(chartData: [PaxCounterEntity], maxValue: Int32) -> some View { + Chart { + ForEach(chartData, id: \.self) { point in + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", (point.wifi + point.ble)) + ) + } + .accessibilityLabel("Total PAX") + .accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)") + .foregroundStyle(paxChartColor) + .interpolationMethod(.cardinal) + + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.wifi) + ) + } + .accessibilityLabel("WiFi") + .accessibilityValue("X: \(point.time!), Y: \(point.wifi)") + .foregroundStyle(wifiChartColor) + + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.ble) + ) + } + .accessibilityLabel("BLE") + .accessibilityValue("X: \(point.time!), Y: \(point.ble)") + .foregroundStyle(bleChartColor) + } + } + .chartXAxis(content: { + AxisMarks(position: .top) + }) + .chartXAxis(.automatic) + .chartYScale(domain: 0...maxValue) + .chartForegroundStyleScale([ + "BLE".localized: .blue, + "WiFi".localized: .orange, + "Total PAX".localized: .green + ]) + .chartLegend(position: .automatic, alignment: .bottom) + } var body: some View { VStack { if node.hasPax { let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) - let pax = node.pax?.reversed() as? [PaxCounterEntity] ?? [] + let pax = Array(node.pax.reversed()) let chartData = pax .filter { $0.time != nil && $0.time! >= oneWeekAgo! } .sorted { $0.time! < $1.time! } let maxValue = (chartData.map { $0.wifi }.max() ?? 0) + (chartData.map { $0.ble }.max() ?? 0) + 5 if chartData.count > 0 { GroupBox(label: Label("\(pax.count) Readings Total", systemImage: "chart.xyaxis.line")) { - - Chart { - ForEach(chartData, id: \.self) { point in - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", (point.wifi + point.ble)) - ) - } - .accessibilityLabel("Total PAX") - .accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)") - .foregroundStyle(paxChartColor) - .interpolationMethod(.cardinal) - - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", point.wifi) - ) - } - .accessibilityLabel("WiFi") - .accessibilityValue("X: \(point.time!), Y: \(point.wifi)") - .foregroundStyle(wifiChartColor) - - Plot { - PointMark( - x: .value("x", point.time!), - y: .value("y", point.ble) - ) - } - .accessibilityLabel("BLE") - .accessibilityValue("X: \(point.time!), Y: \(point.ble)") - .foregroundStyle(bleChartColor) - } - } - .chartXAxis(content: { - AxisMarks(position: .top) - }) - .chartXAxis(.automatic) - .chartYScale(domain: 0...maxValue) - .chartForegroundStyleScale([ - "BLE".localized: .blue, - "WiFi".localized: .orange, - "Total PAX".localized: .green - ]) - .chartLegend(position: .automatic, alignment: .bottom) + paxChart(chartData: chartData, maxValue: maxValue) } .frame(minHeight: 250) } @@ -225,15 +229,17 @@ struct PaxCounterLog: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" node.user = user - return PaxCounterLog(node: node) + PaxCounterLog(node: node) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } +*/ diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index f667f5ee..227af994 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -8,7 +8,7 @@ import SwiftUI import OSLog struct PositionLog: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? @@ -18,7 +18,7 @@ struct PositionLog: View { } @State var isExporting = false @State var exportString = "" - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity @State private var isPresentingClearLogConfirm = false @State private var sortOrder = [KeyPathComparator(\PositionEntity.time)] @@ -29,7 +29,7 @@ struct PositionLog: View { let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") if UIDevice.current.userInterfaceIdiom == .pad && !useGrid || UIDevice.current.userInterfaceIdiom == .mac { // Add a table for mac and ipad - let positions = node.positions?.reversed() as? [PositionEntity] ?? [] + let positions = node.positions.reversed() Table(positions, sortOrder: $sortOrder) { TableColumn("Latitude") { position in Text(String(format: "%.5f", position.latitude ?? 0)) @@ -93,8 +93,7 @@ struct PositionLog: View { .font(.caption2) .fontWeight(.bold) } - if let positions = node.positions?.reversed() as? [PositionEntity] { - ForEach(positions, id: \.self) { (mappin: PositionEntity) in + ForEach(node.positions.reversed(), id: \.self) { (mappin: PositionEntity) in let altitude = Measurement(value: Double(mappin.altitude), unit: UnitLength.meters) GridRow { Text(String(format: "%.5f", mappin.latitude ?? 0)) @@ -109,7 +108,6 @@ struct PositionLog: View { .font(.caption2) } } - } } } .padding(.leading) @@ -141,7 +139,7 @@ struct PositionLog: View { } } Button { - exportString = positionToCsvFile(positions: node.positions!.array as? [PositionEntity] ?? []) + exportString = positionToCsvFile(positions: node.positions) isExporting = true } label: { Label("Save", systemImage: "square.and.arrow.down") @@ -172,7 +170,7 @@ struct PositionLog: View { ContentUnavailableView("No Positions", systemImage: "mappin.slash") } } - .navigationTitle("Position Log \(node.positions?.count ?? 0) Points") + .navigationTitle("Position Log \(node.positions.count) Points") .navigationBarItems( trailing: ZStack { @@ -182,15 +180,17 @@ struct PositionLog: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" node.user = user - return PositionLog(node: node) + PositionLog(node: node) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } +*/ diff --git a/Meshtastic/Views/Nodes/PowerMetricsLog.swift b/Meshtastic/Views/Nodes/PowerMetricsLog.swift index b43b1c47..4d5cb5f8 100644 --- a/Meshtastic/Views/Nodes/PowerMetricsLog.swift +++ b/Meshtastic/Views/Nodes/PowerMetricsLog.swift @@ -12,9 +12,9 @@ import OSLog struct PowerMetricsLog: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @State private var sortOrder = [KeyPathComparator(\TelemetryEntity.time, order: .reverse)] @State private var selection: TelemetryEntity.ID? @@ -27,8 +27,7 @@ struct PowerMetricsLog: View { @State private var channelSelection = 0 var powerMetrics: [TelemetryEntity] { - let telemetries = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 2")) - return (telemetries?.reversed() as? [TelemetryEntity]) ?? [] + return node.telemetries.filter { $0.metricsType == 2 }.reversed() } var minMax: (min: Double, max: Double) { @@ -298,15 +297,17 @@ struct PowerMetricsLog: View { } } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" node.user = user - return PowerMetricsLog(node: node) + PowerMetricsLog(node: node) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } +*/ diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index a4c4c91e..eb678eb6 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -6,19 +6,19 @@ // import SwiftUI -import CoreData +import SwiftData import OSLog import MapKit struct TraceRouteLog: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @ObservedObject var locationsHandler = LocationsHandler.shared - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @State private var isPresentingClearLogConfirm: Bool = false @State var isExporting = false @State var exportString = "" - @ObservedObject var node: NodeInfoEntity + @Bindable var node: NodeInfoEntity @State private var selectedRoute: TraceRouteEntity? // Map Configuration @Namespace var mapScope @@ -35,7 +35,7 @@ struct TraceRouteLog: View { HStack(alignment: .top) { VStack { VStack { - List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in + List(node.traceRoutes.reversed(), id: \.self, selection: $selectedRoute) { route in Label { let routeTime = route.time?.formatted() ?? "Unknown".localized if route.response && route.hopsTowards == route.hopsBack { @@ -139,7 +139,7 @@ struct TraceRouteLog: View { // Set the view rotation animation after the view appeared, // to avoid animating initial rotation DispatchQueue.main.async { - indexes = (selectedRoute?.hops?.array.count ?? 0) * 2 + indexes = (selectedRoute?.hops.count ?? 0) * 2 animation = .easeInOut(duration: 1.0) withAnimation(.easeInOut(duration: 2.0)) { angle = (angle == .degrees(-90) ? .degrees(-90) : .degrees(-90)) @@ -165,7 +165,7 @@ struct TraceRouteLog: View { // .annotationTitles(.automatic) // // Direct Trace Route // if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { -// if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { +// if selectedRoute?.node?.positions?.count ?? 0 > 0, let mostRecent = selectedRoute?.node?.positions?.last { // let traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] // Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { // ZStack { @@ -190,7 +190,7 @@ struct TraceRouteLog: View { // /// Distance // if selectedRoute?.node?.positions?.count ?? 0 > 0, // selectedRoute?.coordinate != nil, -// let mostRecent = selectedRoute?.node?.positions?.lastObject as? PositionEntity { +// let mostRecent = selectedRoute?.node?.positions?.last { // let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) // if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { // let metersAway = selectedRoute?.coordinate?.distance(from: CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) @@ -224,7 +224,7 @@ struct TraceRouteLog: View { @ViewBuilder func contents(animation: Animation? = nil) -> some View { ForEach(0.. [TraceRouteHopEntity] { - /// static let context = PersistenceController.preview.container.viewContext +func getTraceRouteHops(context: ModelContext) -> [TraceRouteHopEntity] { + /// static let context = PersistenceController.preview.context var array = [TraceRouteHopEntity]() - let trh1 = TraceRouteHopEntity(context: context) + let trh1 = TraceRouteHopEntity() trh1.num = 366311664 trh1.snr = 12.5 - let trh2 = TraceRouteHopEntity(context: context) + let trh2 = TraceRouteHopEntity() trh2.num = 3662955168 trh2.snr = -115.00 - let trh3 = TraceRouteHopEntity(context: context) + let trh3 = TraceRouteHopEntity() trh3.num = 3663982804 trh3.snr = 17.5 - let trh4 = TraceRouteHopEntity(context: context) + let trh4 = TraceRouteHopEntity() trh4.num = 4202719792 trh4.snr = 7.0 - let trh5 = TraceRouteHopEntity(context: context) + let trh5 = TraceRouteHopEntity() trh5.num = 603700594 trh5.snr = 8.9 - let trh6 = TraceRouteHopEntity(context: context) + let trh6 = TraceRouteHopEntity() trh6.num = 836212501 trh6.snr = -24.0 - let trh7 = TraceRouteHopEntity(context: context) + let trh7 = TraceRouteHopEntity() trh7.num = 3663116644 trh7.snr = -6.0 - let trh8 = TraceRouteHopEntity(context: context) + let trh8 = TraceRouteHopEntity() trh8.num = 8362955168 trh8.snr = 7.5 array.append(trh1) @@ -288,15 +288,17 @@ func getTraceRouteHops(context: NSManagedObjectContext) -> [TraceRouteHopEntity] return array } +// TODO: Fix preview for SwiftData +/* #Preview { - let context = PersistenceController.preview.container.viewContext - let node = NodeInfoEntity(context: context) + let node = NodeInfoEntity() node.num = 123456789 - let user = UserEntity(context: context) + let user = UserEntity() user.longName = "Test Node" user.shortName = "TN" node.user = user - return TraceRouteLog(node: node) + TraceRouteLog(node: node) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } +*/ diff --git a/Meshtastic/Views/Settings/AppData.swift b/Meshtastic/Views/Settings/AppData.swift index 16034020..3f695252 100644 --- a/Meshtastic/Views/Settings/AppData.swift +++ b/Meshtastic/Views/Settings/AppData.swift @@ -7,12 +7,12 @@ import SwiftUI import OSLog -import CoreData +import SwiftData import Foundation struct AppData: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @State private var files = [URL]() private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @@ -145,8 +145,7 @@ struct AppData: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return AppData() + AppData() .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/AppIconPicker.swift b/Meshtastic/Views/Settings/AppIconPicker.swift index dd39e549..d278695a 100644 --- a/Meshtastic/Views/Settings/AppIconPicker.swift +++ b/Meshtastic/Views/Settings/AppIconPicker.swift @@ -2,7 +2,7 @@ import SwiftUI struct AppIconPicker: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @Binding var isPresenting: Bool @State private var didError = false @State private var errorDetails: String? diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index a9f0ad53..0a0ef6e1 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -8,7 +8,7 @@ import OSLog struct AppSettings: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @State var totalDownloadedTileSize = "" @State private var isPresentingCoreDataResetConfirm = false @@ -159,9 +159,8 @@ struct AppSettings: View { Logger.services.error("šŸ—„ Error Deleting Meshtastic.sqlite file \(error, privacy: .public)") } } - await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: true) + await PersistenceController.shared.clearDatabase(includeRoutes: true) clearNotifications() - context.refreshAllObjects() } } } diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index f5c89071..7de8413f 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -5,7 +5,7 @@ // Copyright(c) Garth Vander Houwen 4/8/22. // -import CoreData +import SwiftData import MapKit import MeshtasticProtobufs import OSLog @@ -22,7 +22,7 @@ func generateChannelKey(size: Int) -> String { struct Channels: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @Environment(\.sizeCategory) var sizeCategory @@ -50,13 +50,8 @@ struct Channels: View { @State var minimumVersion = "2.2.24" @State private var showingHelp = false - @FetchRequest( - sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), - NSSortDescriptor(key: "lastHeard", ascending: false), - NSSortDescriptor(key: "user.longName", ascending: true)], - animation: .default) - - var nodes: FetchedResults + @Query(sort: \NodeInfoEntity.lastHeard, order: .reverse) + var nodes: [NodeInfoEntity] var body: some View { @@ -66,7 +61,7 @@ struct Channels: View { .tipBackground(colorScheme == .dark ? Color(.systemBackground) : Color(.secondarySystemBackground)) .listRowSeparator(.hidden) if node != nil && node?.myInfo != nil { - ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in + ForEach(node?.myInfo?.channels ?? [], id: \.self) { (channel: ChannelEntity) in Button(action: { channelIndex = channel.index channelRole = Int(channel.role) @@ -177,17 +172,15 @@ struct Channels: View { selectedChannel!.downlinkEnabled = downlink selectedChannel!.positionPrecision = Int32(positionPrecision) - guard let mutableChannels = node?.myInfo?.channels?.mutableCopy() as? NSMutableOrderedSet else { + guard var channels = node?.myInfo?.channels else { return } - if mutableChannels.contains(selectedChannel as Any) { - let replaceChannel = mutableChannels.first(where: { selectedChannel?.psk == ($0 as AnyObject).psk && selectedChannel?.name == ($0 as AnyObject).name}) - mutableChannels.replaceObject(at: mutableChannels.index(of: replaceChannel as Any), with: selectedChannel as Any) + if let idx = channels.firstIndex(where: { $0.psk == selectedChannel?.psk && $0.name == selectedChannel?.name }) { + channels[idx] = selectedChannel! } else { - mutableChannels.add(selectedChannel as Any) + channels.append(selectedChannel!) } - node?.myInfo?.channels = mutableChannels.copy() as? NSOrderedSet - context.refresh(selectedChannel!, mergeChanges: true) + node?.myInfo?.channels = channels if channel.role != Channel.Role.disabled { do { try context.save() @@ -246,12 +239,10 @@ struct Channels: View { #endif } } - if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { + if node?.myInfo?.channels.count ?? 0 < 8 && node != nil { Button { - let channelIndexes = node?.myInfo?.channels?.compactMap({(ch) -> Int in - return (ch as AnyObject).index - }) + let channelIndexes = node?.myInfo?.channels.map { Int($0.index) } let firstChannelIndex = firstMissingChannelIndex(channelIndexes ?? []) channelKeySize = 16 let key = generateChannelKey(size: channelKeySize) @@ -265,7 +256,8 @@ struct Channels: View { uplink = false downlink = false - let newChannel = ChannelEntity(context: context) + let newChannel = ChannelEntity() + context.insert(newChannel) newChannel.id = channelIndex newChannel.index = channelIndex newChannel.uplinkEnabled = uplink diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index b1934df3..10b2eae5 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -10,7 +10,7 @@ import OSLog import SwiftUI struct BluetoothConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack var node: NodeInfoEntity? @@ -149,8 +149,7 @@ struct BluetoothConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return BluetoothConfig(node: nil) + BluetoothConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/ConfigHeader.swift b/Meshtastic/Views/Settings/Config/ConfigHeader.swift index c8ed4f91..e8217008 100644 --- a/Meshtastic/Views/Settings/Config/ConfigHeader.swift +++ b/Meshtastic/Views/Settings/Config/ConfigHeader.swift @@ -1,5 +1,5 @@ import SwiftUI -import CoreData +import SwiftData struct ConfigHeader: View { @EnvironmentObject var accessoryManager: AccessoryManager diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 7d0814dc..2a8482fc 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -11,7 +11,7 @@ import SwiftUI struct DeviceConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -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() - await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false) + await PersistenceController.shared.clearDatabase(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() - await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false) + await PersistenceController.shared.clearDatabase(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() - await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false) + await PersistenceController.shared.clearDatabase(includeRoutes: false) clearNotifications() } catch { Logger.mesh.error("Factory Reset Failed") @@ -347,8 +347,7 @@ struct DeviceConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return DeviceConfig(node: nil) + DeviceConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index a9240da0..59809b7d 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -11,7 +11,7 @@ import SwiftUI struct DisplayConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -237,8 +237,7 @@ struct DisplayConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return DisplayConfig(node: nil) + DisplayConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 4129e133..6669dc4c 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CoreData +import SwiftData import MeshtasticProtobufs import OSLog @@ -24,7 +24,7 @@ struct LoRaConfig: View { return formatter }() - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @FocusState var focusedField: Field? @@ -323,8 +323,7 @@ struct LoRaConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return LoRaConfig(node: nil) + LoRaConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index db2c98c2..dfbab686 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -10,7 +10,7 @@ import OSLog struct AmbientLightingConfig: View { @Environment(\.self) var environment - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -136,8 +136,7 @@ struct AmbientLightingConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return AmbientLightingConfig(node: nil) + AmbientLightingConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 54a9a6bd..92d04bb8 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -9,7 +9,7 @@ import OSLog import SwiftUI struct CannedMessagesConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack var node: NodeInfoEntity? @@ -357,8 +357,7 @@ struct CannedMessagesConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return CannedMessagesConfig(node: nil) + CannedMessagesConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index a56d42b1..ce69ccc1 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -24,7 +24,7 @@ enum DetectionSensorRole: String, CaseIterable, Equatable, Decodable { struct DetectionSensorConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack var node: NodeInfoEntity? @@ -263,8 +263,7 @@ struct DetectionSensorConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return DetectionSensorConfig(node: nil) + DetectionSensorConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index b3694ee3..35c8d555 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -10,7 +10,7 @@ import SwiftUI struct ExternalNotificationConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -284,8 +284,7 @@ struct ExternalNotificationConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return ExternalNotificationConfig(node: nil) + ExternalNotificationConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index a3417650..615f7869 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -11,7 +11,7 @@ import SwiftUI struct MQTTConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack var node: NodeInfoEntity? @@ -466,8 +466,7 @@ struct MQTTConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return MQTTConfig(node: nil) + MQTTConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index 90b83f73..0898a724 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -10,7 +10,7 @@ import SwiftUI import OSLog struct PaxCounterConfig: View { - @Environment(\.managedObjectContext) private var context + @Environment(\.modelContext) private var context @EnvironmentObject private var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -125,8 +125,7 @@ struct PaxCounterConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return PaxCounterConfig(node: nil) + PaxCounterConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index f3d19871..45f13dc3 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -5,13 +5,13 @@ // Copyright (c) Garth Vander Houwen 6/13/22. // import MeshtasticProtobufs -import CoreData +import SwiftData import OSLog import SwiftUI struct RangeTestConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -23,7 +23,7 @@ struct RangeTestConfig: View { @State var save = false @State private var sender: UpdateInterval = UpdateInterval(from: 0) private var isPrimaryChannelPublic: Bool { - guard let channels = node?.myInfo?.channels?.array as? [ChannelEntity] else { + guard let channels = node?.myInfo?.channels else { return false } // Treat the primary channel on this node as "public" when it is effectively unencrypted @@ -144,8 +144,7 @@ struct RangeTestConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return RangeTestConfig(node: nil) + RangeTestConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 6f45103a..4103e47b 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -9,7 +9,7 @@ import SwiftUI import OSLog struct RtttlConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -116,8 +116,7 @@ struct RtttlConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return RtttlConfig(node: nil) + RtttlConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index ff070c09..e8c25e99 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -10,7 +10,7 @@ import SwiftUI struct SerialConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -207,8 +207,7 @@ struct SerialConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return SerialConfig(node: nil) + SerialConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index 577ef4b5..8d617f93 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -10,7 +10,7 @@ import SwiftUI struct StoreForwardConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack var node: NodeInfoEntity? @@ -199,8 +199,7 @@ struct StoreForwardConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return StoreForwardConfig(node: nil) + StoreForwardConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift b/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift index 8125956f..e2c2118c 100644 --- a/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift @@ -2,12 +2,12 @@ // TAKModuleConfig.swift // Meshtastic import SwiftUI -import CoreData +import SwiftData import OSLog import MeshtasticProtobufs struct TAKModuleConfig: View { - @Environment(\.managedObjectContext) private var context + @Environment(\.modelContext) private var context @EnvironmentObject private var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -262,8 +262,7 @@ struct TAKModuleConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return TAKModuleConfig(node: nil) + TAKModuleConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index caae19c3..49dc3570 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -10,7 +10,7 @@ import SwiftUI struct TelemetryConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -243,8 +243,7 @@ struct TelemetryConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return TelemetryConfig(node: nil) + TelemetryConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 044e2a2f..b32f6e1f 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -10,7 +10,7 @@ import SwiftUI struct NetworkConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -211,8 +211,7 @@ struct NetworkConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return NetworkConfig(node: nil) + NetworkConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 68c8e9bd..828ace77 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -25,7 +25,7 @@ struct PositionFlags: OptionSet { struct PositionConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack var node: NodeInfoEntity? @@ -548,9 +548,7 @@ struct PositionConfig: View { Logger.mesh.error("Remove Fixed Position Failed") } } - let mutablePositions = node?.positions?.mutableCopy() as? NSMutableOrderedSet - mutablePositions?.removeAllObjects() - node?.positions = mutablePositions + node?.positions = [] node?.positionConfig?.fixedPosition = false do { try context.save() @@ -564,8 +562,7 @@ struct PositionConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return PositionConfig(node: nil) + PositionConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index 737bd7f7..3daccba1 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -3,7 +3,7 @@ import MeshtasticProtobufs import OSLog struct PowerConfig: View { - @Environment(\.managedObjectContext) private var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -228,8 +228,7 @@ private struct FloatField: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return PowerConfig(node: nil) + PowerConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index d8c2a969..99bc8582 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -7,7 +7,7 @@ import Foundation import SwiftUI -import CoreData +import SwiftData import MeshtasticProtobufs import OSLog import CryptoKit @@ -15,7 +15,7 @@ import CryptoKit struct SecurityConfig: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -430,8 +430,7 @@ struct SecurityConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return SecurityConfig(node: nil) + SecurityConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 3869d145..3584cddc 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -10,7 +10,7 @@ import StoreKit import OSLog struct Firmware: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager var node: NodeInfoEntity? @State var minimumVersion = "2.5.4" @@ -208,8 +208,7 @@ struct Firmware: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return Firmware(node: nil) + Firmware(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index b1c63bc2..d9b0b63f 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -6,7 +6,7 @@ // import SwiftUI -import CoreData +import SwiftData import MapKit import CoreLocation import CoreMotion @@ -15,7 +15,7 @@ import OSLog struct RouteRecorder: View { @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic) @State var isShowingDetails = false @@ -182,7 +182,8 @@ struct RouteRecorder: View { locationsHandler.elevationGain = 0.0 locationsHandler.locationsArray.removeAll() locationsHandler.recordingStarted = Date() - let newRoute = RouteEntity(context: context) + let newRoute = RouteEntity() + context.insert(newRoute) newRoute.date = Date() let at = ActivityType(rawValue: activity) newRoute.name = "\(newRoute.date?.relativeTimeOfDay() ?? "morning".localized) \(at?.fileNameString ?? "hike")" @@ -241,7 +242,6 @@ struct RouteRecorder: View { rec.enabled = true rec.distance = locationsHandler.distanceTraveled rec.elevationGain = locationsHandler.elevationGain - context.refresh(rec, mergeChanges: true) } locationsHandler.isRecording = false locationsHandler.isRecordingPaused = false @@ -296,7 +296,8 @@ struct RouteRecorder: View { if locationsHandler.isRecording { if let loc = newLoc { if recording != nil { - let locationEntity = LocationEntity(context: context) + let locationEntity = LocationEntity() + context.insert(locationEntity) locationEntity.routeLocation = recording locationEntity.id = Int32(locationsHandler.count) locationEntity.altitude = Int32(loc.altitude) diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index 3db1cc4f..0b43fee9 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -6,14 +6,14 @@ // import SwiftUI -import CoreData +import SwiftData import MapKit import OSLog struct Routes: View { @State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @State private var selectedRoute: RouteEntity? @State private var importing = false @@ -27,9 +27,9 @@ struct Routes: View { @State var enabled = true @State var color = Color(red: 51, green: 199, blue: 88) - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "enabled", ascending: false), NSSortDescriptor(key: "name", ascending: true), NSSortDescriptor(key: "date", ascending: false)], animation: .default) - - var routes: FetchedResults + @Query(sort: [SortDescriptor(\RouteEntity.name, order: .forward), + SortDescriptor(\RouteEntity.date, order: .reverse)]) + var routes: [RouteEntity] var body: some View { VStack { @@ -72,7 +72,8 @@ struct Routes: View { } } if latIndex >= 0 && longIndex >= 0 { - let newRoute = RouteEntity(context: context) + let newRoute = RouteEntity() + context.insert(newRoute) newRoute.name = String(routeName) newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) newRoute.color = Int64(UIColor.random.hex) @@ -84,13 +85,14 @@ struct Routes: View { if data.count > 1 { let latitude = latIndex >= 0 ? data[latIndex].trimmingCharacters(in: .whitespaces) : "0" let longitude = longIndex >= 0 ? data[longIndex].trimmingCharacters(in: .whitespaces) : "0" - let loc = LocationEntity(context: context) + let loc = LocationEntity() + context.insert(loc) loc.latitudeI = Int32((Double(latitude) ?? 0) * 1e7) loc.longitudeI = Int32((Double(longitude) ?? 0) * 1e7) newLocations.append(loc) } } - newRoute.locations? = NSOrderedSet(array: newLocations) + newRoute.locations = newLocations do { try context.save() } catch let error as NSError { @@ -143,7 +145,7 @@ struct Routes: View { } } } - .badge(Text("\(Image(systemName: "mappin.and.ellipse")) \(route.locations?.count ?? 0)")) + .badge(Text("\(Image(systemName: "mappin.and.ellipse")) \(route.locations.count)")) .font(.headline) .swipeActions { Button(role: .destructive) { @@ -163,7 +165,7 @@ struct Routes: View { } else { VStack { if selectedRoute != nil { - let locationArray = selectedRoute?.locations?.array as? [LocationEntity] ?? [] + let locationArray = selectedRoute?.locations ?? [] let lineCoords = locationArray.compactMap({(location) -> CLLocationCoordinate2D in return location.locationCoordinate ?? LocationsHandler.DefaultLocation }) @@ -276,7 +278,7 @@ struct Routes: View { .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) { Button { - exportString = routeToCsvFile(locations: selectedRoute!.locations!.array as? [LocationEntity] ?? []) + exportString = routeToCsvFile(locations: selectedRoute?.locations ?? []) isExporting = true } label: { Label("Export", systemImage: "square.and.arrow.down") diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index 089b12c2..79efb4cc 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -5,7 +5,7 @@ // Copyright(c) Garth Vander Houwen 7/13/22. // import SwiftUI -import CoreData +import SwiftData import OSLog import MeshtasticProtobufs @@ -17,7 +17,7 @@ struct SaveChannelLinkData: Identifiable { struct SaveChannelQRCode: View { @Environment(\.dismiss) private var dismiss - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context let channelSetLink: String @State var addChannels: Bool = false var accessoryManager: AccessoryManager @@ -165,12 +165,14 @@ struct SaveChannelQRCode: View { channelData = channelSetLink } Logger.data.info("Processing channel data: \(channelData)") - // Fetch current LoRa config from Core Data - let fetchRequest = NodeInfoEntity.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(accessoryManager.activeDeviceNum ?? 0)) + // Fetch current LoRa config + let activeNum = Int64(accessoryManager.activeDeviceNum ?? 0) + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.num == activeNum } + ) do { - let nodes = try context.fetch(fetchRequest) + let nodes = try context.fetch(descriptor) if let node = nodes.first { currentLoRaConfig = node.loRaConfig?.toProto() } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index c57f0b3d..4852fb93 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -6,24 +6,17 @@ // import SwiftUI +import SwiftData import OSLog import TipKit import MeshtasticProtobufs struct Settings: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @Environment(\.colorScheme) private var colorScheme @EnvironmentObject var accessoryManager: AccessoryManager - @FetchRequest( - sortDescriptors: [ - NSSortDescriptor(key: "favorite", ascending: false), - NSSortDescriptor(key: "user.pkiEncrypted", ascending: false), - NSSortDescriptor(key: "viaMqtt", ascending: true), - NSSortDescriptor(key: "user.longName", ascending: true) - ], - animation: .default - ) - private var nodes: FetchedResults + @Query(sort: \NodeInfoEntity.lastHeard, order: .reverse) + private var nodes: [NodeInfoEntity] @State private var selectedNode: Int = 0 @State private var preferredNodeNum: Int = 0 diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 281a90fd..77133f81 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -5,7 +5,7 @@ // Copyright(c) Garth Vander Houwen 4/8/22. // import SwiftUI -import CoreData +import SwiftData import CoreImage.CIFilterBuiltins import MeshtasticProtobufs import TipKit @@ -33,7 +33,7 @@ struct QrCodeImage { struct ShareChannels: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var dismiss @State var channelSet: ChannelSet = ChannelSet() @@ -78,7 +78,7 @@ struct ShareChannels: View { .font(.caption) .fontWeight(.bold) } - ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in + ForEach(node?.myInfo?.channels ?? [], id: \.self) { (channel: ChannelEntity) in GridRow { Spacer() if channel.index == 0 { @@ -285,8 +285,8 @@ struct ShareChannels: View { loRaConfig.ignoreMqtt = node?.loRaConfig?.ignoreMqtt ?? false loRaConfig.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.0 channelSet.loraConfig = loRaConfig - if node?.myInfo?.channels != nil && node?.myInfo?.channels?.count ?? 0 > 0 { - for ch in node?.myInfo?.channels?.array as? [ChannelEntity] ?? [] where ch.role > 0 { + if node?.myInfo != nil && (node?.myInfo?.channels.count ?? 0) > 0 { + for ch in node?.myInfo?.channels ?? [] where ch.role > 0 { var includeChannel = false switch ch.index { case 0: diff --git a/Meshtastic/Views/Settings/TAKServerConfig.swift b/Meshtastic/Views/Settings/TAKServerConfig.swift index 7e8b6502..10af606c 100644 --- a/Meshtastic/Views/Settings/TAKServerConfig.swift +++ b/Meshtastic/Views/Settings/TAKServerConfig.swift @@ -8,7 +8,7 @@ import SwiftUI import UniformTypeIdentifiers import OSLog -import CoreData +import SwiftData enum CertificateImportType { case p12 @@ -16,14 +16,12 @@ enum CertificateImportType { } struct TAKServerConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)], - predicate: NSPredicate(format: "role > 0"), - animation: .default - ) private var channels: FetchedResults + @Query(filter: #Predicate { $0.role > 0 }, + sort: \ChannelEntity.index) + private var channels: [ChannelEntity] @StateObject private var takServer = TAKServerManager.shared @Environment(\.dismiss) private var dismiss diff --git a/Meshtastic/Views/Settings/Tools.swift b/Meshtastic/Views/Settings/Tools.swift index c4508deb..ed82d808 100644 --- a/Meshtastic/Views/Settings/Tools.swift +++ b/Meshtastic/Views/Settings/Tools.swift @@ -15,7 +15,7 @@ import OSLog @available(iOS 18, *) struct Tools: View { @EnvironmentObject var accessoryManager: AccessoryManager - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context #if !targetEnvironment(macCatalyst) @StateObject private var nfcReader = NFCReader() @@ -72,10 +72,9 @@ struct Tools: View { @available(iOS 18, *) #Preview { - let context = PersistenceController.preview.container.viewContext - return Tools() + Tools() .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) } #if !targetEnvironment(macCatalyst) diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index aab5a127..d41cf3df 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -4,13 +4,13 @@ // // Copyright (c) Garth Vander Houwen 6/27/22. // -import CoreData +import SwiftData import MeshtasticProtobufs import SwiftUI struct UserConfig: View { - @Environment(\.managedObjectContext) var context + @Environment(\.modelContext) private var context @EnvironmentObject var accessoryManager: AccessoryManager @Environment(\.dismiss) private var goBack @@ -255,8 +255,7 @@ struct UserConfig: View { } #Preview { - let context = PersistenceController.preview.container.viewContext - return UserConfig(node: nil) + UserConfig(node: nil) .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) + .modelContainer(PersistenceController.preview.container) }