diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cfd4fb58..fe26dd7b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -49,7 +49,6 @@ 233E99C52D84A0B600CC3A77 /* CompactWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 233E99C42D84A0B600CC3A77 /* CompactWidget.swift */; }; 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 */; }; 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 */; }; @@ -407,9 +406,6 @@ 233E99C42D84A0B600CC3A77 /* CompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactWidget.swift; sourceTree = ""; }; 233E99C62D84A70900CC3A77 /* SoilCompactWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoilCompactWidgets.swift; sourceTree = ""; }; 233E99CA2D85AAA900CC3A77 /* RainfallCompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RainfallCompactWidget.swift; sourceTree = ""; }; - 2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedAttributePropertyWrapper.swift; sourceTree = ""; }; - 2344A2AD2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TelemetryEntity+CoreDataClass.swift"; sourceTree = ""; }; - 2344A2AE2D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TelemetryEntity+CoreDataProperties.swift"; sourceTree = ""; }; 2346A7182E2FB9A300CB9239 /* SerialConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConnection.swift; sourceTree = ""; }; 2346A71C2E2FB9C500CB9239 /* SerialTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialTransport.swift; sourceTree = ""; }; 2349A0492EAE4DA30060A581 /* ManualConnectionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualConnectionList.swift; sourceTree = ""; }; @@ -822,15 +818,6 @@ path = "Compact Widgets"; sourceTree = ""; }; - 2344A2AC2D66978000170A77 /* CoreData */ = { - isa = PBXGroup; - children = ( - 2344A2AD2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift */, - 2344A2AE2D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift */, - ); - path = CoreData; - sourceTree = ""; - }; 237AEB8D2E1FE120003B7CE3 /* Accessory */ = { isa = PBXGroup; children = ( @@ -1013,7 +1000,6 @@ isa = PBXGroup; children = ( 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */, - 2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */, DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */, 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */, DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */, @@ -1337,7 +1323,6 @@ D8D66A4063FE4E868CD30D3C /* TraceRouteModels.swift */, 918722D2C1474B2D99ED01DC /* UserEntity.swift */, E98797BFD67F449B9ACCEDA7 /* WaypointEntity.swift */, - 2344A2AC2D66978000170A77 /* CoreData */, 231B3F1E2D0879BC0069A07D /* Metrics Visualization */, DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */, ); @@ -1987,7 +1972,6 @@ DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, - 2344A2AB2D66974300170A77 /* ManagedAttributePropertyWrapper.swift in Sources */, BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */, @@ -1996,9 +1980,7 @@ 2346A71D2E2FB9C500CB9239 /* SerialTransport.swift in Sources */, BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */, - 2344A2AF2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift in Sources */, 2349A04A2EAE4DA30060A581 /* ManualConnectionList.swift in Sources */, - 2344A2B02D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift in Sources */, D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */, 237AEB8F2E1FE457003B7CE3 /* Transport.swift in Sources */, DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */, diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 8958bc5b..ba1ce44d 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -661,6 +661,7 @@ extension AccessoryManager { let traceRoute = TraceRouteEntity() context.insert(traceRoute) + traceRoute.sent = true // TODO: Not sure what's going on here. We always have a fromNodeNum let nodes = FetchDescriptor(predicate: #Predicate { $0.num == destNum || $0.num == fromNodeNum }) do { diff --git a/Meshtastic/Extensions/CoreData/ManagedAttributePropertyWrapper.swift b/Meshtastic/Extensions/CoreData/ManagedAttributePropertyWrapper.swift deleted file mode 100644 index 5c5fc71f..00000000 --- a/Meshtastic/Extensions/CoreData/ManagedAttributePropertyWrapper.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// ManagedAttributePropertyWrapper.swift -// Meshtastic -// -// Created by Jake Bordens on 12/26/24. -// -import CoreData - -@propertyWrapper -public struct ManagedAttribute { - private let attributeName: String - private let converter: (NSNumber) -> Value? - - public init(attributeName: String) { - self.attributeName = attributeName - - // Define the converter closure based on the generic type Value - if Value.self == Float.self { - converter = { $0.floatValue as? Value } - } else if Value.self == Double.self { - converter = { $0.doubleValue as? Value } - } else if Value.self == Int.self { - converter = { $0.intValue as? Value } - } else if Value.self == Int8.self { - converter = { $0.int8Value as? Value } - } else if Value.self == Int16.self { - converter = { $0.int16Value as? Value } - } else if Value.self == Int32.self { - converter = { $0.int32Value as? Value } - } else if Value.self == Int64.self { - converter = { $0.int64Value as? Value } - } else if Value.self == UInt32.self { - converter = { $0.uint32Value as? Value } - } else { - fatalError("Unsupported type: \(Value.self)") - } - } - - public var wrappedValue: Value? { - get { fatalError("Access via enclosing instance required.") } - set { fatalError("Access via enclosing instance required.") } - } - - public static subscript( - _enclosingInstance observed: EnclosingSelf, - wrapped wrappedKeyPath: KeyPath, - storage storageKeyPath: ReferenceWritableKeyPath> - ) -> Value? { - get { - let wrapper = observed[keyPath: storageKeyPath] - let number = observed.primitiveValue(forKey: wrapper.attributeName) as? NSNumber - return number.flatMap { wrapper.converter($0) } - } - set { - let wrapper = observed[keyPath: storageKeyPath] - if let newValue = newValue { - observed.setPrimitiveValue(NSNumber(value: Double("\(newValue)")!), forKey: wrapper.attributeName) - } else { - observed.setPrimitiveValue(nil, forKey: wrapper.attributeName) - } - } - } -} diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index daa38433..2d0417e7 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -177,8 +177,15 @@ actor MeshPackets { do { let fetchedMyInfo = try modelContext.fetch(fetchDescriptor) if fetchedMyInfo.count == 1 { - let newChannel = ChannelEntity() - modelContext.insert(newChannel) + let existing = fetchedMyInfo[0].channels.first(where: { $0.index == Int32(channel.index) }) + let newChannel: ChannelEntity + if let existing { + newChannel = existing + } else { + newChannel = ChannelEntity() + modelContext.insert(newChannel) + fetchedMyInfo[0].channels.append(newChannel) + } newChannel.id = Int32(channel.index) newChannel.index = Int32(channel.index) newChannel.uplinkEnabled = channel.settings.uplinkEnabled @@ -190,11 +197,6 @@ actor MeshPackets { newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision) newChannel.mute = channel.settings.moduleSettings.isMuted } - if let oldIndex = fetchedMyInfo[0].channels.firstIndex(where: { $0.index == newChannel.index }) { - fetchedMyInfo[0].channels[oldIndex] = newChannel - } else { - fetchedMyInfo[0].channels.append(newChannel) - } do { try modelContext.save() } catch { diff --git a/Meshtastic/Model/CoreData/TelemetryEntity+CoreDataClass.swift b/Meshtastic/Model/CoreData/TelemetryEntity+CoreDataClass.swift deleted file mode 100644 index 79dd0485..00000000 --- a/Meshtastic/Model/CoreData/TelemetryEntity+CoreDataClass.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// TelemetryEntity+CoreDataClass.swift -// -// -// Created by Jake Bordens on 12/26/24. -// -// - -import Foundation -import CoreData - -// Manual implementation of the TelemetryEntry object for CoreData. -// Add optional scalar types here using the @ManagedAttribute property wrapper. -// CoreData is based on Objective-C, which doesn't have optional scalars. -// The @ManagedAttribute property wrapper handles the conversion to optional scalars. - -@objc(TelemetryEntity) -public class TelemetryEntity: NSManagedObject, Identifiable { - - @ManagedAttribute(attributeName: "airUtilTx") public var airUtilTx: Float? - @ManagedAttribute(attributeName: "barometricPressure") public var barometricPressure: Float? - @ManagedAttribute(attributeName: "batteryLevel") public var batteryLevel: Int32? - @ManagedAttribute(attributeName: "channelUtilization") public var channelUtilization: Float? - @ManagedAttribute(attributeName: "current") public var current: Float? - @ManagedAttribute(attributeName: "distance") public var distance: Float? - @ManagedAttribute(attributeName: "gasResistance") public var gasResistance: Float? - @ManagedAttribute(attributeName: "iaq") public var iaq: Int32? - @ManagedAttribute(attributeName: "powerCh1Current") var powerCh1Current: Float? - @ManagedAttribute(attributeName: "powerCh1Voltage") var powerCh1Voltage: Float? - @ManagedAttribute(attributeName: "powerCh2Current") var powerCh2Current: Float? - @ManagedAttribute(attributeName: "powerCh2Voltage") var powerCh2Voltage: Float? - @ManagedAttribute(attributeName: "powerCh3Current") var powerCh3Current: Float? - @ManagedAttribute(attributeName: "powerCh3Voltage") var powerCh3Voltage: Float? - @ManagedAttribute(attributeName: "relativeHumidity") public var relativeHumidity: Float? - @ManagedAttribute(attributeName: "rssi") public var rssi: Int32? - @ManagedAttribute(attributeName: "snr") public var snr: Float? - @ManagedAttribute(attributeName: "temperature") public var temperature: Float? - @ManagedAttribute(attributeName: "uptimeSeconds") public var uptimeSeconds: Int32? - @ManagedAttribute(attributeName: "voltage") public var voltage: Float? - @ManagedAttribute(attributeName: "weight") public var weight: Float? - @ManagedAttribute(attributeName: "windDirection") public var windDirection: Int32? - @ManagedAttribute(attributeName: "windGust") public var windGust: Float? - @ManagedAttribute(attributeName: "windLull") public var windLull: Float? - @ManagedAttribute(attributeName: "windSpeed") public var windSpeed: Float? - @ManagedAttribute(attributeName: "irLux") public var irLux: Float? - @ManagedAttribute(attributeName: "lux") public var lux: Float? - @ManagedAttribute(attributeName: "uvLux") public var uvLux: Float? - @ManagedAttribute(attributeName: "whiteLux") public var whiteLux: Float? - @ManagedAttribute(attributeName: "radiation") public var radiation: Float? - @ManagedAttribute(attributeName: "rainfall1H") public var rainfall1H: Float? - @ManagedAttribute(attributeName: "rainfall24H") public var rainfall24H: Float? - @ManagedAttribute(attributeName: "soilTemperature") public var soilTemperature: Float? - @ManagedAttribute(attributeName: "soilMoisture") public var soilMoisture: UInt32? - - public var dewPoint: Float? { - guard let temp = self.temperature, let rh = self.relativeHumidity else { - return nil - } - return Float(calculateDewPoint(temp: temp, relativeHumidity: rh, convertToLocale: false)) - } -} diff --git a/Meshtastic/Model/CoreData/TelemetryEntity+CoreDataProperties.swift b/Meshtastic/Model/CoreData/TelemetryEntity+CoreDataProperties.swift deleted file mode 100644 index 278a322a..00000000 --- a/Meshtastic/Model/CoreData/TelemetryEntity+CoreDataProperties.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// TelemetryEntity+CoreDataProperties.swift -// -// -// Created by Jake Bordens on 12/26/24. -// -// - -import Foundation -import CoreData - -// Manual implementation of the TelemetryEntry object for CoreData. -// Add non-optional scalar types here using the standard @NSManaged proprty wrapper -// Add optional/non-optional object types here using the standard @NSManaged proprty wrapper -// CoreData is based on Objective-C which natively supports optionals for class types and -// non-optional scalars. - -extension TelemetryEntity { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "TelemetryEntity") - } - - @NSManaged public var time: Date? - @NSManaged public var metricsType: Int32 - @NSManaged public var numOnlineNodes: Int32 - @NSManaged public var numPacketsRx: Int32 - @NSManaged public var numPacketsRxBad: Int32 - @NSManaged public var numPacketsTx: Int32 - @NSManaged public var numRxDupe: Int32 - @NSManaged public var numTotalNodes: Int32 - @NSManaged public var numTxRelay: Int32 - @NSManaged public var numTxRelayCanceled: Int32 - @NSManaged public var nodeTelemetry: NodeInfoEntity? - -} diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 7de8413f..bdeb208b 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -239,51 +239,13 @@ struct Channels: View { #endif } } - if node?.myInfo?.channels.count ?? 0 < 8 && node != nil { - - Button { - let channelIndexes = node?.myInfo?.channels.map { Int($0.index) } - let firstChannelIndex = firstMissingChannelIndex(channelIndexes ?? []) - channelKeySize = 16 - let key = generateChannelKey(size: channelKeySize) - channelName = "" - channelIndex = Int32(firstChannelIndex) - channelRole = 2 - channelKey = key - positionsEnabled = false - preciseLocation = false - positionPrecision = 0 - uplink = false - downlink = false - - let newChannel = ChannelEntity() - context.insert(newChannel) - newChannel.id = channelIndex - newChannel.index = channelIndex - newChannel.uplinkEnabled = uplink - newChannel.downlinkEnabled = downlink - newChannel.name = channelName - newChannel.role = Int32(channelRole) - newChannel.psk = Data(base64Encoded: channelKey) ?? Data() - newChannel.positionPrecision = Int32(positionPrecision) - selectedChannel = newChannel - hasChanges = true - - } label: { - Label("Add Channel", systemImage: "plus.square") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - } } .sheet(isPresented: $showingHelp) { ChannelsHelp() .presentationDetents([.large]) .presentationDragIndicator(.visible) } - .safeAreaInset(edge: .bottom, alignment: .leading) { + .safeAreaInset(edge: .bottom) { HStack { Button(action: { withAnimation { @@ -296,9 +258,46 @@ struct Channels: View { .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) + .controlSize(.regular) + Spacer() + if node?.myInfo?.channels.count ?? 0 < 8 && node != nil { + Button { + let channelIndexes = node?.myInfo?.channels.map { Int($0.index) } + let firstChannelIndex = firstMissingChannelIndex(channelIndexes ?? []) + channelKeySize = 16 + let key = generateChannelKey(size: channelKeySize) + channelName = "" + channelIndex = Int32(firstChannelIndex) + channelRole = 2 + channelKey = key + positionsEnabled = false + preciseLocation = false + positionPrecision = 0 + uplink = false + downlink = false + + let newChannel = ChannelEntity() + context.insert(newChannel) + newChannel.id = channelIndex + newChannel.index = channelIndex + newChannel.uplinkEnabled = uplink + newChannel.downlinkEnabled = downlink + newChannel.name = channelName + newChannel.role = Int32(channelRole) + newChannel.psk = Data(base64Encoded: channelKey) ?? Data() + newChannel.positionPrecision = Int32(positionPrecision) + selectedChannel = newChannel + hasChanges = true + } label: { + Label("Add Channel", systemImage: "plus.square") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + } } - .controlSize(.regular) - .padding(5) + .padding(.horizontal, 10) + .padding(.vertical, 5) } .padding(.bottom, 5) .navigationTitle("Channels") diff --git a/MeshtasticTests/ChannelEntityTests.swift b/MeshtasticTests/ChannelEntityTests.swift new file mode 100644 index 00000000..957b10a2 --- /dev/null +++ b/MeshtasticTests/ChannelEntityTests.swift @@ -0,0 +1,50 @@ +import XCTest +import SwiftData +@testable import Meshtastic + +final class ChannelEntityTests: XCTestCase { + var modelContainer: ModelContainer! + var context: ModelContext! + + override func setUp() { + super.setUp() + let schema = Schema([ChannelEntity.self]) + modelContainer = try! ModelContainer(for: schema, configurations: []) + context = ModelContext(modelContainer) + } + + override func tearDown() { + modelContainer = nil + context = nil + super.tearDown() + } + + func testChannelEntityDefaultInit() { + let channel = ChannelEntity() + XCTAssertFalse(channel.downlinkEnabled) + XCTAssertEqual(channel.id, 0) + XCTAssertEqual(channel.index, 0) + XCTAssertFalse(channel.mute) + XCTAssertNil(channel.name) + XCTAssertEqual(channel.positionPrecision, 32) + XCTAssertNil(channel.psk) + XCTAssertEqual(channel.role, 0) + XCTAssertFalse(channel.uplinkEnabled) + XCTAssertNil(channel.myInfoChannel) + } + + func testChannelEntityInsertAndFetch() throws { + let channel = ChannelEntity() + channel.id = 42 + channel.name = "Test Channel" + channel.uplinkEnabled = true + context.insert(channel) + try context.save() + + let descriptor = FetchDescriptor(predicate: #Predicate { $0.id == 42 }) + let fetched = try context.fetch(descriptor) + XCTAssertEqual(fetched.count, 1) + XCTAssertEqual(fetched.first?.name, "Test Channel") + XCTAssertTrue(fetched.first?.uplinkEnabled ?? false) + } +}