mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
initial swift data conversion
This commit is contained in:
parent
183924d4dc
commit
b2c72ae166
130 changed files with 2939 additions and 2269 deletions
|
|
@ -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 = "<group>"; };
|
||||
4B88B4AE298C43D1B8F4516C /* ConfigModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigModels.swift; sourceTree = "<group>"; };
|
||||
509A1C42A695463093654617 /* DeviceMetadataEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetadataEntity.swift; sourceTree = "<group>"; };
|
||||
38DF7D72AA0A450399DBC592 /* MeshtasticSchema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticSchema.swift; sourceTree = "<group>"; };
|
||||
F3204EF969484E549C139730 /* MessageEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntity.swift; sourceTree = "<group>"; };
|
||||
DC5970BC1CAF4B66999C7248 /* MyInfoEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntity.swift; sourceTree = "<group>"; };
|
||||
7101240749F24E96835A040A /* NodeInfoEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntity.swift; sourceTree = "<group>"; };
|
||||
A97241903A924144A3EEB679 /* PositionEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntity.swift; sourceTree = "<group>"; };
|
||||
DB55D64F064C45ADBD31ABF8 /* RouteModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteModels.swift; sourceTree = "<group>"; };
|
||||
31483AF5F5354D3481698E32 /* TelemetryEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryEntity.swift; sourceTree = "<group>"; };
|
||||
D8D66A4063FE4E868CD30D3C /* TraceRouteModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteModels.swift; sourceTree = "<group>"; };
|
||||
918722D2C1474B2D99ED01DC /* UserEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntity.swift; sourceTree = "<group>"; };
|
||||
E98797BFD67F449B9ACCEDA7 /* WaypointEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntity.swift; sourceTree = "<group>"; };
|
||||
01028778B8BFD81F7A039593 /* TAKConnection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKConnection.swift; sourceTree = "<group>"; };
|
||||
0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerConfig.swift; sourceTree = "<group>"; };
|
||||
09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKCertificateManager.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -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<MyInfoEntity>(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 {
|
||||
|
|
|
|||
|
|
@ -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<NodeInfoEntity>(
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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<UserEntity>(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)..<UInt32.max))
|
||||
newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970)
|
||||
newMessage.receivedACK = false
|
||||
|
|
@ -340,7 +341,6 @@ extension AccessoryManager {
|
|||
let contactString = try contact.serializedData().base64EncodedString()
|
||||
try? await am.addContactFromURL(base64UrlString: contactString)
|
||||
try context.save()
|
||||
user.objectWillChange.send()
|
||||
} catch {
|
||||
Logger.services.error("Error inserting new contact and resending encrypted send failed message: \(error)")
|
||||
}
|
||||
|
|
@ -454,8 +454,7 @@ extension AccessoryManager {
|
|||
var i: Int32 = 0
|
||||
|
||||
if addChannels {
|
||||
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(deviceNum))
|
||||
let fetchMyInfoRequest = FetchDescriptor<MyInfoEntity>(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<NodeInfoEntity>(predicate: #Predicate { $0.num == destNum || $0.num == fromNodeNum })
|
||||
do {
|
||||
let fetchedNodes = try context.fetch(nodes)
|
||||
let receivingNode = fetchedNodes.first(where: { $0.num == destNum })
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<NodeInfoEntity>(
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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<NSFetchRequestResult> = NSFetchRequest(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
let nodeNumInt64 = Int64(nodeNum)
|
||||
var descriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<NSFetchRequestResult> = NSFetchRequest(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
let nodeNumInt64 = Int64(nodeNum)
|
||||
var descriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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<MessageEntity> {
|
||||
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<MessageEntity>(
|
||||
predicate: #Predicate<MessageEntity> { 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<MessageEntity>(
|
||||
predicate: #Predicate<MessageEntity> { 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<MessageEntity>(
|
||||
predicate: #Predicate<MessageEntity> { 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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// Copyright (c) Garth Vander Houwen 11/21/23.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<MessageEntity>(
|
||||
predicate: #Predicate<MessageEntity> { 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> = UserEntity.fetchRequest()
|
||||
request.predicate = NSPredicate(
|
||||
format: "(num & 0xFF) == %lld",
|
||||
relaySuffix
|
||||
)
|
||||
let descriptor = FetchDescriptor<UserEntity>()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MessageEntity> {
|
||||
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<MessageEntity>(
|
||||
predicate: #Predicate<MessageEntity> { 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<MessageEntity>(
|
||||
predicate: #Predicate<MessageEntity> { 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Any> {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PositionEntity> {
|
||||
|
||||
let request: NSFetchRequest<PositionEntity> = 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<PositionEntity> {
|
||||
var descriptor = FetchDescriptor<PositionEntity>(
|
||||
predicate: #Predicate<PositionEntity> { 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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// Copyright(c) Garth Vander Houwen 12/7/23.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
|
|
|||
|
|
@ -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<MessageEntity> {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<WaypointEntity> {
|
||||
let request: NSFetchRequest<WaypointEntity> = 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<WaypointEntity> {
|
||||
let now = Date()
|
||||
return FetchDescriptor<WaypointEntity>(
|
||||
predicate: #Predicate<WaypointEntity> { 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// Created by Benjamin Faershtein on 6/27/25.
|
||||
//
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import OSLog
|
||||
import TipKit
|
||||
import MeshtasticProtobufs
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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> = NodeInfoEntity.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(
|
||||
format: "user != nil"
|
||||
var descriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $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> = NodeInfoEntity.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
fetchRequest.fetchLimit = 1
|
||||
let nodeNumInt64 = Int64(nodeNum)
|
||||
var descriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $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
|
||||
|
|
|
|||
|
|
@ -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<MyInfoEntity>()
|
||||
|
||||
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<MyInfoEntity>()
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
26
Meshtastic/Model/ChannelEntity.swift
Normal file
26
Meshtastic/Model/ChannelEntity.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
320
Meshtastic/Model/ConfigModels.swift
Normal file
320
Meshtastic/Model/ConfigModels.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
28
Meshtastic/Model/DeviceMetadataEntity.swift
Normal file
28
Meshtastic/Model/DeviceMetadataEntity.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
53
Meshtastic/Model/MeshtasticSchema.swift
Normal file
53
Meshtastic/Model/MeshtasticSchema.swift
Normal file
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
}
|
||||
43
Meshtastic/Model/MessageEntity.swift
Normal file
43
Meshtastic/Model/MessageEntity.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class MetricsChartSeries: ObservableObject {
|
|||
@ChartContentBuilder chartBody: @escaping (MetricsChartSeries, ClosedRange<Float>?, 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
|
||||
|
|
|
|||
27
Meshtastic/Model/MyInfoEntity.swift
Normal file
27
Meshtastic/Model/MyInfoEntity.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
112
Meshtastic/Model/NodeInfoEntity.swift
Normal file
112
Meshtastic/Model/NodeInfoEntity.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
29
Meshtastic/Model/PositionEntity.swift
Normal file
29
Meshtastic/Model/PositionEntity.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
53
Meshtastic/Model/RouteModels.swift
Normal file
53
Meshtastic/Model/RouteModels.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
74
Meshtastic/Model/TelemetryEntity.swift
Normal file
74
Meshtastic/Model/TelemetryEntity.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
46
Meshtastic/Model/TraceRouteModels.swift
Normal file
46
Meshtastic/Model/TraceRouteModels.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
40
Meshtastic/Model/UserEntity.swift
Normal file
40
Meshtastic/Model/UserEntity.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
27
Meshtastic/Model/WaypointEntity.swift
Normal file
27
Meshtastic/Model/WaypointEntity.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<NodeInfoEntity>(
|
||||
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<MessageEntity>(
|
||||
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<TraceRouteEntity>(
|
||||
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<UserEntity>(
|
||||
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<WaypointEntity>(
|
||||
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
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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<C: Collection>(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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ChannelEntity>
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MessageEntity>
|
||||
@Query private var allPrivateMessages: [MessageEntity]
|
||||
|
||||
init(myInfo: MyInfoEntity, channel: ChannelEntity) {
|
||||
self.myInfo = myInfo
|
||||
self.channel = channel
|
||||
|
||||
// Configure fetch request here
|
||||
let request: NSFetchRequest<MessageEntity> = 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<MessageEntity> {
|
||||
$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)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MessageEntity> // 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<MessageEntity>,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<UserEntity>
|
||||
@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<NodeInfoEntity?>, userSelection: Binding<UserEntity?>) {
|
||||
let request: NSFetchRequest<UserEntity> = 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
|
||||
|
|
|
|||
|
|
@ -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<MessageEntity>
|
||||
|
||||
init(user: UserEntity) {
|
||||
self.user = user
|
||||
|
||||
// Configure fetch request here
|
||||
let request: NSFetchRequest<MessageEntity> = 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)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<MessageEntity>
|
||||
@Bindable var node: NodeInfoEntity
|
||||
@Query(filter: #Predicate<MessageEntity> { $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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<TelemetryEntity>
|
||||
|
||||
init(node: NodeInfoEntity) {
|
||||
self.node = node
|
||||
|
||||
// Build fetch request:
|
||||
let request: NSFetchRequest<TelemetryEntity> = 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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<NodeInfoEntity> = NSFetchRequest(entityName: "NodeInfoEntity")
|
||||
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum))
|
||||
var descriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<UUID>
|
||||
|
||||
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn)
|
||||
var positions: FetchedResults<PositionEntity>
|
||||
|
||||
@FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none)
|
||||
var waypoints: FetchedResults<WaypointEntity>
|
||||
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
|
||||
predicate: NSPredicate(format: "enabled == true", ""), animation: .none)
|
||||
private var routes: FetchedResults<RouteEntity>
|
||||
@Query(filter: #Predicate<PositionEntity> { $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<RouteEntity> { $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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
|
||||
|
|
@ -30,10 +31,10 @@ private struct NodeMapContentEquatableWrapper<Content: View>: 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<WaypointEntity>
|
||||
@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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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> = NodeInfoEntity.fetchRequest()
|
||||
createdByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.createdBy))
|
||||
createdByFetch.fetchLimit = 1
|
||||
let createdByNum = Int64(waypoint.createdBy)
|
||||
var createdByDescriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $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> = NodeInfoEntity.fetchRequest()
|
||||
updatedByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.lastUpdatedBy))
|
||||
updatedByFetch.fetchLimit = 1
|
||||
let updatedByNum = Int64(waypoint.lastUpdatedBy)
|
||||
var updatedByDescriptor = FetchDescriptor<NodeInfoEntity>(
|
||||
predicate: #Predicate<NodeInfoEntity> { $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)")
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import CoreImage.CIFilterBuiltins
|
|||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
import CoreData
|
||||
import SwiftData
|
||||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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> = 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<NodeInfoEntity>
|
||||
@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<Bool>,
|
||||
deleteNodeId: Binding<Int64>,
|
||||
shareContactNode: Binding<NodeInfoEntity?>
|
||||
shareContactNode: Binding<NodeInfoEntity?>,
|
||||
filteredNodeCount: Binding<Int>
|
||||
) {
|
||||
self.router = router
|
||||
let request: NSFetchRequest<NodeInfoEntity> = 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] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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..<indexes, id: \.self) { idx in
|
||||
TraceRouteComponent(animation: animation) {
|
||||
let hops = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? [] // getTraceRouteHops(context: PersistenceController.preview.container.viewContext)//
|
||||
let hops = selectedRoute?.hops ?? []
|
||||
if idx % 2 == 0 {
|
||||
let i = idx / 2
|
||||
let snrColor = getSnrColor(snr: hops[i].snr, preset: modemPreset)
|
||||
|
|
@ -250,31 +250,31 @@ struct TraceRouteLog: View {
|
|||
}
|
||||
}
|
||||
|
||||
func getTraceRouteHops(context: NSManagedObjectContext) -> [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)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<NodeInfoEntity>
|
||||
@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
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue