From 7b1c6c2078144db58ce039dab391c8c589191add Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 27 May 2025 09:12:25 -0700 Subject: [PATCH 01/22] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Views/Settings/Firmware.swift | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 67778f10..877cd45a 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1806,7 +1806,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.3; + MARKETING_VERSION = 2.6.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1839,7 +1839,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.3; + MARKETING_VERSION = 2.6.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1870,7 +1870,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.3; + MARKETING_VERSION = 2.6.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1902,7 +1902,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.3; + MARKETING_VERSION = 2.6.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index ad885d86..0e21850a 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -197,6 +197,9 @@ struct Firmware: View { } Api().loadFirmwareReleaseData { (fw) in latestStable = fw.releases.stable.first + let archString = currentDevice?.architecture.rawValue ?? "" + let ls = fw.releases.stable.first(where: { $0.zipURL.contains(archString) == true }) + latestStable = fw.releases.stable.first(where: { $0.zipURL.contains(archString) == true }) latestAlpha = fw.releases.alpha.first } } From cf9d832ccef170bbe313144a79d304ce9b94ff72 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 28 May 2025 14:21:13 -0500 Subject: [PATCH 02/22] Updated protos and generation script --- .../Sources/meshtastic/admin.pb.swift | 186 +++++++++ .../Sources/meshtastic/config.pb.swift | 10 + .../Sources/meshtastic/deviceonly.pb.swift | 16 + .../Sources/meshtastic/mesh.pb.swift | 389 ++++++++++++++++++ .../Sources/meshtastic/portnums.pb.swift | 8 + .../Sources/meshtastic/telemetry.pb.swift | 217 ++++++++++ protobufs | 2 +- scripts/gen_protos.sh | 4 + 8 files changed, 831 insertions(+), 1 deletion(-) diff --git a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift index 3f259682..188799b9 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/admin.pb.swift @@ -498,6 +498,16 @@ public struct AdminMessage: @unchecked Sendable { set {payloadVariant = .addContact(newValue)} } + /// + /// Initiate or respond to a key verification request + public var keyVerification: KeyVerificationAdmin { + get { + if case .keyVerification(let v)? = payloadVariant {return v} + return KeyVerificationAdmin() + } + set {payloadVariant = .keyVerification(newValue)} + } + /// /// Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. public var factoryResetDevice: Int32 { @@ -719,6 +729,9 @@ public struct AdminMessage: @unchecked Sendable { /// Add a contact (User) to the nodedb case addContact(SharedContact) /// + /// Initiate or respond to a key verification request + case keyVerification(KeyVerificationAdmin) + /// /// Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. case factoryResetDevice(Int32) /// @@ -1077,6 +1090,98 @@ public struct SharedContact: Sendable { fileprivate var _user: User? = nil } +/// +/// This message is used by a client to initiate or complete a key verification +public struct KeyVerificationAdmin: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var messageType: KeyVerificationAdmin.MessageType = .initiateVerification + + /// + /// The nodenum we're requesting + public var remoteNodenum: UInt32 = 0 + + /// + /// The nonce is used to track the connection + public var nonce: UInt64 = 0 + + /// + /// The 4 digit code generated by the remote node, and communicated outside the mesh + public var securityNumber: UInt32 { + get {return _securityNumber ?? 0} + set {_securityNumber = newValue} + } + /// Returns true if `securityNumber` has been explicitly set. + public var hasSecurityNumber: Bool {return self._securityNumber != nil} + /// Clears the value of `securityNumber`. Subsequent reads from it will return its default value. + public mutating func clearSecurityNumber() {self._securityNumber = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + /// + /// Three stages of this request. + public enum MessageType: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + /// + /// This is the first stage, where a client initiates + case initiateVerification // = 0 + + /// + /// After the nonce has been returned over the mesh, the client prompts for the security number + /// And uses this message to provide it to the node. + case provideSecurityNumber // = 1 + + /// + /// Once the user has compared the verification message, this message notifies the node. + case doVerify // = 2 + + /// + /// This is the cancel path, can be taken at any point + case doNotVerify // = 3 + case UNRECOGNIZED(Int) + + public init() { + self = .initiateVerification + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .initiateVerification + case 1: self = .provideSecurityNumber + case 2: self = .doVerify + case 3: self = .doNotVerify + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .initiateVerification: return 0 + case .provideSecurityNumber: return 1 + case .doVerify: return 2 + case .doNotVerify: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [KeyVerificationAdmin.MessageType] = [ + .initiateVerification, + .provideSecurityNumber, + .doVerify, + .doNotVerify, + ] + + } + + public init() {} + + fileprivate var _securityNumber: UInt32? = nil +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" @@ -1130,6 +1235,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 64: .standard(proto: "begin_edit_settings"), 65: .standard(proto: "commit_edit_settings"), 66: .standard(proto: "add_contact"), + 67: .standard(proto: "key_verification"), 94: .standard(proto: "factory_reset_device"), 95: .standard(proto: "reboot_ota_seconds"), 96: .standard(proto: "exit_simulator"), @@ -1585,6 +1691,19 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .addContact(v) } }() + case 67: try { + var v: KeyVerificationAdmin? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerification(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerification(v) + } + }() case 94: try { var v: Int32? try decoder.decodeSingularInt32Field(value: &v) @@ -1833,6 +1952,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .addContact(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 66) }() + case .keyVerification?: try { + guard case .keyVerification(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 67) + }() case .factoryResetDevice?: try { guard case .factoryResetDevice(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularInt32Field(value: v, fieldNumber: 94) @@ -2040,3 +2163,66 @@ extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa return true } } + +extension KeyVerificationAdmin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".KeyVerificationAdmin" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "message_type"), + 2: .standard(proto: "remote_nodenum"), + 3: .same(proto: "nonce"), + 4: .standard(proto: "security_number"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.messageType) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.remoteNodenum) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 4: try { try decoder.decodeSingularUInt32Field(value: &self._securityNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.messageType != .initiateVerification { + try visitor.visitSingularEnumField(value: self.messageType, fieldNumber: 1) + } + if self.remoteNodenum != 0 { + try visitor.visitSingularUInt32Field(value: self.remoteNodenum, fieldNumber: 2) + } + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 3) + } + try { if let v = self._securityNumber { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationAdmin, rhs: KeyVerificationAdmin) -> Bool { + if lhs.messageType != rhs.messageType {return false} + if lhs.remoteNodenum != rhs.remoteNodenum {return false} + if lhs.nonce != rhs.nonce {return false} + if lhs._securityNumber != rhs._securityNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension KeyVerificationAdmin.MessageType: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "INITIATE_VERIFICATION"), + 1: .same(proto: "PROVIDE_SECURITY_NUMBER"), + 2: .same(proto: "DO_VERIFY"), + 3: .same(proto: "DO_NOT_VERIFY"), + ] +} diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 55e6e5f4..12a57c69 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -756,6 +756,10 @@ public struct Config: Sendable { /// Flags for enabling/disabling network protocols public var enabledProtocols: UInt32 = 0 + /// + /// Enable/Disable ipv6 support + public var ipv6Enabled: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum AddressMode: SwiftProtobuf.Enum, Swift.CaseIterable { @@ -2385,6 +2389,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp 8: .standard(proto: "ipv4_config"), 9: .standard(proto: "rsyslog_server"), 10: .standard(proto: "enabled_protocols"), + 11: .standard(proto: "ipv6_enabled"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2402,6 +2407,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp case 8: try { try decoder.decodeSingularMessageField(value: &self._ipv4Config) }() case 9: try { try decoder.decodeSingularStringField(value: &self.rsyslogServer) }() case 10: try { try decoder.decodeSingularUInt32Field(value: &self.enabledProtocols) }() + case 11: try { try decoder.decodeSingularBoolField(value: &self.ipv6Enabled) }() default: break } } @@ -2439,6 +2445,9 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if self.enabledProtocols != 0 { try visitor.visitSingularUInt32Field(value: self.enabledProtocols, fieldNumber: 10) } + if self.ipv6Enabled != false { + try visitor.visitSingularBoolField(value: self.ipv6Enabled, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -2452,6 +2461,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp if lhs._ipv4Config != rhs._ipv4Config {return false} if lhs.rsyslogServer != rhs.rsyslogServer {return false} if lhs.enabledProtocols != rhs.enabledProtocols {return false} + if lhs.ipv6Enabled != rhs.ipv6Enabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift index cbcbda13..acbc9682 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/deviceonly.pb.swift @@ -227,6 +227,14 @@ public struct NodeInfoLite: @unchecked Sendable { set {_uniqueStorage()._nextHop = newValue} } + /// + /// Bitfield for storing booleans. + /// LSB 0 is_key_manually_verified + public var bitfield: UInt32 { + get {return _storage._bitfield} + set {_uniqueStorage()._bitfield = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -608,6 +616,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 10: .standard(proto: "is_favorite"), 11: .standard(proto: "is_ignored"), 12: .standard(proto: "next_hop"), + 13: .same(proto: "bitfield"), ] fileprivate class _StorageClass { @@ -623,6 +632,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _isFavorite: Bool = false var _isIgnored: Bool = false var _nextHop: UInt32 = 0 + var _bitfield: UInt32 = 0 #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -649,6 +659,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat _isFavorite = source._isFavorite _isIgnored = source._isIgnored _nextHop = source._nextHop + _bitfield = source._bitfield } } @@ -679,6 +690,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() case 11: try { try decoder.decodeSingularBoolField(value: &_storage._isIgnored) }() case 12: try { try decoder.decodeSingularUInt32Field(value: &_storage._nextHop) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &_storage._bitfield) }() default: break } } @@ -727,6 +739,9 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._nextHop != 0 { try visitor.visitSingularUInt32Field(value: _storage._nextHop, fieldNumber: 12) } + if _storage._bitfield != 0 { + try visitor.visitSingularUInt32Field(value: _storage._bitfield, fieldNumber: 13) + } } try unknownFields.traverse(visitor: &visitor) } @@ -748,6 +763,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._isFavorite != rhs_storage._isFavorite {return false} if _storage._isIgnored != rhs_storage._isIgnored {return false} if _storage._nextHop != rhs_storage._nextHop {return false} + if _storage._bitfield != rhs_storage._bitfield {return false} return true } if !storagesAreEqual {return false} diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index d59ec2ed..407d395f 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -442,6 +442,22 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { /// Elecrow CrowPanel Advance models, ESP32-S3 and TFT with SX1262 radio plugin case crowpanel // = 97 + ///* + /// Lilygo LINK32 board with sensors + case link32 // = 98 + + ///* + /// Seeed Tracker L1 + case seeedWioTrackerL1 // = 99 + + ///* + /// Seeed Tracker L1 EINK driver + case seeedWioTrackerL1Eink // = 100 + + /// + /// Reserved ID for future and past use + case qwantzTinyArms // = 101 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -553,6 +569,10 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case 95: self = .seeedSolarNode case 96: self = .nomadstarMeteorPro case 97: self = .crowpanel + case 98: self = .link32 + case 99: self = .seeedWioTrackerL1 + case 100: self = .seeedWioTrackerL1Eink + case 101: self = .qwantzTinyArms case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -658,6 +678,10 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { case .seeedSolarNode: return 95 case .nomadstarMeteorPro: return 96 case .crowpanel: return 97 + case .link32: return 98 + case .seeedWioTrackerL1: return 99 + case .seeedWioTrackerL1Eink: return 100 + case .qwantzTinyArms: return 101 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -763,6 +787,10 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable { .seeedSolarNode, .nomadstarMeteorPro, .crowpanel, + .link32, + .seeedWioTrackerL1, + .seeedWioTrackerL1Eink, + .qwantzTinyArms, .privateHw, ] @@ -1820,6 +1848,31 @@ public struct DataMessage: @unchecked Sendable { fileprivate var _bitfield: UInt32? = nil } +/// +/// The actual over-the-mesh message doing KeyVerification +public struct KeyVerification: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// random value Selected by the requesting node + public var nonce: UInt64 = 0 + + /// + /// The final authoritative hash, only to be sent by NodeA at the end of the handshake + public var hash1: Data = Data() + + /// + /// The intermediary hash (actually derived from hash1), + /// sent from NodeB to NodeA in response to the initial message. + public var hash2: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + /// /// Waypoint message, used to share arbitrary locations across the mesh public struct Waypoint: Sendable { @@ -2441,6 +2494,15 @@ public struct NodeInfo: @unchecked Sendable { set {_uniqueStorage()._isIgnored = newValue} } + /// + /// True if node public key has been verified. + /// Persists between NodeDB internal clean ups + /// LSB 0 of the bitfield + public var isKeyManuallyVerified: Bool { + get {return _storage._isKeyManuallyVerified} + set {_uniqueStorage()._isKeyManuallyVerified = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -2903,13 +2965,94 @@ public struct ClientNotification: Sendable { /// The message body of the notification public var message: String = String() + public var payloadVariant: ClientNotification.OneOf_PayloadVariant? = nil + + public var keyVerificationNumberInform: KeyVerificationNumberInform { + get { + if case .keyVerificationNumberInform(let v)? = payloadVariant {return v} + return KeyVerificationNumberInform() + } + set {payloadVariant = .keyVerificationNumberInform(newValue)} + } + + public var keyVerificationNumberRequest: KeyVerificationNumberRequest { + get { + if case .keyVerificationNumberRequest(let v)? = payloadVariant {return v} + return KeyVerificationNumberRequest() + } + set {payloadVariant = .keyVerificationNumberRequest(newValue)} + } + + public var keyVerificationFinal: KeyVerificationFinal { + get { + if case .keyVerificationFinal(let v)? = payloadVariant {return v} + return KeyVerificationFinal() + } + set {payloadVariant = .keyVerificationFinal(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() + public enum OneOf_PayloadVariant: Equatable, Sendable { + case keyVerificationNumberInform(KeyVerificationNumberInform) + case keyVerificationNumberRequest(KeyVerificationNumberRequest) + case keyVerificationFinal(KeyVerificationFinal) + + } + public init() {} fileprivate var _replyID: UInt32? = nil } +public struct KeyVerificationNumberInform: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var nonce: UInt64 = 0 + + public var remoteLongname: String = String() + + public var securityNumber: UInt32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct KeyVerificationNumberRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var nonce: UInt64 = 0 + + public var remoteLongname: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct KeyVerificationFinal: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var nonce: UInt64 = 0 + + public var remoteLongname: String = String() + + public var isSender: Bool = false + + public var verificationCharacters: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + /// /// Individual File info for the device public struct FileInfo: Sendable { @@ -3431,6 +3574,10 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 95: .same(proto: "SEEED_SOLAR_NODE"), 96: .same(proto: "NOMADSTAR_METEOR_PRO"), 97: .same(proto: "CROWPANEL"), + 98: .same(proto: "LINK_32"), + 99: .same(proto: "SEEED_WIO_TRACKER_L1"), + 100: .same(proto: "SEEED_WIO_TRACKER_L1_EINK"), + 101: .same(proto: "QWANTZ_TINY_ARMS"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -4075,6 +4222,50 @@ extension DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati } } +extension KeyVerification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".KeyVerification" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "nonce"), + 2: .same(proto: "hash1"), + 3: .same(proto: "hash2"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.hash1) }() + case 3: try { try decoder.decodeSingularBytesField(value: &self.hash2) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.hash1.isEmpty { + try visitor.visitSingularBytesField(value: self.hash1, fieldNumber: 2) + } + if !self.hash2.isEmpty { + try visitor.visitSingularBytesField(value: self.hash2, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerification, rhs: KeyVerification) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.hash1 != rhs.hash1 {return false} + if lhs.hash2 != rhs.hash2 {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Waypoint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Waypoint" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -4511,6 +4702,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 9: .standard(proto: "hops_away"), 10: .standard(proto: "is_favorite"), 11: .standard(proto: "is_ignored"), + 12: .standard(proto: "is_key_manually_verified"), ] fileprivate class _StorageClass { @@ -4525,6 +4717,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _hopsAway: UInt32? = nil var _isFavorite: Bool = false var _isIgnored: Bool = false + var _isKeyManuallyVerified: Bool = false #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -4550,6 +4743,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB _hopsAway = source._hopsAway _isFavorite = source._isFavorite _isIgnored = source._isIgnored + _isKeyManuallyVerified = source._isKeyManuallyVerified } } @@ -4579,6 +4773,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }() case 10: try { try decoder.decodeSingularBoolField(value: &_storage._isFavorite) }() case 11: try { try decoder.decodeSingularBoolField(value: &_storage._isIgnored) }() + case 12: try { try decoder.decodeSingularBoolField(value: &_storage._isKeyManuallyVerified) }() default: break } } @@ -4624,6 +4819,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._isIgnored != false { try visitor.visitSingularBoolField(value: _storage._isIgnored, fieldNumber: 11) } + if _storage._isKeyManuallyVerified != false { + try visitor.visitSingularBoolField(value: _storage._isKeyManuallyVerified, fieldNumber: 12) + } } try unknownFields.traverse(visitor: &visitor) } @@ -4644,6 +4842,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if _storage._hopsAway != rhs_storage._hopsAway {return false} if _storage._isFavorite != rhs_storage._isFavorite {return false} if _storage._isIgnored != rhs_storage._isIgnored {return false} + if _storage._isKeyManuallyVerified != rhs_storage._isKeyManuallyVerified {return false} return true } if !storagesAreEqual {return false} @@ -5146,6 +5345,9 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 2: .same(proto: "time"), 3: .same(proto: "level"), 4: .same(proto: "message"), + 11: .standard(proto: "key_verification_number_inform"), + 12: .standard(proto: "key_verification_number_request"), + 13: .standard(proto: "key_verification_final"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -5158,6 +5360,45 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple case 2: try { try decoder.decodeSingularFixed32Field(value: &self.time) }() case 3: try { try decoder.decodeSingularEnumField(value: &self.level) }() case 4: try { try decoder.decodeSingularStringField(value: &self.message) }() + case 11: try { + var v: KeyVerificationNumberInform? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerificationNumberInform(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerificationNumberInform(v) + } + }() + case 12: try { + var v: KeyVerificationNumberRequest? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerificationNumberRequest(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerificationNumberRequest(v) + } + }() + case 13: try { + var v: KeyVerificationFinal? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .keyVerificationFinal(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .keyVerificationFinal(v) + } + }() default: break } } @@ -5180,6 +5421,21 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if !self.message.isEmpty { try visitor.visitSingularStringField(value: self.message, fieldNumber: 4) } + switch self.payloadVariant { + case .keyVerificationNumberInform?: try { + guard case .keyVerificationNumberInform(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() + case .keyVerificationNumberRequest?: try { + guard case .keyVerificationNumberRequest(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + }() + case .keyVerificationFinal?: try { + guard case .keyVerificationFinal(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + }() + case nil: break + } try unknownFields.traverse(visitor: &visitor) } @@ -5188,6 +5444,139 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if lhs.time != rhs.time {return false} if lhs.level != rhs.level {return false} if lhs.message != rhs.message {return false} + if lhs.payloadVariant != rhs.payloadVariant {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension KeyVerificationNumberInform: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".KeyVerificationNumberInform" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "nonce"), + 2: .standard(proto: "remote_longname"), + 3: .standard(proto: "security_number"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.remoteLongname) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.securityNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.remoteLongname.isEmpty { + try visitor.visitSingularStringField(value: self.remoteLongname, fieldNumber: 2) + } + if self.securityNumber != 0 { + try visitor.visitSingularUInt32Field(value: self.securityNumber, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationNumberInform, rhs: KeyVerificationNumberInform) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.remoteLongname != rhs.remoteLongname {return false} + if lhs.securityNumber != rhs.securityNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension KeyVerificationNumberRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".KeyVerificationNumberRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "nonce"), + 2: .standard(proto: "remote_longname"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.remoteLongname) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.remoteLongname.isEmpty { + try visitor.visitSingularStringField(value: self.remoteLongname, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationNumberRequest, rhs: KeyVerificationNumberRequest) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.remoteLongname != rhs.remoteLongname {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension KeyVerificationFinal: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".KeyVerificationFinal" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "nonce"), + 2: .standard(proto: "remote_longname"), + 3: .same(proto: "isSender"), + 4: .standard(proto: "verification_characters"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.nonce) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.remoteLongname) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.isSender) }() + case 4: try { try decoder.decodeSingularStringField(value: &self.verificationCharacters) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.nonce != 0 { + try visitor.visitSingularUInt64Field(value: self.nonce, fieldNumber: 1) + } + if !self.remoteLongname.isEmpty { + try visitor.visitSingularStringField(value: self.remoteLongname, fieldNumber: 2) + } + if self.isSender != false { + try visitor.visitSingularBoolField(value: self.isSender, fieldNumber: 3) + } + if !self.verificationCharacters.isEmpty { + try visitor.visitSingularStringField(value: self.verificationCharacters, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: KeyVerificationFinal, rhs: KeyVerificationFinal) -> Bool { + if lhs.nonce != rhs.nonce {return false} + if lhs.remoteLongname != rhs.remoteLongname {return false} + if lhs.isSender != rhs.isSender {return false} + if lhs.verificationCharacters != rhs.verificationCharacters {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift index cac96bc4..182e233c 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/portnums.pb.swift @@ -111,6 +111,10 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { /// Same as Text Message but used for critical alerts. case alertApp // = 11 + /// + /// Module/port for handling key verification requests. + case keyVerificationApp // = 12 + /// /// Provides a 'ping' service that replies to any packet it receives. /// Also serves as a small example module. @@ -232,6 +236,7 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { case 9: self = .audioApp case 10: self = .detectionSensorApp case 11: self = .alertApp + case 12: self = .keyVerificationApp case 32: self = .replyApp case 33: self = .ipTunnelApp case 34: self = .paxcounterApp @@ -268,6 +273,7 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { case .audioApp: return 9 case .detectionSensorApp: return 10 case .alertApp: return 11 + case .keyVerificationApp: return 12 case .replyApp: return 32 case .ipTunnelApp: return 33 case .paxcounterApp: return 34 @@ -304,6 +310,7 @@ public enum PortNum: SwiftProtobuf.Enum, Swift.CaseIterable { .audioApp, .detectionSensorApp, .alertApp, + .keyVerificationApp, .replyApp, .ipTunnelApp, .paxcounterApp, @@ -342,6 +349,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding { 9: .same(proto: "AUDIO_APP"), 10: .same(proto: "DETECTION_SENSOR_APP"), 11: .same(proto: "ALERT_APP"), + 12: .same(proto: "KEY_VERIFICATION_APP"), 32: .same(proto: "REPLY_APP"), 33: .same(proto: "IP_TUNNEL_APP"), 34: .same(proto: "PAXCOUNTER_APP"), diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index ccf4cfb4..2b89d4bd 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -180,6 +180,10 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { /// /// MAX17261 lipo battery gauge case max17261 // = 38 + + /// + /// PCT2075 Temperature Sensor + case pct2075 // = 39 case UNRECOGNIZED(Int) public init() { @@ -227,6 +231,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case 36: self = .dps310 case 37: self = .rak12035 case 38: self = .max17261 + case 39: self = .pct2075 default: self = .UNRECOGNIZED(rawValue) } } @@ -272,6 +277,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { case .dps310: return 36 case .rak12035: return 37 case .max17261: return 38 + case .pct2075: return 39 case .UNRECOGNIZED(let i): return i } } @@ -317,6 +323,7 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum, Swift.CaseIterable { .dps310, .rak12035, .max17261, + .pct2075, ] } @@ -959,6 +966,14 @@ public struct LocalStats: Sendable { /// This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you. public var numTxRelayCanceled: UInt32 = 0 + /// + /// Number of bytes used in the heap + public var heapTotalBytes: UInt32 = 0 + + /// + /// Number of bytes free in the heap + public var heapFreeBytes: UInt32 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1013,6 +1028,80 @@ public struct HealthMetrics: Sendable { fileprivate var _temperature: Float? = nil } +/// +/// Linux host metrics +public struct HostMetrics: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// Host system uptime + public var uptimeSeconds: UInt32 = 0 + + /// + /// Host system free memory + public var freememBytes: UInt64 = 0 + + /// + /// Host system disk space free for / + public var diskfree1Bytes: UInt64 = 0 + + /// + /// Secondary system disk space free + public var diskfree2Bytes: UInt64 { + get {return _diskfree2Bytes ?? 0} + set {_diskfree2Bytes = newValue} + } + /// Returns true if `diskfree2Bytes` has been explicitly set. + public var hasDiskfree2Bytes: Bool {return self._diskfree2Bytes != nil} + /// Clears the value of `diskfree2Bytes`. Subsequent reads from it will return its default value. + public mutating func clearDiskfree2Bytes() {self._diskfree2Bytes = nil} + + /// + /// Tertiary disk space free + public var diskfree3Bytes: UInt64 { + get {return _diskfree3Bytes ?? 0} + set {_diskfree3Bytes = newValue} + } + /// Returns true if `diskfree3Bytes` has been explicitly set. + public var hasDiskfree3Bytes: Bool {return self._diskfree3Bytes != nil} + /// Clears the value of `diskfree3Bytes`. Subsequent reads from it will return its default value. + public mutating func clearDiskfree3Bytes() {self._diskfree3Bytes = nil} + + /// + /// Host system one minute load in 1/100ths + public var load1: UInt32 = 0 + + /// + /// Host system five minute load in 1/100ths + public var load5: UInt32 = 0 + + /// + /// Host system fifteen minute load in 1/100ths + public var load15: UInt32 = 0 + + /// + /// Optional User-provided string for arbitrary host system information + /// that doesn't make sense as a dedicated entry. + public var userString: String { + get {return _userString ?? String()} + set {_userString = newValue} + } + /// Returns true if `userString` has been explicitly set. + public var hasUserString: Bool {return self._userString != nil} + /// Clears the value of `userString`. Subsequent reads from it will return its default value. + public mutating func clearUserString() {self._userString = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _diskfree2Bytes: UInt64? = nil + fileprivate var _diskfree3Bytes: UInt64? = nil + fileprivate var _userString: String? = nil +} + /// /// Types of Measurements the telemetry module is equipped to handle public struct Telemetry: Sendable { @@ -1086,6 +1175,16 @@ public struct Telemetry: Sendable { set {variant = .healthMetrics(newValue)} } + /// + /// Linux host metrics + public var hostMetrics: HostMetrics { + get { + if case .hostMetrics(let v)? = variant {return v} + return HostMetrics() + } + set {variant = .hostMetrics(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Variant: Equatable, Sendable { @@ -1107,6 +1206,9 @@ public struct Telemetry: Sendable { /// /// Health telemetry metrics case healthMetrics(HealthMetrics) + /// + /// Linux host metrics + case hostMetrics(HostMetrics) } @@ -1178,6 +1280,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 36: .same(proto: "DPS310"), 37: .same(proto: "RAK12035"), 38: .same(proto: "MAX17261"), + 39: .same(proto: "PCT2075"), ] } @@ -1673,6 +1776,8 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 9: .standard(proto: "num_rx_dupe"), 10: .standard(proto: "num_tx_relay"), 11: .standard(proto: "num_tx_relay_canceled"), + 12: .standard(proto: "heap_total_bytes"), + 13: .standard(proto: "heap_free_bytes"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1692,6 +1797,8 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 9: try { try decoder.decodeSingularUInt32Field(value: &self.numRxDupe) }() case 10: try { try decoder.decodeSingularUInt32Field(value: &self.numTxRelay) }() case 11: try { try decoder.decodeSingularUInt32Field(value: &self.numTxRelayCanceled) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self.heapTotalBytes) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &self.heapFreeBytes) }() default: break } } @@ -1731,6 +1838,12 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if self.numTxRelayCanceled != 0 { try visitor.visitSingularUInt32Field(value: self.numTxRelayCanceled, fieldNumber: 11) } + if self.heapTotalBytes != 0 { + try visitor.visitSingularUInt32Field(value: self.heapTotalBytes, fieldNumber: 12) + } + if self.heapFreeBytes != 0 { + try visitor.visitSingularUInt32Field(value: self.heapFreeBytes, fieldNumber: 13) + } try unknownFields.traverse(visitor: &visitor) } @@ -1746,6 +1859,8 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if lhs.numRxDupe != rhs.numRxDupe {return false} if lhs.numTxRelay != rhs.numTxRelay {return false} if lhs.numTxRelayCanceled != rhs.numTxRelayCanceled {return false} + if lhs.heapTotalBytes != rhs.heapTotalBytes {return false} + if lhs.heapFreeBytes != rhs.heapFreeBytes {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1799,6 +1914,90 @@ extension HealthMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa } } +extension HostMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HostMetrics" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "uptime_seconds"), + 2: .standard(proto: "freemem_bytes"), + 3: .standard(proto: "diskfree1_bytes"), + 4: .standard(proto: "diskfree2_bytes"), + 5: .standard(proto: "diskfree3_bytes"), + 6: .same(proto: "load1"), + 7: .same(proto: "load5"), + 8: .same(proto: "load15"), + 9: .standard(proto: "user_string"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.uptimeSeconds) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.freememBytes) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self.diskfree1Bytes) }() + case 4: try { try decoder.decodeSingularUInt64Field(value: &self._diskfree2Bytes) }() + case 5: try { try decoder.decodeSingularUInt64Field(value: &self._diskfree3Bytes) }() + case 6: try { try decoder.decodeSingularUInt32Field(value: &self.load1) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self.load5) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &self.load15) }() + case 9: try { try decoder.decodeSingularStringField(value: &self._userString) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.uptimeSeconds != 0 { + try visitor.visitSingularUInt32Field(value: self.uptimeSeconds, fieldNumber: 1) + } + if self.freememBytes != 0 { + try visitor.visitSingularUInt64Field(value: self.freememBytes, fieldNumber: 2) + } + if self.diskfree1Bytes != 0 { + try visitor.visitSingularUInt64Field(value: self.diskfree1Bytes, fieldNumber: 3) + } + try { if let v = self._diskfree2Bytes { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4) + } }() + try { if let v = self._diskfree3Bytes { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 5) + } }() + if self.load1 != 0 { + try visitor.visitSingularUInt32Field(value: self.load1, fieldNumber: 6) + } + if self.load5 != 0 { + try visitor.visitSingularUInt32Field(value: self.load5, fieldNumber: 7) + } + if self.load15 != 0 { + try visitor.visitSingularUInt32Field(value: self.load15, fieldNumber: 8) + } + try { if let v = self._userString { + try visitor.visitSingularStringField(value: v, fieldNumber: 9) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: HostMetrics, rhs: HostMetrics) -> Bool { + if lhs.uptimeSeconds != rhs.uptimeSeconds {return false} + if lhs.freememBytes != rhs.freememBytes {return false} + if lhs.diskfree1Bytes != rhs.diskfree1Bytes {return false} + if lhs._diskfree2Bytes != rhs._diskfree2Bytes {return false} + if lhs._diskfree3Bytes != rhs._diskfree3Bytes {return false} + if lhs.load1 != rhs.load1 {return false} + if lhs.load5 != rhs.load5 {return false} + if lhs.load15 != rhs.load15 {return false} + if lhs._userString != rhs._userString {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Telemetry" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -1809,6 +2008,7 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 5: .standard(proto: "power_metrics"), 6: .standard(proto: "local_stats"), 7: .standard(proto: "health_metrics"), + 8: .standard(proto: "host_metrics"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1896,6 +2096,19 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.variant = .healthMetrics(v) } }() + case 8: try { + var v: HostMetrics? + var hadOneofValue = false + if let current = self.variant { + hadOneofValue = true + if case .hostMetrics(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.variant = .hostMetrics(v) + } + }() default: break } } @@ -1934,6 +2147,10 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .healthMetrics(let v)? = self.variant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 7) }() + case .hostMetrics?: try { + guard case .hostMetrics(let v)? = self.variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) diff --git a/protobufs b/protobufs index 816595c8..24c7a3d2 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 816595c8bbdfc3b4388e11348ccd043294d58705 +Subproject commit 24c7a3d287a4bd269ce191827e5dabd8ce8f57a7 diff --git a/scripts/gen_protos.sh b/scripts/gen_protos.sh index d07bc798..1ce1d1e9 100755 --- a/scripts/gen_protos.sh +++ b/scripts/gen_protos.sh @@ -5,6 +5,10 @@ if [ ! -x "$(which protoc)" ]; then brew install swift-protobuf fi +git submodule update --init --recursive + +git submodule foreach --recursive git pull origin master + protoc --proto_path=./protobufs --swift_opt=Visibility=Public --swift_out=./MeshtasticProtobufs/Sources ./protobufs/meshtastic/*.proto echo "Done generating the swift files from the proto files." From 453022efe7622ca0b1dc851743309db4ae291cd6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 28 May 2025 15:30:03 -0700 Subject: [PATCH 03/22] Finish removing legacy admin functionality --- Meshtastic/Views/Nodes/Helpers/NodeDetail.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index b2c97074..a420b8bf 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -486,7 +486,7 @@ struct NodeDetail: View { let connectedNode, self.bleManager.connectedPeripheral != nil { Section("Administration") { - if connectedNode.myInfo?.hasAdmin ?? false { + if UserDefaults.enableAdministration { Button { let adminMessageId = bleManager.requestDeviceMetadata( fromUser: connectedNode.user!, From c906bc2baf67a8f1434f75c6c197fee57f5a13c5 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 29 May 2025 12:49:36 -0500 Subject: [PATCH 04/22] Switch is comprehensive now --- Meshtastic/Helpers/BLEManager.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 050958e0..5af2941e 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -978,6 +978,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.info("🕸️ MESH PACKET received for Power Stress App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .reticulumTunnelApp: Logger.mesh.info("🕸️ MESH PACKET received for Reticulum Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + case .keyVerificationApp: + Logger.mesh.warning("🕸️ MESH PACKET received for Key Verification App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { From f8728df6639b81c0f6f10cd64bba58b96957f4d3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 30 May 2025 12:37:14 -0700 Subject: [PATCH 05/22] Map report opt in plumbing --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Helpers/BLEManager.swift | 2 + .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 506 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 1 + 5 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 877cd45a..ec788769 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -352,6 +352,7 @@ DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = ""; }; DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = ""; }; DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = ""; }; + DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 52.xcdatamodel"; sourceTree = ""; }; DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 46.xcdatamodel"; sourceTree = ""; }; DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorEnums.swift; sourceTree = ""; }; DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 28.xcdatamodel"; sourceTree = ""; }; @@ -2001,6 +2002,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */, DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */, 233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */, 8D3F8A3D2D44B137009EAAA4 /* MeshtasticDataModelV 49.xcdatamodel */, @@ -2053,7 +2055,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */; + currentVersion = DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 050958e0..933dc73d 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -978,6 +978,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate Logger.mesh.info("🕸️ MESH PACKET received for Power Stress App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .reticulumTunnelApp: Logger.mesh.info("🕸️ MESH PACKET received for Reticulum Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + case .keyVerificationApp: + Logger.mesh.info("🕸️ MESH PACKET received for Key Verification App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 1853a00f..160ee4b2 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 51.xcdatamodel + MeshtasticDataModelV 52.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents new file mode 100644 index 00000000..c36266d8 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 52.xcdatamodel/contents @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 53da7355..c53b2f73 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -1231,6 +1231,7 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6 newMQTTConfig.jsonEnabled = config.jsonEnabled newMQTTConfig.tlsEnabled = config.tlsEnabled newMQTTConfig.mapReportingEnabled = config.mapReportingEnabled + newMQTTConfig.mapReportingShouldReportLocation = config.mapReportSettings.shouldReportLocation newMQTTConfig.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) newMQTTConfig.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) fetchedNode[0].mqttConfig = newMQTTConfig From 701a06a02d9c6a4c6024714079473891116666b4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 1 Jun 2025 07:00:06 -0700 Subject: [PATCH 06/22] Bump minimum firmare version to match android --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5af2941e..04e32452 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -27,7 +27,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate @Published var automaticallyReconnect: Bool = true @Published var mqttProxyConnected: Bool = false @Published var mqttError: String = "" - public var minimumVersion = "2.3.15" + public var minimumVersion = "2.5.14" public var connectedVersion: String public var isConnecting: Bool = false public var isConnected: Bool = false From 339ecb3aceb6741bd6b36d1ede79e07492daa31b Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 7 Jun 2025 14:58:06 -0700 Subject: [PATCH 07/22] Actually save isClientMuted to the node and switch channelList to a fetch request --- .../CoreData/ChannelEntityExtension.swift | 1 + Meshtastic/Views/Messages/ChannelList.swift | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 57babf4a..c85eef4a 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -32,6 +32,7 @@ extension ChannelEntity { channel.settings.psk = self.psk ?? Data() channel.role = Channel.Role(rawValue: Int(self.role)) ?? Channel.Role.secondary channel.settings.moduleSettings.positionPrecision = UInt32(self.positionPrecision) + channel.settings.moduleSettings.isClientMuted = self.mute return channel } } diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 1123c4ab..61f4fe00 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -26,6 +26,12 @@ 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 + @ViewBuilder private func makeChannelRow( myInfo: MyInfoEntity, @@ -87,6 +93,9 @@ struct ChannelList: View { .foregroundColor(.secondary) } } + if channel.mute { + Image(systemName: "bell.slash") + } } if channel.allPrivateMessages.count > 0 { @@ -103,7 +112,7 @@ struct ChannelList: View { var body: some View { VStack { // Display Contacts for the rest of the non admin channels - if let node, let myInfo = node.myInfo, let channels = myInfo.channels?.array as? [ChannelEntity] { + if let node, let myInfo = node.myInfo { List(selection: $channelSelection) { ForEach(channels) { (channel: ChannelEntity) in if !restrictedChannels.contains(channel.name?.lowercased() ?? "") { @@ -119,7 +128,7 @@ struct ChannelList: View { } } Button { - channel.mute = !channel.mute + channel.mute.toggle() do { let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node.user!, toUser: node.user!) if adminMessageId > 0 { From 807c747182e8e3e9c76dcd8779b91c1f0bca8d95 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:35:48 -0700 Subject: [PATCH 08/22] fix filter --- Meshtastic/Views/Messages/UserList.swift | 409 +++++++++++------------ 1 file changed, 196 insertions(+), 213 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index a74e3fd2..45f922e9 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -21,7 +21,6 @@ struct UserList: View { @State private var isPkiEncrypted = false @State private var isFavorite = false @State private var isIgnored = false - @State private var isUnmessagable = false @State private var isEnvironment = false @State private var distanceFilter = false @State private var maxDistance: Double = 800000 @@ -40,18 +39,6 @@ struct UserList: View { roleFilter ]} - @FetchRequest( - 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)], - predicate: NSPredicate( - format: "userNode.ignored == NO AND unmessagable = NO" - ), animation: .spring - ) - var users: FetchedResults - @Binding var node: NodeInfoEntity? @Binding var userSelection: UserEntity? @@ -61,196 +48,162 @@ struct UserList: View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY") VStack { - List(users, selection: $userSelection) { (user: UserEntity) in - let mostRecent = user.messageList.last - let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) - let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 - let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 - if user.num != bleManager.connectedPeripheral?.num ?? 0 { - NavigationLink(value: user) { - ZStack { - Image(systemName: "circle.fill") - .opacity(user.unreadMessages > 0 ? 1 : 0) - .font(.system(size: 10)) - .foregroundColor(.accentColor) - .brightness(0.2) - } + FilteredUserList( + searchText: searchText, + viaLora: viaLora, + viaMqtt: viaMqtt, + isOnline: isOnline, + isPkiEncrypted: isPkiEncrypted, + isFavorite: isFavorite, + isIgnored: isIgnored, + isUnmessagable: false, + isEnvironment: isEnvironment, + distanceFilter: distanceFilter, + maxDistance: maxDistance, + hopsAway: hopsAway, + roleFilter: roleFilter, + deviceRoles: deviceRoles, + userSelection: $userSelection + ) { users in + List(users, selection: $userSelection) { (user: UserEntity) in + let mostRecent = user.messageList.last + let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) + let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 + let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 + if user.num != bleManager.connectedPeripheral?.num ?? 0 { + NavigationLink(value: user) { + ZStack { + Image(systemName: "circle.fill") + .opacity(user.unreadMessages > 0 ? 1 : 0) + .font(.system(size: 10)) + .foregroundColor(.accentColor) + .brightness(0.2) + } - CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num)))) + CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num)))) - VStack(alignment: .leading) { - HStack { - if user.pkiEncrypted { - if !user.keyMatch { - /// Public Key on the User and the Public Key on the Last Message don't match - Image(systemName: "key.slash") - .foregroundColor(.red) + VStack(alignment: .leading) { + HStack { + if user.pkiEncrypted { + if !user.keyMatch { + /// Public Key on the User and the Public Key on the Last Message don't match + Image(systemName: "key.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + } + Text(user.longName ?? "Unknown".localized) + .font(.headline) + .allowsTightening(true) + Spacer() + if user.userNode?.favorite ?? false { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + } + if user.messageList.count > 0 { + if lastMessageDay == currentDay { + Text(lastMessageTime, style: .time ) + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay == (currentDay - 1) { + Text("Yesterday") + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { + Text(lastMessageTime.formattedDate(format: dateFormatString)) + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay < (currentDay - 1800) { + Text(lastMessageTime.formattedDate(format: dateFormatString)) + .font(.footnote) + .foregroundColor(.secondary) + } + } + } + + if user.messageList.count > 0 { + HStack(alignment: .top) { + Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") + .font(.footnote) + .foregroundColor(.secondary) + } + } + } + } + .frame(height: 62) + .contextMenu { + Button { + if node != nil && !(user.userNode?.favorite ?? false) { + let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) + if success { + user.userNode?.favorite = !(user.userNode?.favorite ?? false) + Logger.data.info("Favorited a node") } } else { - Image(systemName: "lock.open.fill") - .foregroundColor(.yellow) - } - Text(user.longName ?? "Unknown".localized) - .font(.headline) - .allowsTightening(true) - Spacer() - if user.userNode?.favorite ?? false { - Image(systemName: "star.fill") - .foregroundColor(.yellow) - } - if user.messageList.count > 0 { - if lastMessageDay == currentDay { - Text(lastMessageTime, style: .time ) - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay == (currentDay - 1) { - Text("Yesterday") - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { - Text(lastMessageTime.formattedDate(format: dateFormatString)) - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay < (currentDay - 1800) { - Text(lastMessageTime.formattedDate(format: dateFormatString)) - .font(.footnote) - .foregroundColor(.secondary) + let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) + if success { + user.userNode?.favorite = !(user.userNode?.favorite ?? false) + Logger.data.info("Unfavorited a node") } } - } - - if user.messageList.count > 0 { - HStack(alignment: .top) { - Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") - .font(.footnote) - .foregroundColor(.secondary) + context.refresh(user, mergeChanges: true) + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save Node Favorite Error") } - } - } - } - .frame(height: 62) - .contextMenu { - Button { - - if node != nil && !(user.userNode?.favorite ?? false) { - let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) - if success { - user.userNode?.favorite = !(user.userNode?.favorite ?? true) - Logger.data.info("Favorited a node") - } - } else { - let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) - if success { - user.userNode?.favorite = !(user.userNode?.favorite ?? true) - Logger.data.info("Un Favorited a node") - } - } - context.refresh(user, mergeChanges: true) - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save Node Favorite Error") - } - } label: { - Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill") - } - Button { - user.mute = !user.mute - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save User Mute Error") - } - } label: { - Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") - } - if user.messageList.count > 0 { - Button(role: .destructive) { - isPresentingDeleteUserMessagesConfirm = true - userSelection = user } label: { - Label("Delete Messages", systemImage: "trash") + Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill") + } + Button { + user.mute = !user.mute + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save User Mute Error") + } + } label: { + Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") + } + if user.messageList.count > 0 { + Button(role: .destructive) { + isPresentingDeleteUserMessagesConfirm = true + userSelection = user + } label: { + Label("Delete Messages", systemImage: "trash") + } } } - } - .confirmationDialog( - "This conversation will be deleted.", - isPresented: $isPresentingDeleteUserMessagesConfirm, - titleVisibility: .visible - ) { - Button(role: .destructive) { - deleteUserMessages(user: userSelection!, context: context) - context.refresh(node!.user!, mergeChanges: true) - } label: { - Text("Delete") + .confirmationDialog( + "This conversation will be deleted.", + isPresented: $isPresentingDeleteUserMessagesConfirm, + titleVisibility: .visible + ) { + Button(role: .destructive) { + deleteUserMessages(user: userSelection!, context: context) + context.refresh(node!.user!, mergeChanges: true) + } label: { + Text("Delete") + } } } } + .listStyle(.plain) + .navigationTitle(String.localizedStringWithFormat("Contacts (%@)", String(users.count == 0 ? 0 : users.count - 1))) } - .listStyle(.plain) - .navigationTitle(String.localizedStringWithFormat("Contacts (%@)".localized, String(users.count == 0 ? 0 : users.count - 1))) .sheet(isPresented: $editingFilters) { NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } .sheet(isPresented: $showingHelp) { DirectMessagesHelp() } - .onChange(of: searchText) { - Task { - await searchUserList() - } - } - .onChange(of: viaLora) { - if !viaLora && !viaMqtt { - viaMqtt = true - } - Task { - await searchUserList() - } - } - .onChange(of: viaMqtt) { - if !viaLora && !viaMqtt { - viaLora = true - } - Task { - await searchUserList() - } - } - .onChange(of: [deviceRoles]) { - Task { - await searchUserList() - } - } - .onChange(of: hopsAway) { - Task { - await searchUserList() - } - } - .onChange(of: [boolFilters]) { - Task { - await searchUserList() - } - } - .onChange(of: maxDistance) { - Task { - await searchUserList() - } - } - .onChange(of: isPkiEncrypted) { - Task { - await searchUserList() - } - } - .onFirstAppear { - Task { - await searchUserList() - } - } .safeAreaInset(edge: .bottom, alignment: .leading) { HStack { Button(action: { @@ -281,36 +234,61 @@ struct UserList: View { .padding(5) } .padding(.bottom, 5) - .padding(.bottom, 5) - .searchable(text: $searchText, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact") + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Find a contact") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately) } } - private func searchUserList() async { +} - /// Case Insensitive Search Text Predicates - let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in - return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) - } - /// Create a compound predicate using each text search preicate as an OR - let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) - /// Create an array of predicates to hold our AND predicates +struct FilteredUserList: View { + @FetchRequest var fetchRequest: FetchedResults + let content: (FetchedResults) -> Content + + var body: some View { + content(fetchRequest) + } + + init( + searchText: String, + viaLora: Bool, + viaMqtt: Bool, + isOnline: Bool, + isPkiEncrypted: Bool, + isFavorite: Bool, + isIgnored: Bool, + isUnmessagable: Bool, + isEnvironment: Bool, + distanceFilter: Bool, + maxDistance: Double, + hopsAway: Double, + roleFilter: Bool, + deviceRoles: Set, + userSelection: Binding, + @ViewBuilder content: @escaping (FetchedResults) -> Content + ) { + self.content = content + // Build predicates based on filter variables var predicates: [NSPredicate] = [] - /// Mqtt and lora + // Search text predicates + if !searchText.isEmpty { + let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in + return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) + } + let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) + predicates.append(textSearchPredicate) + } + // Mqtt and lora if !(viaLora && viaMqtt) { if viaLora { let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO") predicates.append(loraPredicate) } else { - let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES") + let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES AND userNode.hopsAway == 0") predicates.append(mqttPredicate) } - } else { - let mqttPredicate = NSPredicate(format: "NOT (userNode.viaMqtt == YES)") - predicates.append(mqttPredicate) } - /// Roles + // Roles if roleFilter && deviceRoles.count > 0 { var rolesArray: [NSPredicate] = [] for dr in deviceRoles { @@ -320,7 +298,7 @@ struct UserList: View { let compoundPredicate = NSCompoundPredicate(type: .or, subpredicates: rolesArray) predicates.append(compoundPredicate) } - /// Hops Away + // Hops Away if hopsAway == 0.0 { let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) @@ -328,32 +306,29 @@ struct UserList: View { let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway > 0 AND userNode.hopsAway <= %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) } - /// Online + // Online if isOnline { let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } - /// Encrypted + // Encrypted if isPkiEncrypted { let isPkiEncryptedPredicate = NSPredicate(format: "pkiEncrypted == YES") predicates.append(isPkiEncryptedPredicate) } - /// Favorites + // Favorites if isFavorite { let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") predicates.append(isFavoritePredicate) } - /// Ignored + // Always apply these base filters let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") predicates.append(isIgnoredPredicate) - /// Unmessagable let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") predicates.append(isUnmessagablePredicate) - - /// Distance + // Distance if distanceFilter { let pointOfInterest = LocationsHandler.currentLocation - if pointOfInterest.latitude != LocationsHandler.DefaultLocation.latitude && pointOfInterest.longitude != LocationsHandler.DefaultLocation.longitude { let d: Double = maxDistance * 1.1 let r: Double = 6371009 @@ -368,11 +343,19 @@ struct UserList: View { predicates.append(distancePredicate) } } - if !searchText.isEmpty { - let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates) - users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates]) - } else { - users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) - } + // Combine all predicates + let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) + // Initialize the fetch request with the combined predicate + _fetchRequest = FetchRequest( + 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) + ], + predicate: finalPredicate, + animation: .spring + ) } } From 219a85900a3f3c55e5dee1c32750d38b7357c17a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 17:10:03 -0700 Subject: [PATCH 09/22] Unredact client notification text in the logs --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 04e32452..a4a5389c 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -667,7 +667,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate ) ] manager.schedule() - Logger.data.error("⚠️ Client Notification \((try? decodedInfo.clientNotification.jsonString()) ?? "JSON Decode Failure")") + Logger.data.error("⚠️ Client Notification \((try? decodedInfo.clientNotification.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } switch decodedInfo.packet.decoded.portnum { From 9e1766b90a1075e5c6c526673a3e5875bba6c0b0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 17:54:19 -0700 Subject: [PATCH 10/22] Switch minimum firmware version back --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index a4a5389c..05e38338 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -27,7 +27,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate @Published var automaticallyReconnect: Bool = true @Published var mqttProxyConnected: Bool = false @Published var mqttError: String = "" - public var minimumVersion = "2.5.14" + public var minimumVersion = "2.3.15" public var connectedVersion: String public var isConnecting: Bool = false public var isConnected: Bool = false From 269f784b2598453fd996eda0e118fd97f2e980b2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 20:23:48 -0700 Subject: [PATCH 11/22] Show client notification message in the logs not the JSON --- Meshtastic/Helpers/BLEManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 05e38338..ff822e90 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -659,7 +659,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate manager.notifications = [ Notification( id: UUID().uuidString, - title: "Firmware Notification", + title: "Firmware Notification".localized, subtitle: "\(decodedInfo.clientNotification.level)".capitalized, content: decodedInfo.clientNotification.message, target: "settings", @@ -667,7 +667,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate ) ] manager.schedule() - Logger.data.error("⚠️ Client Notification \((try? decodedInfo.clientNotification.jsonString()) ?? "JSON Decode Failure", privacy: .public)") + Logger.data.error("⚠️ Client Notification: \(decodedInfo.clientNotification.message, privacy: .public)") } switch decodedInfo.packet.decoded.portnum { From af98e807759c9094a4c946ae0e7d4c218ce95700 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 20:38:37 -0700 Subject: [PATCH 12/22] remove force unwrap from users on fetched nodes --- Meshtastic/Persistence/UpdateCoreData.swift | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c53b2f73..22f76f04 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -284,13 +284,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } if nodeInfoMessage.hasUser { /// Seeing Some crashes here ? - fetchedNode[0].user!.userId = nodeInfoMessage.user.id - fetchedNode[0].user!.num = Int64(nodeInfoMessage.num) - fetchedNode[0].user!.longName = nodeInfoMessage.user.longName - fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName - fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) - fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() - fetchedNode[0].user!.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) + fetchedNode[0].user?.userId = nodeInfoMessage.user.id + fetchedNode[0].user?.num = Int64(nodeInfoMessage.num) + fetchedNode[0].user?.longName = nodeInfoMessage.user.longName + fetchedNode[0].user?.shortName = nodeInfoMessage.user.shortName + fetchedNode[0].user?.role = Int32(nodeInfoMessage.user.role.rawValue) + fetchedNode[0].user?.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() + fetchedNode[0].user?.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default if nodeInfoMessage.user.hasIsUnmessagable { fetchedNode[0].user!.unmessagable = nodeInfoMessage.user.isUnmessagable @@ -304,13 +304,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } } if !nodeInfoMessage.user.publicKey.isEmpty { - fetchedNode[0].user!.pkiEncrypted = true - fetchedNode[0].user!.publicKey = nodeInfoMessage.user.publicKey + fetchedNode[0].user?.pkiEncrypted = true + fetchedNode[0].user?.publicKey = nodeInfoMessage.user.publicKey } Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 }) - fetchedNode[0].user!.hwDisplayName = dh?.displayName + fetchedNode[0].user?.hwDisplayName = dh?.displayName } } } @@ -384,7 +384,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } else { position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) } - guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else { + guard let mutablePositions = fetchedNode[0].positions?.mutableCopy() as? NSMutableOrderedSet else { return } /// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one. From 0cdf1aeb1f583a6fab3cf605ae83d2a61483224e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 20:41:08 -0700 Subject: [PATCH 13/22] Missed a spot --- Meshtastic/Persistence/UpdateCoreData.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 22f76f04..a286b08e 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -283,7 +283,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) } if nodeInfoMessage.hasUser { - /// Seeing Some crashes here ? fetchedNode[0].user?.userId = nodeInfoMessage.user.id fetchedNode[0].user?.num = Int64(nodeInfoMessage.num) fetchedNode[0].user?.longName = nodeInfoMessage.user.longName @@ -293,7 +292,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user?.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue) /// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default if nodeInfoMessage.user.hasIsUnmessagable { - fetchedNode[0].user!.unmessagable = nodeInfoMessage.user.isUnmessagable + fetchedNode[0].user?.unmessagable = nodeInfoMessage.user.isUnmessagable } else { let roles = [-1, 2, 4, 5, 6, 7, 10, 11] let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1)) From 7a5330dd4b997464cdc4c32196b50991b5f06d3d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 22:47:18 -0700 Subject: [PATCH 14/22] Slight updates to filter and title --- Meshtastic/Views/Messages/UserList.swift | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 45f922e9..b3e6affd 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -196,7 +196,7 @@ struct UserList: View { } } .listStyle(.plain) - .navigationTitle(String.localizedStringWithFormat("Contacts (%@)", String(users.count == 0 ? 0 : users.count - 1))) + .navigationTitle(String.localizedStringWithFormat("Contacts (%@)", String(users.count))) } .sheet(isPresented: $editingFilters) { NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) @@ -244,11 +244,11 @@ struct UserList: View { struct FilteredUserList: View { @FetchRequest var fetchRequest: FetchedResults let content: (FetchedResults) -> Content - + var body: some View { content(fetchRequest) } - + init( searchText: String, viaLora: Bool, @@ -299,7 +299,7 @@ struct FilteredUserList: View { predicates.append(compoundPredicate) } // Hops Away - if hopsAway == 0.0 { + if hopsAway == 0 { let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) } else if hopsAway > -1.0 { @@ -321,11 +321,14 @@ struct FilteredUserList: View { let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") predicates.append(isFavoritePredicate) } - // Always apply these base filters - let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") - predicates.append(isIgnoredPredicate) - let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") - predicates.append(isUnmessagablePredicate) + // Ignored + if isIgnored { + let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == YES") + predicates.append(isIgnoredPredicate) + } else if !isIgnored { + let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") + predicates.append(isIgnoredPredicate) + } // Distance if distanceFilter { let pointOfInterest = LocationsHandler.currentLocation @@ -343,6 +346,9 @@ struct FilteredUserList: View { predicates.append(distancePredicate) } } + // Always apply unmessagable filter + let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") + predicates.append(isUnmessagablePredicate) // Combine all predicates let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) // Initialize the fetch request with the combined predicate From eda9bdf8adbe62308b82c46f69be3151b164778c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 22:57:00 -0700 Subject: [PATCH 15/22] Update Meshtastic/Views/Messages/UserList.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Messages/UserList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index b3e6affd..1077f911 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -284,7 +284,7 @@ struct FilteredUserList: View { let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO") predicates.append(loraPredicate) } else { - let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES AND userNode.hopsAway == 0") + let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES") predicates.append(mqttPredicate) } } From 7b8980f1edff4cbe7cba4811f96e2ea16a625027 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 23:00:26 -0700 Subject: [PATCH 16/22] Remove unmessagable filter as it is static for the contact list --- Meshtastic/Views/Messages/UserList.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index b3e6affd..7cb9ca55 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -56,7 +56,6 @@ struct UserList: View { isPkiEncrypted: isPkiEncrypted, isFavorite: isFavorite, isIgnored: isIgnored, - isUnmessagable: false, isEnvironment: isEnvironment, distanceFilter: distanceFilter, maxDistance: maxDistance, @@ -257,7 +256,6 @@ struct FilteredUserList: View { isPkiEncrypted: Bool, isFavorite: Bool, isIgnored: Bool, - isUnmessagable: Bool, isEnvironment: Bool, distanceFilter: Bool, maxDistance: Double, From 084da97da62ee0fa83c605e462eb2050b17de004 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 23:12:57 -0700 Subject: [PATCH 17/22] Dont be dumb --- Meshtastic/Views/Messages/UserList.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 09bc8045..37749998 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -344,9 +344,11 @@ struct FilteredUserList: View { predicates.append(distancePredicate) } } - // Always apply unmessagable filter + // Always apply unmessagable and connected node filters let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") predicates.append(isUnmessagablePredicate) + let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS \(UserDefaults.preferredPeripheralNum))") + predicates.append(isConnectedNodePredicate) // Combine all predicates let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) // Initialize the fetch request with the combined predicate From 2e92b838c4bd76be1de01298f7f0a98b584181d0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 23:16:19 -0700 Subject: [PATCH 18/22] Update Meshtastic/Views/Messages/UserList.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Messages/UserList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 37749998..079b9593 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -347,7 +347,7 @@ struct FilteredUserList: View { // Always apply unmessagable and connected node filters let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") predicates.append(isUnmessagablePredicate) - let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS \(UserDefaults.preferredPeripheralNum))") + let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS %@)", UserDefaults.preferredPeripheralNum) predicates.append(isConnectedNodePredicate) // Combine all predicates let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) From 8152642c6ca7732250185f3f35946a86d21aaa06 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 10 Jun 2025 23:31:51 -0700 Subject: [PATCH 19/22] Try and fix logo on ios 26 --- Meshtastic/Views/Helpers/MeshtasticLogo.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Helpers/MeshtasticLogo.swift b/Meshtastic/Views/Helpers/MeshtasticLogo.swift index 84040d92..627ffdca 100644 --- a/Meshtastic/Views/Helpers/MeshtasticLogo.swift +++ b/Meshtastic/Views/Helpers/MeshtasticLogo.swift @@ -19,19 +19,20 @@ struct MeshtasticLogo: View { .renderingMode(.template) .foregroundColor(.accentColor) .scaledToFit() + .offset(x: -15) } .padding(.bottom, 5) .padding(.top, 5) - .offset(x: -15) + #else VStack { Image(colorScheme == .dark ? "logo-white" : "logo-black") .resizable() .renderingMode(.template) .scaledToFit() + .offset(x: -15) } .padding(.bottom, 5) - .offset(x: -15) #endif } } From 0e3d99c4580bf69b6f5d7f91d86223286824ace4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 11 Jun 2025 06:49:09 -0700 Subject: [PATCH 20/22] Fix userlist connected node predicate --- Meshtastic/Views/Helpers/MeshtasticLogo.swift | 6 ------ Meshtastic/Views/Messages/UserList.swift | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Meshtastic/Views/Helpers/MeshtasticLogo.swift b/Meshtastic/Views/Helpers/MeshtasticLogo.swift index 627ffdca..c6906f15 100644 --- a/Meshtastic/Views/Helpers/MeshtasticLogo.swift +++ b/Meshtastic/Views/Helpers/MeshtasticLogo.swift @@ -11,26 +11,20 @@ struct MeshtasticLogo: View { @Environment(\.colorScheme) var colorScheme var body: some View { - #if targetEnvironment(macCatalyst) VStack { Image("logo-white") .resizable() - .renderingMode(.template) .foregroundColor(.accentColor) .scaledToFit() - .offset(x: -15) } .padding(.bottom, 5) .padding(.top, 5) - #else VStack { Image(colorScheme == .dark ? "logo-white" : "logo-black") .resizable() - .renderingMode(.template) .scaledToFit() - .offset(x: -15) } .padding(.bottom, 5) #endif diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 079b9593..72ba1206 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -347,7 +347,7 @@ struct FilteredUserList: View { // Always apply unmessagable and connected node filters let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") predicates.append(isUnmessagablePredicate) - let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS %@)", UserDefaults.preferredPeripheralNum) + let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS %@)", String(UserDefaults.preferredPeripheralNum)) predicates.append(isConnectedNodePredicate) // Combine all predicates let finalPredicate = predicates.isEmpty ? NSPredicate(value: true) : NSCompoundPredicate(type: .and, subpredicates: predicates) From f1d69ac5bb88f9666d42f51ce978eb57d3a58548 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 11 Jun 2025 09:25:22 -0700 Subject: [PATCH 21/22] Revert non working changes to firmware link for latest stable version --- Meshtastic/Views/Settings/Firmware.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 0e21850a..2380b677 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -199,7 +199,7 @@ struct Firmware: View { latestStable = fw.releases.stable.first let archString = currentDevice?.architecture.rawValue ?? "" let ls = fw.releases.stable.first(where: { $0.zipURL.contains(archString) == true }) - latestStable = fw.releases.stable.first(where: { $0.zipURL.contains(archString) == true }) + latestStable = fw.releases.stable.first latestAlpha = fw.releases.alpha.first } } From 7466c648e6e91951116aa3d2217977fc9d396c72 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 11 Jun 2025 09:55:29 -0700 Subject: [PATCH 22/22] Remove ignored filter from node filter. --- Meshtastic/Views/Messages/UserList.swift | 10 ++------ .../Views/Nodes/Helpers/NodeListFilter.swift | 25 ++++++++++--------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 72ba1206..41642582 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -319,14 +319,6 @@ struct FilteredUserList: View { let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") predicates.append(isFavoritePredicate) } - // Ignored - if isIgnored { - let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == YES") - predicates.append(isIgnoredPredicate) - } else if !isIgnored { - let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") - predicates.append(isIgnoredPredicate) - } // Distance if distanceFilter { let pointOfInterest = LocationsHandler.currentLocation @@ -347,6 +339,8 @@ struct FilteredUserList: View { // Always apply unmessagable and connected node filters let isUnmessagablePredicate = NSPredicate(format: "unmessagable == NO") predicates.append(isUnmessagablePredicate) + let isIgnoredPredicate = NSPredicate(format: "userNode.ignored == NO") + predicates.append(isIgnoredPredicate) let isConnectedNodePredicate = NSPredicate(format: "NOT (numString CONTAINS %@)", String(UserDefaults.preferredPeripheralNum)) predicates.append(isConnectedNodePredicate) // Combine all predicates diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 063e073a..1a02cfd7 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -91,20 +91,21 @@ struct NodeListFilter: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) - Toggle(isOn: $isIgnored) { - - Label { - Text("Ignored") - } icon: { - - Image(systemName: "minus.circle.fill") - .symbolRenderingMode(.multicolor) - } - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) if filterTitle == "Node Filters" { + Toggle(isOn: $isIgnored) { + + Label { + Text("Ignored") + } icon: { + + Image(systemName: "minus.circle.fill") + .symbolRenderingMode(.multicolor) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + Toggle(isOn: $isEnvironment) { Label { Text("Environment")