Additional fixes

This commit is contained in:
Garth Vander Houwen 2026-04-16 15:32:49 -07:00
parent add5d3f9e5
commit f78f6ed20e
8 changed files with 100 additions and 226 deletions

View file

@ -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 = "<group>"; };
233E99C62D84A70900CC3A77 /* SoilCompactWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoilCompactWidgets.swift; sourceTree = "<group>"; };
233E99CA2D85AAA900CC3A77 /* RainfallCompactWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RainfallCompactWidget.swift; sourceTree = "<group>"; };
2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedAttributePropertyWrapper.swift; sourceTree = "<group>"; };
2344A2AD2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TelemetryEntity+CoreDataClass.swift"; sourceTree = "<group>"; };
2344A2AE2D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TelemetryEntity+CoreDataProperties.swift"; sourceTree = "<group>"; };
2346A7182E2FB9A300CB9239 /* SerialConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConnection.swift; sourceTree = "<group>"; };
2346A71C2E2FB9C500CB9239 /* SerialTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialTransport.swift; sourceTree = "<group>"; };
2349A0492EAE4DA30060A581 /* ManualConnectionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualConnectionList.swift; sourceTree = "<group>"; };
@ -822,15 +818,6 @@
path = "Compact Widgets";
sourceTree = "<group>";
};
2344A2AC2D66978000170A77 /* CoreData */ = {
isa = PBXGroup;
children = (
2344A2AD2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift */,
2344A2AE2D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift */,
);
path = CoreData;
sourceTree = "<group>";
};
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 */,

View file

@ -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<NodeInfoEntity>(predicate: #Predicate { $0.num == destNum || $0.num == fromNodeNum })
do {

View file

@ -1,63 +0,0 @@
//
// ManagedAttributePropertyWrapper.swift
// Meshtastic
//
// Created by Jake Bordens on 12/26/24.
//
import CoreData
@propertyWrapper
public struct ManagedAttribute<Value: Numeric> {
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<EnclosingSelf: NSManagedObject>(
_enclosingInstance observed: EnclosingSelf,
wrapped wrappedKeyPath: KeyPath<EnclosingSelf, Value?>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, ManagedAttribute<Value>>
) -> 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)
}
}
}
}

View file

@ -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 {

View file

@ -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<Float>(attributeName: "airUtilTx") public var airUtilTx: Float?
@ManagedAttribute<Float>(attributeName: "barometricPressure") public var barometricPressure: Float?
@ManagedAttribute<Int32>(attributeName: "batteryLevel") public var batteryLevel: Int32?
@ManagedAttribute<Float>(attributeName: "channelUtilization") public var channelUtilization: Float?
@ManagedAttribute<Float>(attributeName: "current") public var current: Float?
@ManagedAttribute<Float>(attributeName: "distance") public var distance: Float?
@ManagedAttribute<Float>(attributeName: "gasResistance") public var gasResistance: Float?
@ManagedAttribute<Int32>(attributeName: "iaq") public var iaq: Int32?
@ManagedAttribute<Float>(attributeName: "powerCh1Current") var powerCh1Current: Float?
@ManagedAttribute<Float>(attributeName: "powerCh1Voltage") var powerCh1Voltage: Float?
@ManagedAttribute<Float>(attributeName: "powerCh2Current") var powerCh2Current: Float?
@ManagedAttribute<Float>(attributeName: "powerCh2Voltage") var powerCh2Voltage: Float?
@ManagedAttribute<Float>(attributeName: "powerCh3Current") var powerCh3Current: Float?
@ManagedAttribute<Float>(attributeName: "powerCh3Voltage") var powerCh3Voltage: Float?
@ManagedAttribute<Float>(attributeName: "relativeHumidity") public var relativeHumidity: Float?
@ManagedAttribute<Int32>(attributeName: "rssi") public var rssi: Int32?
@ManagedAttribute<Float>(attributeName: "snr") public var snr: Float?
@ManagedAttribute<Float>(attributeName: "temperature") public var temperature: Float?
@ManagedAttribute<Int32>(attributeName: "uptimeSeconds") public var uptimeSeconds: Int32?
@ManagedAttribute<Float>(attributeName: "voltage") public var voltage: Float?
@ManagedAttribute<Float>(attributeName: "weight") public var weight: Float?
@ManagedAttribute<Int32>(attributeName: "windDirection") public var windDirection: Int32?
@ManagedAttribute<Float>(attributeName: "windGust") public var windGust: Float?
@ManagedAttribute<Float>(attributeName: "windLull") public var windLull: Float?
@ManagedAttribute<Float>(attributeName: "windSpeed") public var windSpeed: Float?
@ManagedAttribute<Float>(attributeName: "irLux") public var irLux: Float?
@ManagedAttribute<Float>(attributeName: "lux") public var lux: Float?
@ManagedAttribute<Float>(attributeName: "uvLux") public var uvLux: Float?
@ManagedAttribute<Float>(attributeName: "whiteLux") public var whiteLux: Float?
@ManagedAttribute<Float>(attributeName: "radiation") public var radiation: Float?
@ManagedAttribute<Float>(attributeName: "rainfall1H") public var rainfall1H: Float?
@ManagedAttribute<Float>(attributeName: "rainfall24H") public var rainfall24H: Float?
@ManagedAttribute<Float>(attributeName: "soilTemperature") public var soilTemperature: Float?
@ManagedAttribute<UInt32>(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))
}
}

View file

@ -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<TelemetryEntity> {
return NSFetchRequest<TelemetryEntity>(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?
}

View file

@ -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")

View file

@ -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<ChannelEntity>(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)
}
}